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.49k stars 1.18k forks source link

Update State During SSR #131

Open wilbert-abreu opened 4 years ago

wilbert-abreu commented 4 years ago

Follow up to #5, I understand The only thing to be aware of is that Recoil is bound to a particular React root and hence a particular renderer., but does that mean that we'll never be able to update state during SSR?

Specifically, let's say I fetch some data during SSR(but before the page is rendered) and I'd like to initialize this state on the client via RecoilRoot's initializeState prop.

wilbert-abreu commented 4 years ago

I'm an open source noob, so please let me know if I can ask this question in a better way! 😅

acutmore commented 4 years ago

Hi @wilbert-abreu. I am not 100% sure what you mean by "update state during SSR".

It is unsafe to change an Atom's value during SSR. You would need to fetch the data before you SSR and then include that data again in the response so the client can access it.

I've taken the Redux tutorial on this and modified it for Recoil (haven't tested but should give an idea of how this can be done).

Server:

async function ssr() {
  const data = await getData();
  const html = ReactDOMServer.renderToString(
    <RecoilRoot initializeState={initRecoilState(data)}>
      <App />
    </RecoilRoot>
  );

  return `
    <!doctype html>
    <body>
        <div id="root">${html}</div>
        <script>
            // WARNING: See the following for security issues around embedding JSON in HTML:
            // https://redux.js.org/recipes/server-rendering/#security-considerations
            window.__PRELOADED_STATE__ = ${JSON.stringify(data).replace(
              /</g,
              "\u003c"
            )};
        </script>
        <script src="/static/bundle.js"></script>
    </body>
  `;
}

Client:

function hydrate() {
  const data = window.__PRELOADED_STATE__;

  // Allow the passed state to be garbage-collected
  delete window.__PRELOADED_STATE__;

  hydrate(
    <RecoilRoot initializeState={initRecoilState(data)}>
      <App />
    </RecoilRoot>,
    document.getElementById("root")
  );
}
wilbert-abreu commented 4 years ago

Hey @acutmore, thanks for replying and giving that example. 💯 On the server, after renderToString is run we'd have generated some internal recoil state tree on first pass. How do we then save that initial state generated on the server, and pass it to the client. For the redux docs you mentioned, I'm asking about the equivalent of store.getState() below.

function ssr() {
  const store = createStore(counterApp)
  const html = ReactDOMServer.renderToString(
    <Provider store={store}>
      <App />
    </Provider>
  )

  // Grab the initial state from our Redux store
  const preloadedState = store.getState()

  res.send(renderFullPage(html, preloadedState))
}
acutmore commented 4 years ago

Hi @wilbert-abreu. No problem!

There is an unstable API for 'listening' to Recoil state changes (docs). Unfortunately this won't work with renderToString as it uses useEffect (see here) and this won't be triggered by ReactDOMServer.

On the server, after renderToString is run we'd have generated some internal recoil state tree on first pass

Presumably any recoil state that has been created during the first pass are the Recoil Atom's default values? If so then the same defaults will be picked up by the client. If you could provide any more information about the type of state you are building that would be really useful 😀

morgs32 commented 4 years ago

Hmm @wilbert-abreu well you shouldn't be calling dispatch in a render function so maybe you're doing it from a constructor in a React class? Meanwhile, any actions you dispatch from a constructor wouldn't impact the props on that component anyway. Would actions be processed in the store before nested components render? I actually want to know. What's your use case? Or else, maybe close this.

wilbert-abreu commented 4 years ago

Hey @morgs32 @acutmore, my use case was something similar to this (expanding on the example above)

function ssr() {
  const store = createStore(counterApp)
  const html = ReactDOMServer.renderToString(
    <Provider store={store}>
      <App />
    </Provider>
  )

  const data = fetchData();
  dispatch(LOAD_DATA, { payload: data })
  if(isMobileUser) {
      dispatch(USER_IS_MOBILE)
  }

  // Grab the initial state from our Redux store
  const preloadedState = store.getState()

  res.send(renderFullPage(html, preloadedState))
}

// client
const preloadedState = window.INITIAL_STATE;
const store = createStore(
    createReducer(),
    preloadedState,
  );
hydrate(
<Provider store={store}>
   <App />
</Provider>
, document.getElementById('root'));
morgs32 commented 4 years ago

@wilbert-abreu I think this is the operative line from your earlier post:

On the server, after renderToString is run we'd have generated some internal recoil state tree on first pass.

First off, redux isn't generating internal state from rendering. renderToString of the component tree isn't generating/changing state (or it shouldn't https://github.com/facebookexperimental/Recoil/issues/131#issuecomment-642173003). It creates a state when you createStore because it passes an action to all the reducers (@@INIT I think). So store.getState() should return the same thing before and after renderToString. Maybe you can verify that?

But dispatch will obviously change state. The dispatches you do after renderToString aren't going to impact the html you generated from renderToString, right?

Anyway - since redux isn't generating internal state just from its initial render - I doubt recoil is. But I'm not sure! For example, I wonder if in this example from the docs if the set property in the tempCelcius selector didn't check for the defaultValue on tempFahrenheit, would it change the value of the tempFahrenheit atom synchronously?

const tempFahrenheit = atom({
  key: 'tempFahrenheit',
  default: 32,
});

const tempCelcius = selector({
  key: 'tempCelcius',
  get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9,
  set: ({set}, newValue) =>
    set(
      tempFahrenheit,
      newValue: 'GOTCHA'
    ),
});

function TempCelcius() {
  const [tempC, setTempC] = useRecoilState(tempCelcius);
  const [tempF, setTempF] = useRecoilState(tempFahrenheit);
}

I'd have to create a sandbox to check. In the above I set GOTCHA in the selector. And then I read from the selector first in TempCelsius. So I wonder what tempF will look like...