Paul
Paul

Reputation: 412

Including useContext hook causes child's useState to reset to initial value

I've been struggling with this problem for a few days, so any help would be greatly appreciated. We have a global data context which we're including in a few components in the hierarchy. I've replicated the issue we're seeing in the basic example below.

You can also run it on CodeSandbox

The problem is that childValue in the Content component is reset to its initial useState value every time the component re-renders. But this is only the case when the useData context is included up the chain in the Routes component. Removing the useData line (and hardcoding isAuthenticated) solves the problem. This is not an acceptable solution, however, because we need to be able to keep certain values in a global context and include them wherever.

I have tried wrapping stuff in React.memo(...) to no avail. What am I missing here?

import React, { useState, useContext, useEffect } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { render } from "react-dom";

// Routes

const Routes = () => {
  // We see desired behavior if useData() is removed here.
  //  i.e. Initial Value does NOT get reset in Content
  const { isAuthenticated } = useData();
  // const isAuthenticated = true // uncomment this after removing the above line

  const RouteComponent = isAuthenticated ? PrivateRoute : Route;

  return (
    <Switch>
      <RouteComponent path="/" render={props => <Content {...props} />} />
    </Switch>
  );
};

const PrivateRoute = ({ render: Render, path, ...rest }) => (
  <Route
    path={path}
    render={props => <Render isPrivate={true} {...props} />}
    {...rest}
  />
);

// Data Context

export const DataContext = React.createContext();
export const useData = () => useContext(DataContext);
export const DataProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [contextValue, setContextValue] = useState(false);

  useEffect(() => {
    setIsAuthenticated(true);
  }, []);

  const doSomethingInContext = () => {
    setTimeout(() => setContextValue(!contextValue), 1000);
  };

  return (
    <DataContext.Provider
      value={{
        isAuthenticated,
        doSomethingInContext,
        contextValue
      }}
    >
      {children}
    </DataContext.Provider>
  );
};

// Page Content

const Content = props => {
  const { contextValue, doSomethingInContext } = useData();

  const [childValue, setChildValue] = useState("Initial Value");

  useEffect(() => {
    if (childValue === "Value set on Click") {
      doSomethingInContext();
      setChildValue("Value set in useEffect");
    }
  }, [childValue]);

  return (
    <div>
      <div style={{ fontFamily: "monospace" }}>contextValue:</div>
      <div>{contextValue.toString()}</div>
      <br />
      <div style={{ fontFamily: "monospace" }}>childValue:</div>
      <div>{childValue}</div>
      <br />
      <button onClick={() => setChildValue("Value set on Click")}>
        Set Child Value
      </button>
    </div>
  );
};

const App = () => {
  return (
    <DataProvider>
      <Router>
        <Routes />
      </Router>
    </DataProvider>
  );
};

render(<App />, document.getElementById("root"));

Upvotes: 2

Views: 2656

Answers (1)

mimbrown
mimbrown

Reputation: 196

I think the problem is this: when you call doSomethingInContext it triggers setContextValue (after the timeout). When that runs, it updates the Provider's data, which causes Routes to rebuild (since it is a consumer). Rebuilding Routes changes the render function, causing everything underneath to get thrown away and rebuilt. Try useCallback: in Routes, add this:

// In the body...
const render = useCallback(
  props => <Content {...props} />,
  []
);

// In the RouteComponent
<RouteComponent path="/" render={render} />

That way, the function doesn't change, and the children should be preserved in the rebuild.

Upvotes: 2

Related Questions