Recoil icon indicating copy to clipboard operation
Recoil copied to clipboard

[SSR] Async selectors suspend forever if used on server

Open st33v3 opened this issue 2 years ago • 4 comments

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

st33v3 avatar Aug 17 '22 16:08 st33v3

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.

drarmstr avatar Aug 17 '22 18:08 drarmstr

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.

st33v3 avatar Aug 18 '22 11:08 st33v3

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.

drarmstr avatar Aug 23 '22 22:08 drarmstr

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>;
}

noricube avatar Aug 25 '22 02:08 noricube

@st33v3 were you able to find a solution for the same? I am stuck with same issue.

tirthbodawala avatar Oct 20 '22 12:10 tirthbodawala

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

tirthbodawala avatar Oct 20 '22 14:10 tirthbodawala