-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
SSR Example (SSR data is shared) #182
Comments
The useSessionStore is a store itself as well as a hook. So, it's a singleton. A singleton can't be used for such session data. I'd say it's not meant for this, but other comments from someone are welcome. |
@dai-shi Thanks for taking a look. Yeah I see some Next.js example attempts, but nothing official. Can anybody confirm if this is a no-go for SSR? What do others do/use? |
@jd327 Does SSR work if a use case allow to have data shared? What are the examples? Pointers are welcome. We need to understand how such a library like zustand supports SSR, but personally if there's a way, we would like to add a support for it. |
@dai-shi Thanks for following up. A bit of context: for this new Next.js app, I was hoping to avoid having one global Redux state. If I could just use Zustand hooks alone throughout the app, that would be a big improvement in simplicity. Previously, I've had to use this: https://github.com/kirill-konshin/next-redux-wrapper From what I see so far there's an issue with the way state data is stored/transferred from server to client when using Zustand. When I set data on the server, it seems to persist between requests (another browser will get the same data), which I am guessing means it is stored somewhere in Node.js memory and shared..? Ideally, I'd just like it to transfer from server to client per request. Example: server reads session cookie > server pulls data from somewhere > server calls |
Yes, this is true. But, it must be true for Redux too. We definitely need some more people to help on this issue. |
That'd be super helpful. I'm currently supplementing with
Perhaps @kirill-konshin could chime in on what the magic is? Edit: Just fyi, there was also a nextjs example repo attempt, but looks like it was never finalized. |
|
@kirill-konshin Thank you for the info! @dai-shi Here's a little example repo, which is approximately what I started with: https://codesandbox.io/s/jovial-germain-vyvxo. I'm not sure their preview window will work, you might have to copy files locally. To reproduce:
This is where it seems the logged-in "session" seems to be stored on the server and shared across requests. Because otherwise, the user would always be logged-out on hard refresh. Also if you open a new browser and go to the homepage immediately after seeing that warning, you should see the same (until Node.js releases that variable). A few random notes:
Edit: Also, I didn't include Edit 2: However, here's a Next.js example that uses context: https://github.com/vercel/next.js/tree/canary/examples/with-context-api |
I just ran into this issue and it was nasty for the UX. A simple solution after a complex debugging session is to simply clear your store when the component unmounts. Let me know if this helps.
If you wanted to cache your zustand store you could create a map with keys that represent the page id. This isn't ideal for sensitive data and it can hog memory if you're persisting a lot of data to it across your app but it's a strategic option if you want to load things faster. |
Folks, I finally made an example on my own. It's based on https://github.com/vercel/next.js/tree/canary/examples/with-mobx (suggested in vercel/next.js#11222, @janek26 is still around?) https://codesandbox.io/s/nextjs-with-zustand-x4z2g Now, I understand what you mean by "SSR data is shared." It's not yet solved in the example currently. |
I didn't test this too much and I don't know if this is a great way to do this but I believe this code sandbox (based on @dai-shi clock example) shows a way to solve the "SRR data is shared" problem. It uses React context and creates a Zustand store singleton in the React render vs importing and using the singleton directly. The other somewhat nice thing about this is the store singleton is set in a |
@mkartchner994 Hey, thanks for the example! I think it's valid to use context to avoid the global singleton. But, I'm not really sure if we should recommend this pattern, simply because the zustand api is not designed well for contexts. At the same time, I don't come up with better ideas. So, unless some next.js experts suggest something else, we'd have no other choices... |
I've created a PR in next.js to add a with-zustand example. It uses the information from this discussion using a context provider. @dai-shi I think it should be fine to add the zustand store into context. I have done a little digging into react-redux and i believe it uses the same approach. The redux store is placed into context and then the useStore/useSelector hook access the context to get the redux store. Would be great to get your feedback and I'll make any changes as necessary. |
@callumbooth As for adding zustand store (which is actually Let's see the general example. const Ctx = createContext()
const Component = () => {
const useStore = useContext(Ctx)
const [count] = useStore() // this violates the rule of hooks
return <div>{count}</div>
}
const App = () => {
const [enabled, setEnabled] = useState(false)
const useStore = () => useState(0)
return (
<div>
<button onClick={() => setEnabled(x => !x)}>Toggle</button>
<Ctx.Provider value={enabled ? useStore : () => [0] }>
<Component />
</Ctx.Provider>
</div>
)
} |
I see what you're say. I'm not sure a way around it, hooks has many gotcha's like this. If the responsibility for creating the provider is moved into zustand does that reduce the chance that someone would do this? I'm assuming redux would have the same problem as the one you're describing? |
No, react-redux doesn't have this issue, because it doesn't pass a hook in context. Hooks are imported directly.
Yeah, that's possible. We don't want to change the core api, but middleware can. |
So I've been thinking about it and wanted to discuss it here before starting on any PR. I agree I'm not a fan of putting a hook into the provider feels too easy to misuse, so I wondered if putting the vanilla store into the provider instead is the way to go. As I understand it the src/index create function is an abstraction around the src/vanilla create function to ensure a store exists and then the logic for the useStore hook which closes over the store. What if we moved that logic so it can be shared between the src/index create function and an another hook that would abstract around getting the store from context? Something like this: //src/index.js
export function sharedUseStore(selector, equalityFn, api) {
//...logic goes here
}
export default function create(createState) {
const api =
typeof createState === 'function' ? createImpl(createState) : createState
const useStore = (selector, equalityFn) => sharedUseStore(selector, equalityFn, api)
//...rest of create function
}
export function useStoreFromProvider(selector, equalityFn) {
const api = useContext(zustandContext)
const values = sharedUseStore(selector, equalityFn, api)
return values
} This is just a rough idea and needs refining to avoid/reduce any impact to the core api. Let me know your thoughts and if this heading towards the right direction or not. |
We had some internal discussions before releasing v3 and one of my suggestions is to export two functions like Now, if I understand it correctly, your suggestion is not a breaking change to the current v3 api, but adding a new function to export. Let's call it The way I understand is this: // we can do it what we do currently.
import create from 'zustand'
const useStore = create(...)
const Component = () => {
const values = useStore(selector) // this is the new pattern
import create, { useStore } from 'zustand'
const store = create(...)
const Component = () => {
const values = useStore(store, selector) Hm, this can be done with middleware without changing the core like below: import create from 'zustand'
import { useStore } from 'middleware' I guess this is what I proposed in my previous comment. Rereading your comment, I guess what you really mean is this. import create from 'zustand/vanilla'
import { useStore } from 'zustand'
const store = create(...)
const Component = () => {
const values = useStore(store, selector) This actually works in v3, however we can't support this pattern in v4 (#160). That's why we are hesitant for this. (using vanilla in addition is not good for bundling either. zustand/vanilla is intended to be used without react.) Are you on the same page? Happy to help if something is ambiguous. I might misunderstand your suggestion, so I'd wait for your response. |
Hmm, I was actually thinking that the exported useStore function would be so that it can be imported by the middleware but actually the middleware could use the create function as it accepts a vanilla store. So the useStoreFromProvider hook could be // middleware/index.js file
import create from 'zustand'
export function useStoreFromProvider(selector, equalityFn) {
const api = useContext(zustandContext)
const useStore = create(api)
return useStore(selector, qualityFn)
}
// someComponent.js
import {useStoreFromProvider} from 'middleware'
const Component = () => {
const values = useStoreFromProvider(selector, eqFn) I'm sticking with the provider as a requirement because with SSR (and nextjs) we need to create a new store every request so we need a way saving a reference to that new store, which allows any component within the tree access it. The provider would contain the vanilla store created by a factory function. I'm probably missing something but I think this would still be compatible with v4. |
@callumbooth Thanks for putting your brain to this issue.
This works in v3, but in v4 it has to be something like below:
(which is dependent on react, so can't be done in src/vanilla.ts.) Does this work in practice? (apart from it being weird.) // src/middleware.js
import create from 'zustand'
const ZustandContext = createContext()
export const ZustandProvider = ({ createState, children }) => {
const store = create(createState)
return (
<ZustandContext.Provider value={{ store }}>
{chidlren}
</ZustandContext.Provider>
)
}
export const useZustandStore = (selector, eqlFn) => {
const store = useContext(ZustandContext)
const useStore = store // this is the weird part
return useStore(selector, eqlFn)
} |
As this doesn’t change the core api, it should be fairly easy to get a poc created. I’ll try to get one created tomorrow. Thanks for you help on this @dai-shi |
Sorry taken me a little longer to get this created. Here is an initial poc which works when navigating between different pages but if you navigate to the same page, the page component seems to keep a reference to the old store so the page stops working. Any help would be appreciated to get this working https://codesandbox.io/s/vanilla-store-example-vim2c |
Not sure if it's related to your current issue, but export function useStore(selector, equalityFn) {
const store = useContext(StoreContext);
const useStore = create(store); This won't work in the future. We need to create store just once, not per hook. (In other words, don't use |
Forgive me if I'm not understanding correctly but wont this line mean it isn't creating a new store just using the store we pass in? https://github.com/pmndrs/zustand/blob/master/src/index.ts#L31
|
Here it is, zustand PR #375 + next.js: You can test against @clbn's redux+next.js example, which is (hopefully) the equivalent of the example above: |
* context utils for SSR * move useIsomorphicLayoutEffect to new file * added build commands * 1. remove the useEffect we dont need anymore 2. wrap context and hook into createZustand() function, but keep defaults * issue #182 - changed name to createContext and useStore + added tests * remove default Provider * use alias to index file * change 'createState' prop to 'initialStore' that accepts useStore func/object from create(). This is needed as store access is needed for merging/memoizing, in next.js integration * updated tests * code review changes * snapshot update * add a section in readme * chore imports and so on Co-authored-by: daishi <daishi@axlight.com>
Next.js PR - vercel/next.js#24884 |
@Munawwar @dai-shi We'd been waiting for Provider to set initial states for SSR hydration and Storybook, and thank you for your contributions. However, I don't understand why // store.ts
const { Provider, useStore } = ourWrappedCreateStore(...);
export { Provider, useStore };
// root.component.jsx
const RootComponent = (propsBySSR) => {
return (
// `propsBySSR` is generated in SSR
<Provider initialState={propsBySSR.initialState}>
<Main />
</Provider>
);
};
// root.stories.jsx
export default {
title: "Root",
};
const Template = () => <Root />;
export const Default = Root.bind({});
Default.args = {
// can set in Storybook
initialState: { count: 999 },
};
// foo.component.jsx
const FooComponent = () => {
// can import `useStore` from `store.ts`
const count = useStore((state) => state.count);
...
}; In most cases, we want to create Also, Are our ideas correct? Are there any technical problems? Is this feature not for this case? |
If I don't misunderstand, you can create it with initialState. const RootComponent = (propsBySSR) => {
return (
<Provider initialStore={create(() => propsBySSR.initialState)}>
<Main />
</Provider>
);
}; But, seems like there's misunderstanding. (Would be good to support lazy init, if this is a common pattern.) |
@jagaapple I think that's your confusion - on Though it is not just for TS, it also gives autocomplete/intellisense on editors even on JS codebases, if you pass a good state structure 😏. I have a JS project, and it was a good touch to have that autocomplete. But if it is causing more confusion than good, then we probably need to re-think it @dai-shi. Maybe solve it with some jsdoc? or rename parameter to "initialStateExample"? |
Merged (that was quick). https://github.com/vercel/next.js/tree/canary/examples/with-zustand. We can add this to README now. |
jsdoc sounds good to start. not really sure how much it would mitigate. thoughts? @jagaapple
Any suggestion? Maybe under |
Ok, I slept over it.. my opinion changed.. let's remove it. Also because many SSR websites may have different initial states structure/type per page and JS folks probably wouldn't even bother using it. If they still want autocomplete they can use jsdoc type assertion on the exported useStore ( |
@Munawwar I'm fine with removing it. Please open a PR. |
@Munawwar Sorry for being late. Yes, I agree that |
Ahh, I understand the concept of the API. As @dai-shi wrote, putting initial states on the return value of However, I still don't understand the advantage of giving // -- Give Store instead of Initial States (Current)
// we have to define a function to create a store to merge initial states by SSR and others.
// also we need to call `createStore({})` when we want to use the store outside React Components.
const createStore = (initialState) => {
return create((set) => ({
count: 0,
setCount: (value) => set({ count: value }),
...initialState,
});
};
const { Provider, useStore } = createContext();
const RootComponent = (propsBySSR) => {
// to improve performance, it's recommended to memoize creating a store.
const initialStore = useMemo(() => createStore(propsBySSR), [propsBySSR]);
return (
<Provider initialStore={initialStore}>
<Main />
</Provider>
);
}; // -- Give Initial States instead of Store
// we can create a store following README.
const store = create((set) => ({
count: 0,
setCount: (value) => set({ count: value }),
});
const { Provider, useStore } = createContext(store);
const RootComponent = (propsBySSR) => {
// we don't need to memoize anything and just give initial states to `initialState` Prop of Provider.
return (
<Provider initialState={propsBySSR}>
<Main />
</Provider>
);
}; The above code (giving initial states instead of a store) is just idea, and may have some technical problems in practice. I'm grateful that the Provider was provided in any design, and I just want to know why it was designed to pass a store. 🙂 |
@jagaapple I understand your proposition. I think it's a design choice. <Provider initialStore={store1}>
...
<Provider initialStore={store2}> |
I got it. My idea can allow nested Providers as well because const { Provider: Provider1, useStore: useStore1 } = createContext(store1);
const { Provider: Provider2, useStore: useStore2 } = createContext(store2);
<Provider1> // also can omit initialState in this design
<Provider2 initialState={xxx}>
...
</Provider2>
</Provider1> |
Yes, but it's different from single useStore with nested Providers for DI. I don't know the use case, but it's technically different. |
We kept |
Found this issue after trying the example from the https://github.com/vercel/next.js repo Is there a typed version of the same example with typescript? I'm having a hard time trying to figure it out |
v4 has new APIs for context use case. It would be nice to update the example. |
Hey all, I'm trying to implement the example SSR pattern in a new Next.js app. The problem I'm facing is that I read state outside of components frequently (ex. It doesn't seem like this is possible with the stores created for SSR. Is there a way to accomplish the above with SSR? Thanks in advance! |
Should be possible if you expose the context or a custom hook that use the context: const Component = () => {
const store = useContext(zustandContext)
const handleClick = () => {
console.log('foo is: ', store.getState().foo)
}
// ...
} |
I made my own version of store creator to help me sync up query (an abstract
zustand patchdiff --git a/node_modules/zustand/react.d.ts b/node_modules/zustand/react.d.ts
index 8646581..aedd8a3 100644
--- a/node_modules/zustand/react.d.ts
+++ b/node_modules/zustand/react.d.ts
@@ -1,9 +1,9 @@
import type { Mutate, StateCreator, StoreApi, StoreMutatorIdentifier } from './vanilla';
-type ExtractState<S> = S extends {
+export type ExtractState<S> = S extends {
getState: () => infer T;
} ? T : never;
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>;
-type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
+export type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
getServerState?: () => ExtractState<S>;
};
export declare function useStore<S extends WithReact<StoreApi<unknown>>>(api: S): ExtractState<S>; The store creator helper: // store.ts
import React from "react";
import {
createStore as createZustandStore,
useStore,
StateCreator,
StoreMutatorIdentifier,
StoreApi,
WithReact,
ExtractState,
} from "zustand";
type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any
? R
: never;
// Type in whatever your init data source is
export type InitDataSource = unknown;
// curried useStore hook type recreation
type UseStoreCurried<T> = {
<S extends WithReact<StoreApi<T>>>(): ExtractState<S>;
<S extends WithReact<StoreApi<T>>, U>(
selector: (state: ExtractState<S>) => U,
equalityFn?: (a: U, b: U) => boolean,
): U;
<TState, StateSlice>(
selector: (state: TState) => StateSlice,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
): StateSlice;
};
export const createStore = <
T,
Mos extends [StoreMutatorIdentifier, unknown][] = [],
>(
stateCreator: StateCreator<T, [], Mos>,
getInitData?: (initDataSource: InitDataSource) => Partial<T>,
) => {
const StoreContext = React.createContext<StoreApi<T> | undefined>(undefined);
const useStoreCurried: UseStoreCurried<T> = (...args: unknown[]) => {
const store = React.useContext(StoreContext);
if (!store) {
throw new Error("Expected to have store context");
}
return useStore(store, ...(args as ParametersExceptFirst<typeof useStore>));
};
const Provider = React.memo<
React.PropsWithChildren<{ initDataSource: InitDataSource }>
>(({ children, initDataSource }) => {
const [store] = React.useState(() => {
const boundStore = createZustandStore<T, Mos>(stateCreator);
if (getInitData) {
boundStore.setState(getInitData(initDataSource));
}
return boundStore;
});
return React.createElement(
StoreContext.Provider,
{ value: store },
children,
);
});
return { Provider, useStore: useStoreCurried };
}; A slightly shorter version to run the day we'll have getUseStore to curry the storeimport React from "react";
import {
createStore as createZustandStore,
getUseStore,
UseStoreCurried,
StateCreator,
StoreMutatorIdentifier,
StoreApi,
} from "zustand";
// Type in whatever your init data source is
export type InitDataSource = unknown;
export const createStore = <
T,
Mos extends [StoreMutatorIdentifier, unknown][] = [],
>(
stateCreator: StateCreator<T, [], Mos>,
getInitData?: (initDataSource: InitDataSource) => Partial<T>,
) => {
const StoreContext = React.createContext<StoreApi<T> | undefined>(undefined);
const useStore: UseStoreCurried<T> = (...args) => {
const store = React.useContext(StoreContext);
if (!store) {
throw new Error("Expected to have store context");
}
const useStoreCurried = React.useMemo(() => getUseStore(store), [store]);
return useStoreCurried(...args);
};
const Provider = React.memo<
React.PropsWithChildren<{ initDataSource: InitDataSource }>
>(({ children, initDataSource }) => {
const [store] = React.useState(() => {
const boundStore = createZustandStore<T, Mos>(stateCreator);
if (getInitData) {
boundStore.setState(getInitData(initDataSource));
}
return boundStore;
});
return React.createElement(
StoreContext.Provider,
{ value: store },
children,
);
});
return { Provider, useStore };
}; You can use it to create a store like that: // stores.ts
import { createStore } from "@/store";
type StoreType = {
foo: number;
setFoo: (nextFoo: number) => void;
};
export const myStore = createStore<StoreType>(
(set) => ({
foo: 0,
setFoo: (nextFoo) => set({ foo: nextFoo }),
}),
(initData) => ({ foo: (initData as { foo: number })?.foo }),
);
// Hooks to use in a component
export const useFoo = () => myStore.useStore(({ foo }) => foo);
export const useSetFoo = () => myStore.useStore(({ setFoo }) => setFoo Don't forget to wrap the component using hooks in a store provider! import React from "react";
import { myStore } from "@/stores";
import { InitDataSource } from "@/store";
// You can add more stores here
const stores = [myStore];
export const StateProvider: React.FC<
React.PropsWithChildren<{ initDataSource: InitDataSource }>
> = ({ initDataSource, children }) => (
<>
{stores.reduce(
(acc, { Provider }) => (
<Provider initDataSource={initDataSource}>{acc}</Provider>
),
children,
)}
</>
); Usage in a component: import React from "react";
import { useFoo, useSetFoo } from "@/stores";
export const Component: React.FC = () => {
const foo = useFoo();
const setFoo = useSetFoo();
const increment = React.useCallback(() => setFoo(foo + 1), [foo, setFoo]);
return <div onClick={increment}>{foo}</div>;
}; |
I'm using Next.js and have a separate file
state.js
that has something like this:And then calling
setSession()
on the server (for example in_app.js
) after a successful login. When readingsession
on the server + client, it seems to work fine. So far so good.The problem is: the
session
seems to be saved in Node.js memory and shared with everyone(?). When you login on one browser, the same session data is returned when you load the page on another browser. So I'm assuming when I set session on the server, it persists on Node.js side and returns the same session for all clients.What am I doing wrong, or is zustand not meant for this?
The text was updated successfully, but these errors were encountered: