Search code examples
reactjstypescriptrerenderrecoiljs

Recoil JS - getting rid of the suspense re-rendering my app


I am experimenting with Recoil in a React framework I am currently building. In my use case, the application will produce an action object based on user activities, and then it will send it to the server, from where it will get the application state.

The action is stored in a recoil atom. I am using a recoil selectorFamily that accepts an action and gets the state from the server. Here are trivial examples of what I am actually doing (code is in typescript):

export const MyActionAtom = atom<MyAction|undefined>({
    key: "MyActionAtom",
    default: undefined
});

const MyStateSelectorFamily = selectorFamily({
    key: 'MyStateSelectorFamily',
    get: (action: MyAction|undefined) => async ({get}) => {        
        if (action) {
           return await getStateFromServer(action);
        }
        return SomeFallbackState;
    }
});

const MyStateSelector = selector({
    key: 'MyStateSelector ',
    get: ({get}) => {
        return get(MyStateSelectorFamily(get(MyActionAtom)));
    }
});


function App() {
    const appState = useRecoilValue(MyStateSelector);
    return (
        <RecoilRoot>
            <React.Suspense fallback={<div>Loading...</div>}>
                <MyApp state={appState} />
            </React.Suspense>
        </RecoilRoot>
    );
}

Here, MyApp is a react component that will render the application's entire view tree based on the appState.

Now, from within the components that are either direct or indirect children of MyApp one may trigger another action:

const setMyAction = useSetRecoilState(MyActionAtom);
setMyAction(...);

The result of setMyAction is slightly deviating in values as opposed to the initial state I have initialized my application with. Therefore, I would like to re-render only the components which are dependent on the changed state.

However, my entire view tree gets updated. The reason I am suspecting is that React.Suspense is replacing the dom with the fallback until the action's response is returned (notice that MyStateSelectorFamily is asynchronous, because the state is returned as a promise).

In a more redux-like world I'd fire my request and dispatch the state change after the promise is resolved (and I intend to handle loading state differently than what React.Suspense is doing for me).

Is there a way to prevent my app from re-rendering? I have seen examples of using recoil Loadables in order to avoid React.Suspense, but I am struggling to get the old app state while the loadable is in loading state.


Update

As I experimented further, I was able to make it work mimicking the reducer approach with a dispatch method:

  1. I got rid of the action atom, and changed MyStateSelectorFamily to be an atom itself:

    const MyStateAtom = atom({
        key: 'MyStateAtom ',
        default: SomeFallbackState
    });
    
  2. I replaced the setMyAction(...); part in components that change state with a custom hook:

    // in some dedicated ts file:
    export const useAction = () => {
        const dispatch = useSetRecoilState(MyStateAtom);
        return useCallback(async (action: MyAction) => {
            await getStateFromServer(action).then(dispatch);
        }, [dispatch]);
    };
    
  3. And I use the hook instead in a component:

     export const SomeComponent = (props: SomeProps) => {
         const dispatchAction = useAction();
         // other logic
         const action: MyAction = ... // create the appropriate action
         dispatchAction(action);
    
         return (...);
     };
    

Well, this seems to work as I want it to, and is closer to my former codebase where I used redux, so I quite like this better than the selectorFamily approach from before. However, I am frequently seeing a react warning printed in the console:

 Warning: Cannot update a component (`Batcher`) while rendering a 
 different component (`SomeComponent`) ....

My app is a SPA app, and the warning is printed on the initial load. Then it works with no observable issues. I'd like to know if I can fix the cause of the warning -- this Batcher seems to be a recoil thing and I guess I am not doing things right to break it like that.


Solution

  • The reason the warning pops up is discussed here. It's basically a logic flow mistake inside Recoil and will be fixed in the next release. Currently there is nothing you can do about it, but it only pops up in dev mode, while in production mode the warning is ignored.