Reputation: 412
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
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