Recoil
Recoil copied to clipboard
[SSR] Async selectors suspend forever if used on server
I'm trying to use async selectors (those having get function return Promise) for data fetching during SSR (server side rendering), but it does not work - it sometimes hangs up and sometimes returns stalled value.
First quick question - are async selectors supposed to work in such situation?
Here is simplified code that I'm using:
import { Suspense } from "react";
import {
atom,
selector,
useRecoilState,
useRecoilValue,
RecoilRoot
} from 'recoil';
import { Main } from "./Main";
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
const filter = atom({key: "filter", default: 0});
const query = selector({
key: 'query',
get: ({get}) => {
const f = get(filter);
return sleep(1000).then(() => {return f});
},
});
const Query: React.FC = () => {
const q = useRecoilValue(query);
return <div>{q}</div>;
}
export const TestBody: React.FC = () => {
const [f, setF] = useRecoilState(filter);
return (
<RecoilRoot>
<div className="container-fluid">
<div className="row">
<Main>
<p>
Filter: {f}
</p>
<button onClick={() => setF(f+1)}>Inc</button>
<hr/>
<Suspense fallback={<div>Wait</div>}>
<Query/>
</Suspense>
</Main>
</div>
</div>
</RecoilRoot>
);
};
query
selector just delays value of filter
atom, which is initially set to 0 and later changed by user interaction. During first request the rendering never ends. I found that call to useRecoilValue
in Query
component throws Promise
as expected, but the promise never settles, although promise in selector definition settles after 1s. In browser, the promise settles correctly, so I suspect this is a bug in Recoil (in SSR environement). Or am I doing something wrong?
During any subsequent request query
returns value (0) without waiting. I assume that this is cached value that first request should return. This behavior difference in first and other request is strange. As far as I understood how Recoil works, it should store atoms and selectors to recoil root and the root should be fresh for each request. How can one request affect another? I would expect that TestBody
component would be evaluated in each request with same result.
Additional information:
- I'm serving server response via
renderToPipeableStream
which should support Suspense - React version: 18.2.0
- Recoil version: 0.7.5
While it sounds like Suspense may work with SSR now, as far as I'm aware hooks like useEffect()
do not. Would be interesting if someone wanted to research alternative solutions.
Could you please elaborate how are hooks such as useEffect related to reading data from selector? I suppose that useEffect is used inside useRecoilState to subscribe for future changes of a value (for example when one of its dependencies changes). But during initial render (in SSR) this should not be relevant, because no atoms are set, they have default value. As I understand how Suspense (and Recoil) work, useRecoilState should initially throw a Promise that settles when selector value is available and then the component is re-rendered. Currently useRecoilState throws a Promise (as expected) but it never settles. Hope this is not related to hooks but some internal value processing in Recoil.
Yes, in general useEffect()
can be used for subscribing to changes such as when the Promise resolves. Though, with React 18 the useSyncExternalStore()
hook is used instead. If the selector is in an async state for the initial render then Suspense would be triggered. Honestly, I'm not sure on the specifics of SSR support for Suspense.
Hello.
I have the same issue on atom effect. In my opinion, There seems to be a problem with async processing(suspense) of the recoil value Here is the my code.
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';
const Counter = atom<number>({
key: 'Counter',
effects: [
({ setSelf, trigger, getInfo_UNSTABLE, node }) => {
if (trigger == 'get') {
const info = getInfo_UNSTABLE(node);
if (info.isSet == false) {
setSelf(new Promise<number>((resolve, _) => {
resolve(10); // stuck here
}));
}
}
}
]
});
export const TestComponent = () => {
const counter = useRecoilValue(Counter);
const setCounter = useSetRecoilState(Counter);
return <div>
<p>
{counter}
</p>
<button onClick={() => setCounter(counter + 1)}>
increment
</button>
</div>;
}
@st33v3 were you able to find a solution for the same? I am stuck with same issue.
Hi @drarmstr, I went ahead and debugged the issue with SSR and have provided a PR for the same. Can you please verify and let me know if it works as expected?
PR: https://github.com/facebookexperimental/Recoil/pull/2073