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

[docs] Finish performance.md #190

Open jaredpalmer opened 4 years ago

jaredpalmer commented 4 years ago

Need to finish this docs page and then add it to docusaurus sidebar. It’s critical for newcomers. Took me several hours to understand this, which tbh I’m not 100% on still.

rough outline

drarmstr commented 4 years ago

@csantos42 - was this the kind of content you were planning for the "bonus" tutorial?

@jaredpalmer - Could you elaborate on "show how to use selectorFamily to select default value, may require keeping an initialState atom?"

ryansolid commented 4 years ago

Awesome, it definitely took me a bit to figure this out when working on the JS Frameworks Benchmark implementation which lead me to realizing these features weren't bundled. I think simple todo list like example is critical. Most of the unique value of this approach comes from atomFamily and selectorFamily since they allow this granular delegation. Otherwise when use single values it isn't really any different than 90% of the other solutions out there in terms of performance.

jaredpalmer commented 4 years ago

Here is my more real-world example. It uses a selectorFamily to fetch the correct todo list based on id prop with suspense. It then normalizes it. Initial states for each item use selectorFamily to read from normalized state. The add todo input uses useRecoilCallback to set a new todo's value and id so that no state needs to be managed at the top level, only down in each todo.

In addition, there is a debug selector that will loop through the todos and reconstruct the denormalized array, useful for debugging or possibly saving.

https://codesandbox.io/s/silly-fog-f448y?file=/src/App.js

Without suspense, you would need to use the loadable api.

Folks will want to know how to use recoil without using it for data fetching. If recoil were like other libraries, that would be fine, you'd fetch data to be then set state in useEffect(). However, you quickly realize this issue:

const setIdsState = useRecoilSetState(idsState(listId))
const setText = useRecoilSetState(/* ?? i don't know the id yet */ )
const setIsComplete = useRecoilSetState(/* ?? i don't know the id yet */ )
useEffect(() => {
    getData(listId).then(() => {
      const { ids, byId } = normalize(data, 'id')
      setIdState(ids)
      // instead of setting by ids like redux, 
      for (let id of ids) {
         setText( // now what?) 
         setIsComplete( // now what?) 
      }
    })
}, [])

The solution at the moment is to move use RecoilRoot.initialState, use suspense, or a loadable. However, that's not super obvious. My guess is that instead folks will make top-level atoms, and get pissed that they can't optimize them. Or, they will get confused by having multiple versions of state where defaults/reset don't work as expected.

Odonno commented 4 years ago

I am also having some issue regarding components updates and getting started with recoil is pretty simple. Start making a React app (with hooks) from scratch and then implement atoms is a nice addition.

Problems

Regarding performance, I am looking deeply into each render with the Profiler, with a comparison to mobx mainly. I found at least 2 issues for now and I do not know the right way to solve this:

  1. List re-rendering - there is some useless render. I tried to make a selector to only select the id of each entity of the list. Then again, I thought it should fix this problem but nothing changed. I believe there is no diff checking between the previous and the new value. That can be a nice addition to prevent re-render

  2. Component (linked to atom) re-rendering - considering issue 1. is fixed - since atoms can be objects, there can be properties used in some components and some in other. So again, you have useless re-render when the object is partially updated if those props are not used in the specified component. Because we watch the entire atom. Of course, the solution would be in our hands which is to create more selector.. that will go in a 1 component = 1 selector. A painful solution IMO

Possible solutions

So, regarding issue 1. and 2., I believe there should be a diff checking to improve global performance.

Regarding issue 1., we could provide a atomKeys function or something similar like @jaredpalmer suggested in the 1st post. Not a top priority but can be helpful for keyed list in React.

Regarding issue 2., I thought of subAtom which is kinda like selector but for component purpose only. This sub atom can extract data from atoms/selectors with also diff checking of the output. In my mind, it should take the props component as parameter to have minimalist design. Subsequent hooks can of course be present in the component body. Example:

const MyComponent = subAtom<Props>(
    props => {
         const a = useRecoilValue(atomA);
         const b = useRecoilValue(atomB(props.username));

        return { value: a.x, owner: b.user };
    },
    subAtomProps => {
        return (
             <div>value: {subAtomProps.value} owner: {subAtomProps}</div>
        );
    }
);

That is a simplistic example but it can be expanded to a more complex one. Also note that I could have destructured props and subAtomProps.

drarmstr commented 4 years ago

Subscriptions are currently done at the granularity of atoms and selectors, when one is updated then all downstream components will re-render. We do plan to allow nodes to customize when they may propagate updates, so they could implement a diff or other equivalence check. Likely this can default to reference-quality for a good optimization.

Odonno commented 4 years ago

So, that would mean something like a global comparer (on the RecoilRoot) and a local comparer (on each atom/selector)? That could be interesting.

Instead of subAtom, I would have renamed it componentSelector. Indeed, it is a selector because it reacts to atoms/selectors in the hierarchy but it also reacts to component props.

Anyway, I leave you on that note.

ivawzh commented 4 years ago

I am that newcomer. So can anyone please give us a spoiler what's benchmark TLDR result before we can see this doc? Ideally comparing to useReducer + useContext and redux.

This is very important to us. We are not even trying Recoil yet because of its unstated performance.

Odonno commented 4 years ago

@ivawzh I wrote an article (https://medium.com/@dbottiau/a-state-management-comparison-with-react-hooks-mobx-and-recoiljs-3b7e2f4cc6c3) about how to view the rendering reconciliations/performance on a React project. There is also a small repository to give a try on a very simple project: https://github.com/Odonno/react-state-management-comparison

There you can see the difference between the big 3: React hooks (similar to Redux), mobx and recoiljs.

drarmstr commented 4 years ago

@Odonno - We've put a bunch of performance optimizations into the 0.0.11 release (hopefully out tomorrow), so you may want to test the performance again with that.