I am exploring react context with hooks and noticed why my Custom route re-renders everytime the context state changes. In my example, I created a context provider with a useReducer hook that toggles a loading state.
And, I've learned this along the way,
A React context Provider will cause its consumers to re-render whenever the value provided changes.
However, when context is consumed in regular parent component, it doesn't re-render.
Here is the codesandbox for this problem.
index.js
import React from "react";
import ReactDOM from "react-dom";
import Provider from './Provider';
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>,
rootElement
);
Provider.js
import React from 'react';
export const AppContext = React.createContext({});
export const DispatchContext = React.createContext({});
const initState = { isLoading: false }
const reducer = (initState, action) => {
switch(action.type) {
case 'LOADING_ON':
return {
isLoading: true
}
case 'LOADING_OFF':
return {
isLoading: false
}
default:
throw new Error('Unknown action type');
}
}
const Provider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, { isLoading: false });
React.useEffect(() => {
console.log('PROVIDER MOUNTED');
}, []);
return (
<DispatchContext.Provider value={dispatch}>
<AppContext.Provider value={{ app: state }}>
{children}
</AppContext.Provider>
</DispatchContext.Provider>
)
}
export default Provider;
App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './Home';
import CustomRoute from './CustomRoute';
import { AppContext } from './Provider';
// const App = () => {
// React.useEffect(() => {
// console.log('App Mounted');
// }, []);
// const { app } = React.useContext(AppContext);
// return (
// <Home app={app}/>
// )
// }
/*
Why does context used in a custom route triggers rerender
while when used on a regular component doesn't? 👆🏻
*/
const App = () => (
<BrowserRouter>
<Switch>
<CustomRoute path="/" exact component={Home} />
{/* <Route path="/" exact component={Home} /> */}
</Switch>
</BrowserRouter>
);
export default App;
Child.js for the sake of testing if child rerenders too.
import React from "react";
const Child = () => {
React.useEffect(() => {
console.log('Child MOUNTED');
}, []);
const [text, setText] = React.useState('');
const onTextChange = (e) => {
setText(e.target.value);
}
return (
<div className="App">
<h3>Child</h3>
<input onChange={onTextChange} value={text} type="text" placeholder="Enter Text"/>
</div>
);
}
export default Child;
CustomRoute.js
import React from 'react';
import { Route } from 'react-router-dom';
import { AppContext } from './Provider';
const CustomRoute = ({ component: Component, ...rest }) => {
const { app } = React.useContext(AppContext);
return (
<Route
{...rest}
component={(props) => {
// return <Component {...props} />
return <Component {...props} app={app} /> // pass down the cntext
}}
/>
)
}
export default CustomRoute;
I'm testing out to change the value of isLoading state of context. If you try to input something in input field and trigger the change loading state, the whole component gets re-rendered.
Home.js
import React from "react";
import "./styles.css";
import Child from './Child';
import { AppContext, DispatchContext} from './Provider';
const Home = ({ app }) => {
React.useEffect(() => {
console.log('HOME MOUNTED');
}, []);
const [text, setText] = React.useState('');
const dispatch = React.useContext(DispatchContext);
// const { app } = React.useContext(AppContext);
const onTextChange = (e) => {
setText(e.target.value);
}
return (
<div className="App">
<h3>Loading State: {app.isLoading.toString()}</h3>
<input onChange={onTextChange} value={text} type="text" placeholder="Enter Text"/>
<br/>
<button onClick={() => dispatch({ type: 'LOADING_ON' })}>On</button>
<button onClick={() => dispatch({ type: 'LOADING_OFF' })}>OFF</button>
<br/>
<Child />
</div>
);
}
export default Home;
After a long wait searching for an answer, I finally found the answer.
In my custom route, I was using a component
prop instead of render
prop which resulted in UNMOUNTING
the component on the specified Route
and deleting the states of the children.
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the componentprop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component.
in my old custom route
// I used component prop before
<Route
{...rest}
component={(props) => {
return <Component {...props} app={app} />
}}
/>
To solve this problem, I used render
prop instead of component
prop, like so:
<Route
{...rest}
render={(props) => {
return <Component {...props} app={app} />
}}
/>