Replies: 11 comments 41 replies
-
Before diving into it and suggesting a best practice, let me note from the library perspective, or even from React perspective in general. (I don't know what people mean by "hydrate", so I'd use "initialize" meaning "initialize from the server data".) We can "initialize" in the React render phase, only if the data is empty. // suppose we have a cache (this is like jotai's atom value map).
const cache = new Map()
const Component = ({ id, valueFromServer }) => {
// we can update the cache if it's empty
if (!cache.has(id)) {
cache.set(id, valueFromServer)
}
return (
<div>value: {cache.get(id)}</div>
)
} So, if you want to make this component to update cache on re-render, we have to use // value is either from server or from client
const Component = ({ id, value }) => {
if (!cache.has(id)) {
cache.set(id, value)
}
useEffect(() => {
cache.set(id, value)
}, [id, value])
return (
<div>value: {cache.get(id)}</div>
)
} Does it make sense so far? |
Beta Was this translation helpful? Give feedback.
-
Thanks for the detailed post, it has a lot of insights about a use case, that we currently don't support. However, In terms of enforcing I hope this explains the design choices of |
Beta Was this translation helpful? Give feedback.
-
I've run into a similar problem as @aulneau in my Remix app, when hydrating atoms with values from loaders. Sample code: // app/routes/index.jsx
import {userAtom} from '~/user'
export const loader = ({request}) => {
return getUser(request)
}
export default function Index() {
const user = useLoaderData()
useHydrateAtoms([[userAtom, user]])
return <Dashboard />
} It's probably a Remix edge case, but when there's a redirection chain, let's say |
Beta Was this translation helpful? Give feedback.
-
My team have run into a similar issue on using export const getServerSideProps = (ctx) => {
const initialValue = ctx.query
/** some code **/
return {
props: {
initialValue,
}
}
}
/** _app.tsx **/
const App = ({ pageProps, Component }) => {
useHydrateAtoms//([
[atomA, pageProps.initialValue]
])
/** rest of code **/
} When Next.js renders the page upon each request, Here comes the issue. Since We solved this issue by using /** some code **/
return (
<Provider initialValues={
[
[atomA, pageProps.initialValue]
]
}>
<Component />
</Provider>
) This resulted in Shouldn't |
Beta Was this translation helpful? Give feedback.
-
I'm not sure if this is right or not. In my app, I have a client-side cookie with a JWT, and this is how I decide if the user is logged in or not. The time to live on that cookie is 15 min and after that, it must be logged out. I realize during router pushes the component does not get re-rendered: https://nextjs.org/docs/api-reference/next/router#resetting-state-after-navigation So, I basically did that: /*
* This hook is used to revalidate the session on every page load.
* The session is revalidated by checking the JWT token's expiry time.
*
* In SSR pages we re-hydrate the atom by calling useHydrateAtoms()
*
* In SSG pages we have to force it as suggested in the docs:
* https://nextjs.org/docs/api-reference/next/router#resetting-state-after-navigation
*/
export const useHydrateSession = () => {
const updateLoggedIn = useUpdateAtom(isLoggedInAtom)
const isLoggedIn = !!parseCookies()[MW_JWT_TOKEN]
useHydrateAtoms([[isLoggedInAtom, isLoggedIn]] as const)
useEffect(() => updateLoggedIn(isLoggedIn), [isLoggedIn, updateLoggedIn])
} |
Beta Was this translation helpful? Give feedback.
-
I can't make it work in my case. Data in // _app.tsx
const initialState = [
[profileAtom, pageProps.profile],
[studentsAtom, pageProps.students],
[lessonAtom, pageProps.lesson],
...
];
return (
<JotaiProvider initialValues={initialState}>
<Component {...pageProps} />
</JotaiProvider>
) // index.tsx
export async function getServerSideProps(ctx) {
...
const { profile } = await getProfile(user, client);
const { categories } = await getCategories(client);
...
return {
props: {
profile,
categories,
},
}; // [lessonId].tsx
export async function getServerSideProps(ctx) {
...
const { profile } = await getProfile(user, client);
const { categories } = await getCategories(client);
const { lesson } = await getLesson(user, client);
...
return {
props: {
profile,
categories,
lesson
},
};
|
Beta Was this translation helpful? Give feedback.
-
I would like to store a value that comes from serverside props and use it all over the application. I've used const useSyncAtom = (state) => {
const [[atom, data]] = state
const set = useUpdateAtom(atom)
useEffect(() => {
set(data)
}, [set, data])
}
const useHydrateAndSyncAtoms = (atom, data) => {
const [initialValues] = useState(() => new Map([[atom, data]]))
useHydrateAtoms(initialValues)
useSyncAtom(initialValues)
}
export default function CompanyDetail({ companyId }) {
useHydrateAndSyncAtoms(companyIdAtom, companyId)
return <CompanyDetailPage />
} |
Beta Was this translation helpful? Give feedback.
-
I run into the problem with nextjs and react query. To make it work I had to wrap the application with import { useEffect, useMemo } from 'react';
import { atom, useSetAtom } from 'jotai';
import type { useHydrateAtoms } from 'jotai/react/utils';
const isSSR = typeof window === 'undefined';
export const useSyncAtoms: typeof useHydrateAtoms = (values) => {
const syncAtomsAtom = useMemo(
() => atom(null, (get, set) => {
for (const [atom, value] of values) {
set(atom, value);
}
}),
[values])
const syncAtoms = useSetAtom(syncAtomsAtom);
isSSR && syncAtoms();
useEffect(() => {
syncAtoms();
}, [syncAtoms, values])
}; |
Beta Was this translation helpful? Give feedback.
-
ProblemuseHydrateAtoms is setting the value only once as expected. However, this could cause issues if you need a clean slate on something like checkout as shown in the example below:
I understand that this is happening due to they hydratedMap that has the atom that was already hydrated here, and it seems like there is no straightforward way around it. So I would like to propose a couple of options to handle scenarios like the above, which are very common. Option 1Update the export function useHydrateAtoms<T extends Iterable<AtomTuple>>(
values: InferAtoms<T>,
options?: Options
) {
const store = useStore(options)
const hydratedSet = getHydratedSet(store)
for (const [atom, value] of values) {
const atomHydrated = !hydratedSet.has(atom)
if (options?.forceHydrate || atomHydrated) {
// if atom exists then update value {} else
hydratedSet.add(atom)
store.set(atom, value)
}
}
} Option 2If an atom is a resettable atom and a reset call is completed, then remove it from the hydratedSet. export const hydratedMap: WeakMap<Store, WeakSet<AnyWritableAtom>> = new WeakMap() Update the export function useResetAtom(
anAtom: WritableAtom<unknown, [typeof RESET], unknown>,
options?: Options
) {
const setAtom = useSetAtom(anAtom, options)
const resetAtom = useCallback(() => {
options?.clearHydrate && hydratedMap.delete(anAtom)
return setAtom(RESET), [setAtom]
})
return resetAtom
} I can implement the solution if we believe either Option is liked 👍 |
Beta Was this translation helpful? Give feedback.
-
FYI - I opened a PR to optionally support re-hydration: #1990 |
Beta Was this translation helpful? Give feedback.
-
related: #2337 |
Beta Was this translation helpful? Give feedback.
-
Greetings!
I've been playing around with the new hook
useHydrateAtoms
thanks @Thisen! really excited that we've started to experiment with how best to support ssr use cases for jotai.I've personally ran into some interesting limitations, and I'm curious what every thinks about how we can work to support this, or at least provide some best practices. I've constructed a very small next.js app that fetches some data from hacker news.
The app is structured with some very specific requirements that I request we do not try to change in order to make this hook work, but instead think about how we can adjust the hook or way the hook is used such that it can support these requirements.
App overview
Click here for the app homepage (with
useHydrateAtoms
)Click here for the app homepage + Provider (with
Provider
)There are a few atoms:
postsAtom
(contains all posts, without comments)currentPostIdAtom
(contains the current query param if exists)currentPostDataAtom
(contains the data with comments for the current post)There are a few pages:
/index.tsx
(home)/[id].tsx
(id)/provider/index.tsx
(same as home, but withProvider
)/provider/[id].tsx
(same as above, but withProvider
)Scenario
The way the app works is that the initial data will fetch for all posts when a user lands on the home page, and hydrate
postsAtom
. When a user clicks on one of the links to a post page, the application will fetch data for that page and "hydrate" the atoms with the correct data:currentPostIdAtom
andcurrentPageDataAtom
.The issue occurs when a user wants to navigate to an adjacent page from the current page. When the user clicks the link, the URL updates, but none of the data updates. The reason this happens is because the
useHydrateAtoms
hook has a limitation in place where it tracks which atoms have been hydrated, and if they have been, to not re-hydrate them.Provider example
You can navigate to the
/provider
path and find the same app, but using theProvider
to set the initial values for the atoms. Something to note:key
is used on theProvider
to force a re-render when the postId changes. This results in the correct functionality when a user navigates from page to page.Best practices and open questions
When working with this, I find myself asking the question: "Should
useHydrateAtoms
make the choice to prevent duplicate hydrations?".I don't know the answer to it, I can see why it makes sense to want to prevent atoms from being hydrated more than once, and how much of a can of worms it could be to try and provide a mechanism to prevent abuse of this.
On the other hand, I think it would also make sense to try and be as generic as possible when it comes to this hook, and let app devs/users handle when and how they hydrate a given atom.
client side updates
Something I've considered is maybe we need to let the app dev just use
useAtom
oruseUpdateAtom
and write a hook that will update the atom on the client, anduseHydrateAtoms
won't ever touch any client side updates (as it is intended for SSR hydration).I've played around with it a bit, and find that it just doesn't feel ideal. Of course we can do this, and suggest this as an option, but I think we can probably find something better:
This feels like it can get out of hand very quickly, especially as complexity grows.
Alternatives and initial ideas
I think there are some options that jump out to me immediately as things we can explore:
Remove the limitation
We could simply remove the limitation from
useHydrateAtoms
and provide guidance on how the values will update if they change/re-render.Provide a callback
Currently the hook is what is doing the hydration, but I think something like this could be helpful:
This gives more control to app devs and gives them an ability to determine when and how things are hydrated.
Provide some new hook
Perhaps there is a hook we haven't thought of yet that can provide a great developer experience. I'd love to hear any ideas around this!
Do nothing
Or maybe this is the best / intended functionality, and we should recommend other methods for handling this kind of scenario.
How do other libraries handle it?
To be honest, there are only a few libraries that really have taken great time and care to solve this for users. React Query is one of those libraries, and I think we can take a page out of their book about prioritizing developer experience. From their docs on hydration:
This method does indeed support hydrating on the client side, in which case the data will be merged with the existing data.
Summary
Thanks for reading this far! I hope that by starting this discussion, we'll be able to work though possible solutions or ideas for how to move forward with something like this!
Beta Was this translation helpful? Give feedback.
All reactions