Replies: 5 comments 5 replies
-
I'm just writing to say, I'm looking for the same insight. Really interested if Legend State supports hydration/SSR properly? |
Beta Was this translation helpful? Give feedback.
-
I unfortunately don't have much experience with SSR - our apps are all delivered as static pages that run client-side with Firebase. So it would take me some time to get up to speed on what the best practices for SSR look like. Would someone be able to create a sandbox/repo showing how you'd expect Legend-State to work with app router? And then if it doesn't work already, we can fix it to work properly. There's certainly no reason it wouldn't be able to work - it just may need a tweak or helper functions to add support if it's not already supported. |
Beta Was this translation helpful? Give feedback.
-
I'm currently working on a project which uses Next.js - newest version with App Router. As state lib we are using Zustand, but on a separate branch we are testing Legend. So far we are pretty excited and faced no issues. We plan to finish tests by Saturday, so on Sunday I can create a dedicated repo, using all the experience we have gathered through tests. Would that suffice? |
Beta Was this translation helpful? Give feedback.
-
I have managed to get legend to work in Next.js app directory, on both the server and the client. The client state can be initialised using the state from the server. For example, "params" from page props can be loaded into legend state, enabling all components to have access to params or searchParams on both the server and client, without using useSearchParams (which forces the whole route to be rendered client side, unless you wrap each consuming component in a suspense boundary which leads to bad DX) @jmeistrich Here is a resource that I found very useful for understanding server / client state in React Server Components, particularly using Next.js app dir: https://www.youtube.com/watch?v=OpMAH2hzKi8 The most relevant part of this video is that zustands useStore.getState() and useStore.setState() can be used on the server as they are not hooks, and this is the same for legend state observables. Another key similarity is the need to initialize the state inside both a server AND a client component, otherwise the client component will not show the server state. An overview of how to make it work:
outcome = easily use server state across server components, and access the prefetched server state as the initial values inside client components, while still being able to change the state client side as expected. Here is an example and some more explanation of how I got it to work. First, created observable in separate file (exporting from page.tsx can cause some next errors) // app/testing/testing-store.ts
import { observable, observe } from '@legendapp/state';
import { TestingPageProps } from '~/app/testing/page';
export const testingStore = observable({
props: { searchParams: {}, params: {} } as TestingPageProps,
count: 0,
});
observe(() => {
console.log(testingStore.get());
}); Then to initialize the client store with server data, a component must be created in a separate file with "use client" at the top of the file. I had some issues here with migrating the logic from zustand in the linked video. Using the useRef singleton pattern from the video did not properly init the client store, however, wrapping it in a useEffect did work. 'use client';
import React from 'react';
import { testingStore } from '~/app/testing/testing-store';
export function InitClientTestingStore(props: { state: any }) {
const { state } = props;
const initialized = React.useRef(false);
React.useEffect(() => {
if (!initialized.current) {
testingStore.set((prev) => ({ ...prev, ...state }));
// not sure if this has bad performance implications? any feedback welcome
initialized.current = true;
}
}, [initialized]);
return null;
} This InitClientTestingStore approach did work but led to a delay in the client state showing the server's data. Simply removing the useRef and useEffect hooks eliminates the delay but it may have other implications? // example from a different page, but still relevant
'use client';
import { recipePageStore } from '~/stores/recipe-page-store';
export function InitClientStoreRecipePageId(props: { state: any }) {
const { state } = props;
recipePageStore.set((prev) => ({ ...prev, ...state }));
return null;
}
Then data from server can be used to initialize both the server & client state // app/testing/page.tsx
import { Client } from '~/app/testing/client';
import { CountBtns } from '~/app/testing/count-btns';
import { InitClientTestingStore } from '~/app/testing/init-client-store';
import { Server } from '~/app/testing/server';
import { testingStore } from '~/app/testing/testing-store';
export interface TestingPageProps {
params: {
slug: string;
};
searchParams: {
a: string;
};
}
// note: ASYNC function used, enabling data fetching on server, can initialize client state with prefetched data
// note: when deploying to prod, must use ' export const dynamic = "force-dynamic" ' OR generateStaticParams in order for the server to correctly provide the params
export default async function TestingPage(props: TestingPageProps) {
const {} = props;
// initialize the server state
testingStore.props.set(props);
return (
<>
{/* initialize the client state */}
<InitClientTestingStore state={{ props }} />
<Server />
<Client />
<CountBtns />
</>
);
} Now to see if the testing page state is working, Server & Client components are used. CountBtns is used to test whether we can initialize the client observable with server state, while keeping reactivity - which was an issue I had with zustand that led me to trying Legend state. And indeed, all 3 components indicate that legend state works very well with RSC / Next.js app dir! import { Flex, Text } from '@chakra/react';
import { testingStore } from '~/app/testing/testing-store';
export interface ServerProps {}
export function Server(props: ServerProps) {
const {} = props;
return (
<>
<Flex flexDir="column" m={5}>
server state:
<Text>count = {JSON.stringify(testingStore.count.get())}</Text>
<Text>
searchParam.a ={JSON.stringify(testingStore?.props?.searchParams?.a?.get())}
</Text>
</Flex>
</>
);
} 'use client';
import { Flex, Text } from '@chakra/react';
import { Memo } from '@legendapp/state/react';
import { testingStore } from '~/app/testing/testing-store';
export interface ClientProps {}
export const Client = (props: ClientProps) => {
const {} = props;
return (
<>
<Memo>
{() => (
<Flex flexDir="column" m={5}>
client state:
<Text>count = {JSON.stringify(testingStore.count.get())}</Text>
<Text>
searchParam.a ={JSON.stringify(testingStore?.props?.searchParams?.a?.get())}
</Text>
</Flex>
)}
</Memo>
</>
);
}; 'use client';
import { Button, Flex } from '@chakra/react';
import { testingStore } from '~/app/testing/testing-store';
export interface CountBtnsProps {}
export function CountBtns(props: CountBtnsProps) {
const {} = props;
return (
<>
<Flex>
<Button
onClick={() => {
testingStore.count.set(testingStore.count.get() + 1);
}}
>
inc
</Button>
<Button onClick={() => testingStore.count.set((prev) => prev--)}>dec</Button>
</Flex>
</>
);
}
This is a very positive outcome and I'm tremendously excited to be switching over to using Legend state! I really belive that this package has the potential to solve a lot of complexity relating to state management across server and client components. Thank you for working on this, I really appreciate your work and really hope that you continue to develop the library and improve the docs. I'm happy to talk more about the next server implementation if needed, you can reach me on discord @dawidcodes One area which I think it would be really great to improve on is the DX of initalizing both the client and server state separately. The current DX is not optimal and I hope this could be improved. Using zustand for server & client state has the same problem (as you can see in the video) so I've tried to create a reusable function that abstracts the initialization of server and client state by combining it into one component. I couldn't quite figure out a solution to improve the DX but perhaps details about my attempts could help you find a better solution faster. Problem 1: setting the state twice leads to code duplication NOTE: The examples below were quick experiments built for use with zustand, so treat it as pseudo code and ingore the types / zustand syntax. The logic is still relevant to legend state. example 1: export default async function RecipeIdPage(props: RecipeIdPageProps) {
const { params } = props;
const { slug } = params || {};
if (!slug) notFound();
const recipe = await getRecipeDetails(slug);
if (!recipe) notFound();
// creating variable for newState to avoid some repetition
const newState = {
recipe,
props,
servings: recipe.servings.quantity ?? 1,
};
// init server state
recipePageStore.set((prev) => ({
...prev,
...newState,
}));
return (
<>
{/* init client state */}
<InitClientStoreRecipePageId state={newState} />
{/* rest of code ... */}
</>
);
} Solution: combine setState logic into single component "InitStore" // NOTE: no "use client" directive
export interface InitStoreProps {
useStore: any;
state: any;
InitClientStore: any;
}
export function InitStore(props: InitStoreProps) {
const { state, useStore, InitClientStore } = props;
// set server state
useStore.setState(state);
return (
<>
{/* set client state */}
<InitClientStore state={state} />
</>
);
}
//use case example
export default async function RecipeIdPage(props: RecipeIdPageProps) {
const { params } = props;
const { slug } = params || {};
if (!slug) notFound();
const recipe = await getRecipeDetails(slug);
if (!recipe) notFound();
return (
<>
<InitStore
useStore={useRecipeIdPageStore}
InitClientStore={InitClientRecipeIdPageStore}
state={{ recipe, props, servings: recipe.servings.quantity }}
/>
)
</>
);
}
This solution does indeed work and means that setting the state has less code duplication, however, it still needes to be ported over to fully support the legend syntax Problem 2: The "InitClientMyStore" componenent must be created in a file with "use client" at the top, while the server initialisation must not have this at the top of the file. Creating multipe files just to initialise state is a suboptimal DX. Attempted solution: create InitClientMyStore + set server state in a single component by using factory functions eg importing a factory function createInitClientStore which is exported from a file with the "use client" directive. However, this solution does not work as Next.js does not allow server components to call factory functions returning client components. One work around could be to pass the store function as a prop to a generic InitClient component but passing functions from server/ client is not allowed either. // attempted solution: factory functions
// InitClientMyStore factory function
'use client';
import React from 'react';
export function createInitClientStore(useStore: any) {
return function InitClientStore(props: { state: any }) {
const { state } = props;
const initialized = React.useRef(false);
React.useEffect(() => {
if (!initialized.current) {
useStore.setState(state);
initialized.current = true;
}
}, [initialized]);
return null;
};
}
// InitStore (server + client joint in one file)
// NOTE: without "use client" directive
export interface InitStoreProps {
useStore: any;
state: any;
}
export function InitStore(props: InitStoreProps) {
const { state, useStore } = props;
// set server state
useStore.setState(state);
const InitClientStore = createInitClientStore(useStore);
return (
<>
{/* set client state */}
<InitClientStore state={state} />
</>
);
}
// use case example
export default async function RecipeIdPage(props: RecipeIdPageProps) {
const { params } = props;
const { slug } = params || {};
if (!slug) notFound();
const recipe = await getRecipeDetails(slug);
if (!recipe) notFound();
return (
<>
<InitStore
useStore={useRecipeIdPageStore}
state={{ recipe, props, servings: recipe.servings.quantity }}
// if it worked, then there would be no need to create a InitClient component
/>
)
</>
);
}
Again, This solution does not work as Next.js does not allow server components to call factory functions that return client components, or to pass functions to client component props. I couldn't find a way around this so my current solution still requires multiple files. I would really appreciate any help in creating a solution that enables sharing state across server and client with good DX! Thanks you for making legend state open source and, taking the time to read this! |
Beta Was this translation helpful? Give feedback.
-
I created this sandbox I am encountering a problem with Next.js server components. I can get and set the state in server components but when I pass the state from the server to the client component through props the observable proxy changes to an array losing get, set, and onChange all functions. If I pass state from client to client through props it works as expected. Here's my code sandbox https://codesandbox.io/p/sandbox/upbeat-leavitt-ly5fpp?file=%2Fapp%2Fpage.tsx%3A1%2C1 Also, I am unable to use Reactive components in server components you need to put "use client" on top of the components. It's an easy fix. |
Beta Was this translation helpful? Give feedback.
-
I'd like to use it in my application, however I'm seeking for examples in next js (ideally with the newest App Router) to show how the state is synchronized between the server and the client.
Beta Was this translation helpful? Give feedback.
All reactions