reactjs / react.dev

The React documentation website
https://react.dev/
Creative Commons Attribution 4.0 International
10.9k stars 7.45k forks source link

React Hooks docs: add an useContext example where state is updated within a functional component #1604

Open desmap opened 5 years ago

desmap commented 5 years ago

The React Hooks docs has in the useContext section an example where state is updated within a class component. How is this done within a functional component?

I did this example https://codesandbox.io/s/3vxr57vm7q but I am wondering if it is possible with less DRY, especially in index.js, lines 10-18:

  const [count, setCount] = useState(0)

  return (
    <Context.Provider
      value={{
        count: count,
        setCount: setCount
      }}
    >

With just one state property and one setState function it's ok but imagine you have five of those pairs, then you must repeat every one three times (eg.: count in useState, as key and as property in <Context.Provider />).

Hence, an example would be great and if I did it the right way.

AlmeroSteyn commented 5 years ago

The above code will rerender all consumers with every render. Rather don't create a new object inside the value argument. See https://reactjs.org/docs/context.html#caveats

As for the hook, one can build a custom hook that performs just like the normal setState. For an example look at something like https://github.com/suchipi/use-legacy-state

desmap commented 5 years ago

Rather don't create a new object inside the value argument

But how should you give initial values to the provider? If you pass them with createContext they'll be ignored by the consumer. So, this is the only way as I understood.

As for the hook, one can build a custom hook that performs just like the normal setState

So, you mean I should package all my useStates into a custom hook and fuel the provider with it?

Sorry, if I misunderstood but it would be awesome if you just could take my Codesandbox and show me (or us) the right way how this is done. Thanks.

I also just learned that there's also an RFC https://github.com/reactjs/rfcs/pull/89 to ease this whole thing.

desmap commented 5 years ago

Rather don't create a new object inside the value argument. See reactjs.org/context/#caveats

Do you mean something like this:

function App() {
  const useCounter = iV => {
    const [count, setCount] = useState(iV)
    return {count: count, setCount: setCount}
  }

  const counter = useCounter(0)

  return (
    <Context.Provider value={counter}>
      <div className="App">
        ...
      </div>
    </Context.Provider>
  )
}

Full version: https://codesandbox.io/s/1rql5v30yj

AlmeroSteyn commented 5 years ago

Here is one solution (there are others as well). I added another state var to show how it could work with a state object.

import React, {
  useReducer,
  useMemo,
  createContext,
  useContext,
  Fragment
} from "react";

const Context = createContext({});

const Counter = () => {
  const context = useContext(Context);
  return (
    <Fragment>
      <div onClick={() => context.setState({ count: context.count + 1 })}>
        Count: {context.count}
      </div>
      <div onClick={() => context.setState({ count2: context.count2 + 1 })}>
        Count2: {context.count2}
      </div>
    </Fragment>
  );
};

const App = () => {

  // useReducer could be used to create a lightweight version of the legacy setState like this:
  const [state, setState] = useReducer(
    (state, newState) => {
      return { ...state, ...newState };
    },
    { count: 0, count2: 0 }
  );

  // Because I am adding the dispatch function of useReducer to the context value I am
  // building a new object to pass. But using useMemo to ensure that the object is only changed
  // if the saved state is changed.
  const contextValue = useMemo(
    () => ({
      ...state,
      setState
    }),
    [state.count, state.count2]
  );

  // The object is then passed to value. It will only receive a new object if state is, in fact, changed.
  return (
    <Context.Provider value={contextValue}>
      <div className="App">
        <h1>Hello CodeSandbox</h1>
        <h2>Start editing to see some magic happen!</h2>
        <Counter />
      </div>
    </Context.Provider>
  );
};

All of this could, of course, be extracted to a custom hook.

Do you mean something like this:

That would suffer from the same issue as there is still a new object created with every render. You could fix that with useMemo as well, but it would not take care of your need to use multiple state values.

As I also recently learnt, Context works exactly the same here as it does in a class component. We can just use hooks to replace the state management here.

desmap commented 5 years ago

@AlmeroSteyn, thx for the quick example! Before I fully grasp your code (between meetings), could following idea be one simple solution to avoid all that boilerplate?

Just putting the context provider high enough in the tree or just after the root assuming that on this level re-renders are rare/or happening when opening a site only. Then, we could just employ the very first example...

AlmeroSteyn commented 5 years ago

Context Consumers update when the Provider value changes, no matter how deep the nesting. In fact using Context to share data between multiple deep nested components is the standard use case. If you are using Context to pass data to direct children it is better to just use props and avoid Context unless there is a very good reason not to.

The issue with the first example is that creating a new object in the provider value means that the object reference changes with every render and that means the diff checking will see it as a new value, even if the actual content stays the same. It uses a shallow compare so only the object references are checked.