-
Notifications
You must be signed in to change notification settings - Fork 16
chore: React SDK POC #287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
chore: React SDK POC #287
Conversation
@storyblok/astro
storyblok
@storyblok/eslint-config
@storyblok/js
storyblok-js-client
@storyblok/management-api-client
@storyblok/nuxt
@storyblok/react
@storyblok/region-helper
@storyblok/richtext
@storyblok/svelte
@storyblok/vue
commit: |
| <Bloks {...props} blok={blok} story={story} fallback={FallbackComponent}> | ||
| <Blok | ||
| component="featured-articles-section" | ||
| element={FeaturedArticlesSection} | ||
| /> | ||
| <Blok component="grid-section" element={GridSection} /> | ||
| <Blok component="hero-section" element={HeroSection} /> | ||
| <Blok component="image-text-section" element={ImageTextSection} /> | ||
| <Blok component="tabbed-content-section" element={TabbedContentSection} /> | ||
| <Blok component="text-section" element={TextSection} /> | ||
| <Blok component="banner" element={Banner} /> | ||
| <Blok component="banner-reference" element={BannerReference} /> | ||
| <Blok | ||
| component="latest-articles-section" | ||
| element={LatestArticlesSection} | ||
| /> | ||
| <Blok component="faq-section" element={FaqSection} /> | ||
| <Blok component="logo-section" element={LogoSection} /> | ||
| <Blok | ||
| component="newsletter-form-section" | ||
| element={NewsletterFormSection} | ||
| /> | ||
| <Blok component="personalized-section" element={PersonalizedSection} /> | ||
| <Blok component="products-section" element={ProductsSection} /> | ||
| <Blok component="testimonials-section" element={TestimonialsSection} /> | ||
| <Blok component="article-overview-page" element={ArticleOverviewPage} /> | ||
| <Blok component="article-page" element={ArticlePage} /> | ||
| <Blok component="category" element={Category} /> | ||
| <Blok component="contact-form-section" element={ContactFormSection} /> | ||
| <Blok component="price-card" element={PriceCard} /> | ||
| <Blok component="grid-card" element={GridCard} /> | ||
| <Blok component="image-card" element={ImageCard} /> | ||
| <Blok component="richtext-youtube" element={RichtextYoutube} /> | ||
| <Blok component="tabbed-content-entry" element={TabbedContentEntry} /> | ||
| <Blok component="headline-segment" element={HeadlineSegment} /> | ||
| <Blok component="default-page" element={DefaultPage} /> | ||
| <Blok component="nav-item" element={NavItem} /> | ||
| </Bloks> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This the crux of the API. It's basically a gloried switch statement. This is user generated code and users can create as many or as few of these as they choose to target different rendering contexts.
| slug = slug.join("/"); | ||
| } | ||
|
|
||
| const { story, header } = await fetchPage(slug); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Async data fetching happens on the server and the result is populated in the SWR cache as fallback. This keeps the cache primed for the subsequent useSWR hook calls.
| [`cdn/stories/${slug}`]: story, | ||
| "cdn/stories/site-config": header, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache keys simply need to match the request params for useSWR. Similar patterns exist in other data fetching libraries
| const fetcher = (key: string) => getStory(key).then(res => res.data.story); | ||
|
|
||
| export const useStory = <T = unknown>(slug: string) => { | ||
| const { data, error, isLoading } = useSWR(`cdn/stories/${slug}`, fetcher); | ||
|
|
||
| return { story: data as ISbStoryData<T>, error, isLoading }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user creates their own purpose built hook around their chosen data fetching library.
| | StoryblokComponents.ArticlePage; | ||
|
|
||
| export function Story({ slug }: { slug: string }) { | ||
| const { story, error, isLoading } = useStory<Component>(slug); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a user hook that wraps the their data fetching library (SWR in this case) using the js client. Since the SWR cache is already pre-populated in the Server Component, this will simply return the data needed to complete SSR.
On the client, the SWR cache is re-hydrated and bridge updates will mutate the cache and trigger live updates.
| bridge.on("input", (event) => { | ||
| if (!event || !event.story) return; | ||
| mutate(`cdn/stories/${event.story.slug}`, event.story, { | ||
| revalidate: false, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SWR cache is keyed by slug which is globally unique within the space. When a live update occurs we mutate the cache which triggers a re-render with the updated data. This occurs on the client only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@imranolas do you think is this something that could be part of the SDK?
Also, have you tried reusing the useStoryblokBridge exported from @storyblok/js? I'd suggest it for reusability as it performs a few extra checks:
- Is in editor
- Is the same story, by checking id (needed when you set multiple listeners in the same page
- Already implements the
changeandpublishedevents
Adding the code suggestion below
| const bridge = new window.StoryblokBridge({ | ||
| resolveRelations: resolveRelations, | ||
| }); | ||
|
|
||
| bridge.on("input", (event) => { | ||
| if (!event || !event.story) return; | ||
| mutate(`cdn/stories/${event.story.slug}`, event.story, { | ||
| revalidate: false, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const bridge = new window.StoryblokBridge({ | |
| resolveRelations: resolveRelations, | |
| }); | |
| bridge.on("input", (event) => { | |
| if (!event || !event.story) return; | |
| mutate(`cdn/stories/${event.story.slug}`, event.story, { | |
| revalidate: false, | |
| }); | |
| }); | |
| const cb = event => mutate(`cdn/stories/${event.story.slug}`, event.story, { revalidate: false }); | |
| useStoryblokBridge(story.id, cb, { resolveRelations }); |
(would need BridgeHandler to have the story from [[...slug]]/page.tsx to be passed as property)
A reimagining of the Storyblok React SDK to focus purely on rendering and component registration, removing data fetching concerns and letting users handle their own data strategy.
Changes
useStoryblokApi,useStoryblokState)Next.js 15 Demo
Why is this better?
Reviewers notes
This PR is a bit of a monster but it broadly falls into the following pieces:
resolves WDX-123