How to use next.js 14 zustand in client component

330 Views Asked by At

I would like to fetch data in my layout.tsx and then share across my entire app. In order to achieve that I am fetching the data in layout and pass it into the Provider.

layout.tsx

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const res = await fetch(
    "https://65ca740b3b05d29307e05057.mockapi.io/productsserver",
    {
      cache: "no-cache",
      next: {
        tags: ["products"],
      },
    }
  );

  const products: Product[] = await res.json();

  return (
    <html lang="en">
        <ProductStoreProvider products={products}>
          <body className={inter.className}>{children}</body>
        </ProductStoreProvider>
    </html>
  );
}

ProductProvider.tsx

"use client";
import { useState, createContext, useContext } from "react";
import { Product } from "@/actions/actions";

interface ProductState {
  products: Product[];
}

const createStore = (products: ProductState) => ({
  products,
});

const ProductContext = createContext<ReturnType<typeof createStore> | null>(
  null
);

const ProductStoreProvider = ({
  products,
  children,
}: {
  products: ProductState;
  children: React.ReactNode;
}) => {
  const [store] = useState(() => createStore(products));

  console.log("Product Provider", store);
  //  { product: 'Shirt', price: '798.00', id: '9' },
  //  { product: 'Fish', price: '114.00', id: '10' },
  //  { product: 'Pants', price: '645.00', id: '11' },
  return (
    <ProductContext.Provider value={store}>{children}</ProductContext.Provider>
  );
};

export const useProductStore = () => {
  if (!ProductContext)
    throw new Error(
      "useProductStore must be used within a ProductStoreProvider"
    );
  return useContext(ProductContext)!;
};

export default ProductStoreProvider;

I added a sanity check console.log() in there which gives me the output I expect.

In the page.tsx which is a server component I fetch the data again - because that's apparently how to do this.

export default async function Home() {
  const res = await fetch(
    "https://65ca740b3b05d29307e05057.mockapi.io/productsserver",
    {
      cache: "no-cache",
      next: {
        tags: ["products"],
      },
    }
  );

  const products: Product[] = await res.json();

  return (
    <main className="">
      <h1 className="text-3xl font-bold text-center">Server Products</h1>
           <h2 className="font-bold p-5">List of Products</h2>
      <div className="flex gap-5 flex-wrap">
        {products.map((product) => (
          <div key={product.id} className="p-5 shadow">
            <p>{product.product}</p>
            <p>{product.price}</p>
          </div>
        ))}
      </div>
      <ClientOverview />

    </main>
  );
}

The problem is the client component:

"use client";

import { useProductStore } from "@/store/ProductStore";

function ClientOverview() {
  const { products } = useProductStore();

  console.log(products);  // Empty Array []

  return (
    <div className="flex gap-5 flex-wrap">
      <h1>Client Producuts</h1>
      {products.map((product) => (
        <div key={product.id} className="p-5 shadow">
          <p>{product.product}</p>
          <p>{product.price}</p>
        </div>
      ))}
    </div>
  );
}

export default ClientOverview;

even though inside the provider it tells me that it fetched the data its not coming back from the store. How do I pass fresh data down to the client component?

2

There are 2 best solutions below

5
Micheal Palliparambil On

Could be a couple of issues.

Perhaps if the client component(ClientOverview()) is loading first before the state is loaded, data could be empty in the state.

This could be the reason why const { products } = useProductStore() is returning empty array.

Can you implement this:

{products.length !=0 ?( <div className="flex gap-5 flex-wrap">
      <h1>Client Producuts</h1>
      {products.map((product) => (
        <div key={product.id} className="p-5 shadow">
          <p>{product.product}</p>
          <p>{product.price}</p>
        </div>
      ))}
    </div>)
:
(
// else load a temporary div to for faster error debugging
<div>state not available</div>
)
}

Now that you have a indicator to see if the state has been loaded:-

Try to figure out, if the state is loaded initially before the clientoverview() works.

Instead of fetching the api, set up the state manually to see if the clientcomponent is rendering!!

0
StackMyFlow On

I found a solution that will either get me a new job or shot but I hope it works for someone in dire need.

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {

  return (
    <html lang="en">
      <body className={inter.className}>
        <TanStackProvider>{children}</TanStackProvider>
      </body>
    </html>
  );
}

wrap the app in Tanstackquery if you want to get the data in the server component then do this:

export default async function Home() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ["products"],
    queryFn: getData,
  });

  const products = dehydrate(queryClient).queries[0].state.data;

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
        <h2 className="font-bold p-5">List of Products</h2>
        <div className="flex gap-5 flex-wrap">
          {products.map((product) => (
            <div key={product.id} className="p-5 shadow">
              <p>{product.product}</p>
              <p>{product.price}</p>
            </div>
          ))}
        </div>
        <ClientOverview />
    </HydrationBoundary>
  );
}

so prefetch the query on the server then dehydrate it on the server to access the data for server rendering and pass it on to the client where it can be used

function ClientOverview() {
  //const { products } = useProductStore();

  //console.log(products); // Empty Array []
  const { data, isLoading, isError } = useQuery({
    queryKey: ["products"],
    queryFn: getData,
    refetchOnMount: false,
    refetchOnReconnect: false,
  });

  return (
    <div className="flex gap-5 flex-wrap">
      <h1>Client Producuts</h1>
      {data?.map((product) => (
        <div key={product.id} className="p-5 shadow">
          <p>{product.product}</p>
          <p>{product.price}</p>
        </div>
      ))}
    </div>
  );
}

export default ClientOverview;

I would have really prefered to use zustand and the provider stuff but can't get it to work.