Recoil
Recoil copied to clipboard
[SSR] Rehydration Story
So while recoil.js is experimental, and the API for snapshots is further experimental, the documentation leaves a lot left unsaid about rehydration.
Let's say I have 3 atoms:
- a user name "kelly"
- A list of 10,000 numbers
- A PouchDB database
I also have a component that is rendered on the server and in the browser. How do I get the values generated while rendering on the server into the client?
The docs are light on Server Side Rendering right now because we don't really use Recoil with that approach internally. However, folks have been using the open-source Recoil with it, and we introduced several fixes for it, though we don't officially support it.
A key with SSR is that you don't have effects or multiple renders, so the initial state needs to be setup with the proper values. You can use the initializeState
prop in <RecoilRoot>
for this.
If you'd like to help to contribute to the documentation with better SSR examples, please feel free to do so!
It would be helpful if, via the atom definition, we could declare a atom as "unserializable" or "ssr=false". That would allow SSR users to only dump safe values.
My hope for state persistence/synchronization for per-atom behavior is to define the initialization policy and apply it per atom instead of globally. Then you can apply this synchronization policy to just the SSR serializable atoms. This is the API we are looking at to do so: #380
This is an interesting approach because it allows for common wrapper functions (aka middleware) that you can apply on a per atom basis. Very exciting!!!
@drarmstr What's the best way to pull out the current set of atoms from a rendered react tree?
@drarmstr What's the best way to pull out the current set of atoms from a rendered react tree?
You can use a Snapshot
, such as from useRecoilSnapshot()
, useRecoilCallback()
, or useRecoilTransactionObserver()
and inspect the current set of atoms with getNodes_UNSTABLE(...)
Okay so Turns out I just misunderstood useRecoilCallback()
and useRecoilTransactionObserver()
don't work in the server, just like useEffect()
.useRecoilCallback()
, but it still seems that snapshot.getPromise(atom)
returns undefined, when it definitely should eventually return a value.
useRecoilSnapshot()
seems like it is missing the most important documentation: How to take a snapshot and turn it into data I can use? Maybe I'm just not smart enough to understand. The whole snapshot interface doesn't make sense to me. I expect a snapshot to be a static representation of the state, instead all I see are multiple mapping functions? Where the mapper function gives a setter?
I thought after trying and failing with that interface I'd use snapshot.getLoadable(atom).contents
, but it's always undefined. There maybe an issue here since my recoil state dumper is above the components that are setting state.
For clarity on the situation, this is my tree:
<RecoilRoot>
<RecoilStateHydrater mutableState={recoilState}>
<MaybeAuthenticated>
<Routing />
</MaybeAuthenticated>
</RecoilStateHydrater>
</RecoilRoot>
So RecoilStateHydrater
is a component that takes a Map
and is supposed to use these snapshot APIs to generate a full state at last render in the server, so that I can dump it to json and rehydrate with json in the client side.
MaybeAuthenticated
checks to see if the atom currentAccount
is present and if it's not fetch the session from the server. When the fetch finishes, it sets the atom
to a string (the user's uuid).
What I've found, by accident, is that anything below MaybeAuthenticated
can see the value of currentAccount
's atom using useRecoilValue()
, but it and above cannot, at least on the server. This maybe because I'm wrapping the atom setter in useMemo
instead of useEffect
due to it being on the server:
import {useLazyQuery} from "@apollo/client";
import {useRecoilState} from "recoil";
import {useEffect} from "react";
import {useMemo} from "react";
import {currentAccount as currentAccountAtom} from "@clumsy_chinchilla/atoms";
import fetchSessionQuery from "./fetchSessionQuery.gql";
export default function MaybeAuthenticated ({children}) {
const [fetchSession, {error, data, loading}] = useLazyQuery(fetchSessionQuery);
const [currentAccount, setCurrentAccount] = useRecoilState<string>(currentAccountAtom);
const useIsomorphicEffect = RUNTIME_ENV === "client" ? useEffect : useMemo;
useIsomorphicEffect(() => {
if (!data || !data.session || !data.session.id || currentAccount) {
return;
}
setCurrentAccount(data.session.id);
}, [loading, currentAccount, data, setCurrentAccount]);
useIsomorphicEffect(() => {
if (error) {
return;
}
if (loading || currentAccount) {
return;
}
fetchSession();
}, [fetchSession, error, loading, currentAccount]);
if (error && error.message !== "unauthenticated") {
throw error;
}
return children;
}
FWIW, I know this isn't a priority and I know that there are changes coming that make this easier, so I don't expect this to be solved over night, this week, or maybe even this year. I just wanted to document a little of my own journey in case someone else tries.
The documentation for the Snapshots is here.
I wouldn't expect any asynchronous queries or changing of atom state to work with server-side rendering. If SSR is only rendering the initial state, then, like an effect, you should not be able to change the state and re-render the new state. But, I'm really not that familiar with SSR...
As an aside, you should not rely on useMemo()
for side-effects as React is free to omit execution of the callbacks.
I realize this issue is over a year old but I still don't see anything super useful in the docs. I can certainly use snapshot_UNSTABLE()
to obtain what appears to be a snapshot. Doing:
const snapshot = snapshot_UNSTABLE();
const stateSnapshot = snapshot.getNodes_UNSTABLE();
console.log('stateSnapshot on server:', stateSnapshot);
appears to yield something that could be useful:
[Map Iterator] { RecoilState { key: 'productId' } }
How does one transform this into something serializable that can be fed back into a <RecoilRoot />
though? RecoilRoot does appear to take a prop; a function that can iterate that iterable and set values for each atom. What if I want to use the values that have already been set for those atoms? Is that possible? What I'm looking to do is dump entire app state to a serializable structure, send that to the client/browser, rehydrate the app's state.