How to prevent infinite loop with React's useReducer, useContext and useEffect - reactjs

I'm currently trying to figure out how to avoid creating an infinite loop when wrapping my application in a Context provider (taking in values from useReducer) and then updating via child component with a useEffect hook.
There's an example of the problem here on CodeSandbox.
Obviously it's difficult to talk about the problem without reposting all the code here, but key points:
Root:
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return (
<Context.Provider value={value}>
...
</Context.Provider>
Child:
export const Page1: FC = () => {
const { dispatch, state } = useContext(Context);
const { isLoading } = state;
useEffect(() => {
dispatch({
type: "loading",
payload: false
});
}, [dispatch]);
return (...)
I'm probably missing something obvious, but any pointers might help others who run into the same problem.
Full example on CodeSandbox.

The root of the problem is here
<Route path="/page1" component={() => <Page1 />} />
When you pass inlined arrow function as component you basically creating new component for every render and forcing Route to completely re-mount this part. When it happens useEffect gets called again and so on, and so on.
You need to change it like that:
<Route path="/page1"><Page1 /></Route>
// or
<Route path="/page1" component={Page1} />
Citation from react-router docs:
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 component prop, 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. When using an inline function for inline rendering, use the render or the children prop (below).
Source: https://reactrouter.com/web/api/Route/route-render-methods

Related

child component without props re-renders even with react mem

there are a lot of questions related to my issue, but all deal with child components with props. I am not sending any props but the child component is still getting re-rendered which is causing useless load on the db when the getstate function runs.
When i change the sidebar state in the parent, the child re-renders. What is causing the react memo to not work? and how to memoize the chart data to avoid hitting the db everytime the sidebar changes?
function Child() {
const [state, setstate] = useState("")
useEffect(() => {
getState('addressbalance');
},[]);
const getState = async (urlLoc) => {
try {
const response = await fetch(baseURL.concat(`/${urlLoc}`));
const jsonData = await response.json();
setstate(jsonData);
} catch (err) {
console.error(err.message);
}
};
const renderChart = () => {
return <ChartApex graph='Bar' data = {state} height={'95%'} width={'100%'}/>
}
return (
<Explore>
<Card width="90%" height='550px'>
{renderChart()}
</Card>
</Explore>
);
}
export default React.memo(Child)
Parent
<PageContainer changeSidebar={changeSidebar} sidebar={sidebar}>
<Switch>
...
<Route
path="/addressbalance"
component={() => <Child/>}
/>
</Switch>
</PageContainer>
You are using setState in the memoized component, the documentation is explicit on this point :
https://en.reactjs.org/docs/react-api.html#reactmemo
React.memo only checks for prop changes.
If your function component wrapped in React.memo has a useState, useReducer or useContext Hook in its implementation, it will still rerender when state or context change.

Provider component's useEffect is running before mounted

I have a Provider component which passes down some data through its props, which will later be used across the app.
It looks something like this:
export const MyContext = createContext({});
MyContext.displayName = 'My Context';
export const MyProvider = ({ children }) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const setData = async () => {
setLoading(true);
setItems(await getItemsApiCall());
setLoading(false);
};
useEffect(() => {
console.warn('component re-rendering')
if (!items.length) {
setData();
}
}, [items]);
return (
<MyContext.Provider value={{items, loading}}>
{children}
</MyContext.Provider>
);
};
In theory this should work fine, however I am seeing a bunch of extra re-renders through the console.warn which seem unnecessary, also I am getting the warning relating to the state update on an unmounted component
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application...
Although I have seen the solution for this in other threads, none of them work for me because I am not only setting items, but also loading, which I need to put in spinners and other such things on other parts of the app while fetching data.
Anyway, is there a way to prevent these re-render before the mount and to surpress the warning under these circumstances?
Edit: For the record, I am currently getting 2 re-renders before the warning relating to updating the state of an unmounted component, and then two other re-renders (those of which are to be expected). So basically, my console look something like this:
component re-rendering
component re-rendering
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application...
component re-rendering
component re-rendering
Edit2: For what it is worth, I would also like to show how the MyProvider is being used. It is simply wrapping another component in a particular route. Something like this:
const App = () => (
<Router>
<Switch>
<Route
path='/items'
exact
component={() => (
<MyProvider>
<Items/>
</MyProvider>
)}
/>
</Switch>
</Router>
)
The behaviour I get above happens simply when visiting the /items route.

How do I stop nested React components that dispatch Redux actions which update state from getting stuck in an infinite loop?

I am having an issue with my nested react components getting stuck in an infinite loop.
The outer component is a DashboardLayout. To separate redux logic from 'pure' layout logic I have divided the compnent as follows
DashboardLayout/
index.js
DashboardLayout.js
The dashboard layout is mapped to the route /user
The index.js is (roughly) as follows
const Dashboard = () => {
const { submissions } = useSubmissionsPreloader()
const dispatch = useDispatch()
const { pathname } = useLocation()
useEffect(() => {
if (pathname === '/dashboard') dispatch(replace('/dashboard/tasks'))
}, [dispatch, pathname])
return pathname === '/user' ? null : (
<DashboardLayout submissions={submissions} selected={pathname} />
)
}
DashboardLayout.js is roughly as follows
const DashboardLayout = ({
submissions,
selected
}) => (
<Container>
<SegmentedController
tabs={[
{ path: '/dashboard/submissions', title: 'My Submissions' },
{ path: '/dashboard/tasks', title: 'My Tasks' }
]}
selected={selected}
/>
<Switch>
{dashboardRoutes.map(({ path, loader, exact }) => (
<Route key={path} path={path} component={loadable({ loader })} exact={Boolean(exact)} />
))}
</Switch>
<h4>Submissions ({submissions.length})</h4>
<Table
headers={submissionsHeaders}
rows={submissions.map(submissionsToRows)}
/>
</Container>
)
This all works fine if the sub-component being mounted doesn't affect the redux state. However if we take one of the sub-components as an example
Tasks/
index.js
Tasks.js
index.js is as follows
const Tasks = () => {
const { tasks } = useTasks()
return <PureTasks tasks={tasks} />
}
and Tasks.js is simply this (doesn't actually care about the tasks yet)
const Tasks = () => (
<>
<p>Tasks assigned to me go here</p>
</>
)
The problem is that the useTasks is using a useEffect hook to dispatch a loadTasks action, a saga picks it up, makes an API call, and then dispatches loadTasksSuccess with the loaded tasks. The reducer for that updates the tasks state with the tasks pulled from the api
useTasks
export const useTasks = () => {
const tasks = useSelector(getTasks)
const dispatch = useDispatch()
const doTasksLoad = useCallback(() => dispatch(tasksLoad()), [dispatch])
useEffect(() => {
doTasksLoad()
}, [doTasksLoad])
return { tasks }
and the relevant bit of the saga
function* worker({ type }) {
switch (type) {
case 'TASKS_LOAD':
try {
const tasks = yield call(loadTasks) // api call returns tasks
yield put(tasksLoadSuccess(tasks))
} catch (err) {
yield put(tasksLoadFail(err))
}
/* istanbul ignore next */ break
default:
break
}
}
Nothing controversial there.
The issue is that the change to the state causes the layout to re-render which causes the nested component to re-render which triggers the tasksLoad action again which triggers the tasksLoadSuccess action, which changes the state (tasksLoad sets isLoading to true and tasksLoadSuccess sets it to false again) and this causes an infinite loop.
I've got a gut feeling I ought to be using something like useMemo or useRef to somehow stop the constant re-rendering, but so far I'm not quite getting that to work either.
This general mechanism is fairly core to the way I was planning on building the app so I'd like to get it right. If the nested component only reads from the state and doesn't change it then no re-rendering happens so a brute force approach would be to get the dashboard to simply preload everything it thinks it might need. But that seems ugly to me.
Has anyone got any suggestions as to a better way to approach this?
I have very less experience with Redux but i think const doTaskLoad is being assigned a function. If that is so then...
I think the problem over here is that you are using a function as a element in the dependency array of useEffect , as per the rules of React Render , every time a render happens every function is a new reference , hence React considers it as a new element and keeps on re rendering the value.
May i suggest using a primtive value for the dependency array
export const useTasks = () => {
const tasks = useSelector(getTasks)
const dispatch = useDispatch()
const doTasksLoad = useCallback(() => dispatch(tasksLoad()), [dispatch])
useEffect(() => {
doTasksLoad()
}, [doTasksLoad])
return { tasks }
I worked out what was wrong, with thanks to #cory-harper whose comment above pointed me in the right direction.
The Tasks component was indeed being reloaded. This is due to how my dashboardRoutes were being described.
I render the individual routes as
{dashboardRoutes.map(({ path, loader, exact }) => (
<Route key={path} path={path} component={loadable({ loader })} exact={Boolean(exact)} />
))}
which is all well and good, and standard practice, when rendering entire scenes. The loadable component wrapper ensures that the component is lazily loaded, and the route itself was being defined via a utility function makeRoute which is as follows
const makeRoute = ([path, scene, permissionRequired = null, exact = true]) => ({
path,
loader: () => import(`scenes/${scene}`),
permissionRequired,
exact
})
So the loader asynchronously imports the correct component which is fine for an entire scene, but for the nested component it made react think the component was new each time, thus the useEffect was being called each time, thus the infinite loop.
By removing the fancy-pants lazy/asynchronous loading (not needed as these sub-components are tiny) and replacing that loop with
<Switch>
<Route path="/dashboard/submissions" component={Submissions} exact />
<Route path="/dashboard/tasks" component={Tasks} exact />
</Switch>
the components no longer look unique each load, and everything works as expected.

pass values to parent element react hooks

Hope you can clarify this issue:
I have parent component:
function App() {
const [ thanks, setThanks ] = useState(null);
const thankerHandler=(value)=>{
setThanks(value)
}
**//in the return**
<Route component={PaymentPage} thankerHandler={thankerHandler} path="/payment" />```
and the child component :
const PaymentPage=({thankerHandler})=> {
const [thanks, setThanks] = useState(false);
**//after some logic**
useEffect(() => {
thankerHandler(thanks);
}, [thanks])
the problem is that react is telling me the following:
61 | useEffect(() => {
62 |
> 63 | thankerHandler(thanks);
64 |
65 | }, [thanks])
thankerHandler is not a function
I have no idea what I am doing wrong here, the idea is that when the function will be called in the child component, it will update the value in the parent, but it is not doing the thing, when I console log thankerHandler, just gives me the value of undefined because of the null state in the parent, if I set it there to false, gives me false and so on, but it does not recongnices it as a function.
anyone knows what I am doing wrong here?
thanks in advance
Since PaymentPage component is rendered via Route component, props passed to Route component are not passed to the PaymentPage component.
Instead of using component prop to render the PaymentPage component, use render prop which takes a function. It will allow you to pass the props to PaymentPage component
Change
<Route component={PaymentPage} thankerHandler={thankerHandler} path="/payment" />
to
<Route
path="/payment"
render={(routerProps) => <PaymentPage {...routerProps} thankerHandler={thankerHandler} />}
/>
Apart from this problem, there is another problem in PaymentPage component.
Since useEffect is calling the thankerHandler function, thankerHandler should be added to the dependency array of the useEffect hook
useEffect(() => {
thankerHandler(thanks);
}, [thanks, thankerHandler]);
but adding thankerHandler to the dependency array of useEffect hook will cause another problem, i.e. when App component re-renders, thankerHandler function will be re-created and the reference to this new function will be passed to PaymentPage component which will cause the useEffect hook in PaymentPage component to run again. This may or may not be something you want. If you don't want this, make sure to wrap thankerHanlder function in useCallback hook to prevent useEffect hook from running unnecessarily.
try to use render method in Route component:
<Route render={() => <PaymentPage thankerHandler={thankerHandler} />} path="/payment" />

Updating Parents state from Child without triggering a rerender of Child in React

So I'm trying to build a single page app in react.
What I want:
On the page you can visit different pages like normal. On one page (index) i want a button the user can click that expands another component into view with a form. This component or form should be visible on all pages once expanded.
The Problem:
The index page loads some data from an api, so when the index component gets mounted, an fetch call is made. But when the user clicks the "Expand form"-Button, the state of the Parent component gets updated as expected, but the children get rerendered which causes the index component to fetch data again, which is not what I want.
What I tried
// Parent Component
const App => props => {
const [composer, setComposer] = useState({
// ...
expanded: false,
});
const expandComposer = event => {
event.preventDefault();
setComposer({
...composer,
expanded: true
});
return(
// ...
<Switch>
// ...
<Route
exact path={'/'}
component={() => (<Index onButtonClick={expandComposer}/>)}
// ....
{composer.expanded && (
<Composer/>
)};
);
};
// Index Component
const Index=> props => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(()=> {
// load some data
}, []);
if(isLoading) {
// show spinner
} else {
return (
// ...
<button onClick={props.onButtonClick}>Expand Composer</button>
// ...
);
};
};
So with my approach, when the button is clicked, the Index component fetched the data again and the spinner is visible for a short time. But I dont want to remount Index, or at least reload the data if possible
Two problems here. First, React will by default re render all child components when the parent gets updated. To avoid this behavior you should explicitly define when a component should update. In class based components PureComponent or shouldComponentUpdate are the way to go, and in functional components React.memo is the equivalent to PureComponent. A PureComponent will only update when one of it's props change. So you could implement it like this:
const Index = () =>{/**/}
export default React.memo(Index)
But this won't solve your problem because of the second issue. PureComponent and React.memo perform a shallow comparison in props, and you are passing an inline function as a prop which will return false in every shallow comparison cause a new instance of the function is created every render.
<Child onClick={() => this.onClick('some param')} />
This will actually create a new function every render, causing the comparison to always return false. A workaround this is to pass the parameters as a second prop, like this
<Child onClick={this.onClick} param='some param' />
And inside Child
<button onClick={() => props.onClick(props.param)} />
Now you're not creating any functions on render, just passing a reference of this.onClick to your child.
I'm not fully familiar with your style of React, I do not use them special state functions.
Why not add a boolean in the parent state, called "fetched".
if (!fetched) fetch(params, ()=>setState({ fetched: true ));
Hope this helps
Silly me, I used component={() => ...} instead of render={() => ...} when defining the route. As explained in react router docs, using component always rerenders the component. Dupocas' answer now works perfectly :)

Resources