facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.59k stars 1.18k forks source link

[SSR] Rehydration Story #547

Open krainboltgreene opened 4 years ago

krainboltgreene commented 4 years ago

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:

  1. a user name "kelly"
  2. A list of 10,000 numbers
  3. 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?

drarmstr commented 4 years ago

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!

krainboltgreene commented 4 years ago

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.

drarmstr commented 4 years ago

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

krainboltgreene commented 4 years ago

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!!!

krainboltgreene commented 4 years ago

@drarmstr What's the best way to pull out the current set of atoms from a rendered react tree?

drarmstr commented 4 years ago

@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(...)

krainboltgreene commented 4 years ago

Okay so useRecoilCallback() and useRecoilTransactionObserver() don't work in the server, just like useEffect(). Turns out I just misunderstood 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.

krainboltgreene commented 4 years ago

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;
}
krainboltgreene commented 4 years ago

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.

drarmstr commented 4 years ago

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.

nobleach commented 2 years ago

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.