Search code examples
next.jsapp-routerstoryblok

Nextjs mix server and client components


I am currently using Nextjs v14 with app router, seems like I dont understand how to deal with server and client components. Docs here say you can wrap a ServerComponent inside a ClientComponent. Exactly what I want.

<ClientComponent>
  <ServerComponent />
</ClientComponent>

But everything I try ends up in an error like async/await is not yet supported in Client Components, only Server Components

All I want to do is fetching all blog posts in a server component.

Layout: server component
StoryblokProvider: client component
Page: server component
BlogOverviewPage: server component
AllBlogs: should be server component

My component tree looks something like:

Layout > StoryblokProvider > Page > BlogOverviewPage > AllBlogs

Layout:

export default async function RootLayout({
  children,
  params: { locale },
}: Props) {


  return (
    <StoryblokProvider>
      <html lang={locale}>
        <body>
            {children}
        </body>
      </html>
    </StoryblokProvider>
  )
}

StoryblokProvider:

/** 1. Tag it as a client component */
"use client"

import { apiPlugin, storyblokInit } from "@storyblok/react"

import { storyblokComponents } from "@/components/storyblok/storyblok-components"

/** 2. Initialize it as usual */
storyblokInit({
  accessToken: process.env.NEXT_PUBLIC_STORYBLOK_TOKEN,
  use: [apiPlugin],
  components: storyblokComponents,
})

const StoryblokProvider = ({ children }: { children: React.ReactNode }) => {
  return children
}

export default StoryblokProvider

Page:

const Page = async ({ params }: Props) => {
  const { data } = await fetchData(params.slug)

  return (
    <>
      <CaptchaProvider>
        <StoryblokStory story={data.story} bridgeOptions={bridgeOptions} />
        <div id="pewl" />
      </CaptchaProvider>
    </>
  )
}

export default Page

async function fetchData(slug: string) {
  slug = slug ?? "home"

  if (Array.isArray(slug)) {
    slug = slug.join("/")
  }

  const storyblokApi: StoryblokClient = getStoryblokApi()
  return storyblokApi.getStory(slug)
}

AllBlogs:

const AllBlogs = async () => {
  const blogs = await fetchAllBlogs()

  return blogs.map((blog: BlogPageStoryblok) => (
    <BlogCard blog={blog} key={blog.uuid} />
  ))
}

const fetchAllBlogs = cache(async () => {
  let sbParams: ISbStoriesParams = {
    starts_with: "blog",
  }

  const storyblokApi: StoryblokClient = getStoryblokApi()

  const { data } = await storyblokApi.get("cdn/stories/", sbParams)
  return data.stories.filter(
    (story: StoryblokStory<BlogPageStoryblok>) => !story.is_startpage
  )
})

I also tried wrapping my AllBlogs in an extra ClientComponent but ends with the same error.

"use client"

export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  return <>{children}</>
}
const BlogOverviewPage = ({ blok }: any) => {

  return (
    <ClientComponent>
      <AllBlogs />
    </ClientComponent>
  )
}

UPDATE:

If I replace StoryblokStory with my AllBlogs component within Page.tsx, my blogs are fetched on the server side, but because of replacing StoryblokStory all other pages are not working anymore.

const Page = async ({ params }: Props) => {
  const { data } = await fetchData(params.slug)

  return (
    <CaptchaProvider>
      <BlogOverviewPage blok={data.story} />
      {/*<StoryblokStory story={data.story} bridgeOptions={bridgeOptions} />*/}
      <div id="pewl" />
    </CaptchaProvider>
  )
}

export default Page

async function fetchData(slug: string) {
  slug = slug ?? "home"

  if (Array.isArray(slug)) {
    slug = slug.join("/")
  }

  const storyblokApi: StoryblokClient = getStoryblokApi()
  return storyblokApi.getStory(slug)
}


Solution

  • StoryblokStory is a client component since it will include the bridge state management.

    Currently you cannot use async components for Bloks in Storyblok / Nextjs13+ together with live editing (the full bridge).

    What you can actually do is fetch the blog posts on your top level Page if your content elements include a component of that type and then just pass it along to the StoryblokStory as a prop.

    You will get that information right next to the blok prop.

    TL;DR Fetch on page level depending on page contents and pass as props.

    Other solution is to not use Visual Live Editing and go full on Server Components (user StoryblokComponent instead of StoryblokStory) but then you won’t see live edits and changes will show only after hitting save.