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