With React 16.8, I've implemented my project with useReducer, useContext hooks and created a Global State Management system similar with Redux.
In a view, when I've tried to fetch data in useEffect, it causes Maximum update depth error.
I've already tried all the examples in Facebook React - Hooks-FAQ but can not solve the problem.
My package.json is such:
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-app-polyfill": "^1.0.1",
"react-chartjs-2": "^2.7.6",
"react-dom": "^16.8.6",
"react-router-config": "^5.0.0",
"react-router-dom": "^5.0.0",
"react-test-renderer": "^16.8.6",
"react-uuid": "^1.0.2",
"reactstrap": "^7.1.0",
"simple-line-icons": "^2.4.1",
"styled-components": "^4.2.0"
Here is my code example:
Here is the View.js
import React, { useEffect, useRef } from 'react'
import useView from '/store/hooks/useView'
import isEqual from '/services/isEqual'
import loading from '/service/loading'
const View = () => {
const viewContext = useView()
let viewContextRef = useRef(viewContext)
// Keep latest viewContext in a ref
useEffect(() => {
viewContextRef.current = viewContext
})
useEffect(() => {
// Fetch Data
async function fetchData() {
// This causes the loop
viewContextRef.current.startFetchProcess()
const url = 'http://example.com/fetch/data/'
try {
const config = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
}
}
const response = await fetch(url, config)
if (response.ok) {
const res = await response.json()
finalizeGetViewList(res)
// This causes the loop
viewContextRef.current.stopFetchProcess()
return res
}
} catch (error) {
console.log(error)
return error
}
}
// Prepare data for rows and update state
const finalizeGetViewList = (data) => {
const { Result } = data
if (Result !== null) {
let Arr = []
for (let i = 0; i < Result.length; i++) {
let Obj = {}
//...
//...
Arr.push(Obj)
}
// I compare the prevState with the fetch data to reduce
// the number of update state and re-render,
// so this section do not cause the problem
if (!isEqual(roleContextRef.current.state.rows, Arr)) {
viewContextRef.current.storeViewList(Arr)
}
} else {
console.log(errorMessage)
}
}
function doStartFetch () {
fetchData()
}
const startingFetch = setInterval(doStartFetch, 500)
// aborting request when cleaning
return () => {
clearInterval(startingFetch)
}
}, [])
const {
rows,
isLoading
} = viewContext.state
if (isLoading) {
return (loading())
} else {
return (
<div>
{rows.map(el => (
<tr key={el.id}>
<td>el.name</td>
<td>el.price</td>
<td>el.discount</td>
</tr>
))}
</div>
)
}
}
export default View
If you really willing to solve this issue, please take a look at other files of the Storing cycle.
Here is hook of useView.js:
import { useContext } from 'react'
import { StoreContext } from "../providers/Store"
export default function useUsers() {
const { state, actions, dispatch } = useContext(StoreContext)
const startFetchProcess = () => {
dispatch(actions.viewSystem.startFetchProcess({
isLoading: true
}))
}
const storeViewList = (arr) => {
dispatch(actions.viewSystem.storeViewList({
rows: arr
}))
}
const stopFetchProcess = () => {
dispatch(actions.viewSystem.stopFetchProcess({
isLoading: false
}))
}
return {
state: state.viewSystem,
startFetchProcess,
storeViewList,
stopFetchProcess,
}
}
Here is the viewReducer.js to dispatch:
const types = {
START_LOADING: 'START_LOADING',
STORE_VIEW_LIST: 'STORE_VIEW_LIST',
STOP_LOADING: 'STOP_LOADING',
}
export const initialState = {
isLoading: false,
rows: [
{
ProfilePicture: 'Avatar',
id: 'id', Name: 'Name', Price: 'Price', Discount: 'Discount'
}
],
}
export const actions = {
storeViewList: (data) => ({ type: types.STORE_VIEW_LIST, value: data }),
startFetchProcess: (loading) => ({ type: types.START_LOADING, value: loading }),
stopFetchProcess: (stopLoading) => ({ type: types.STOP_LOADING, value: stopLoading })
}
export const reducer = (state, action) => {
switch (action.type) {
case types.START_LOADING:
const Loading = { ...state, ...action.value }
return Loading
case types.STORE_VIEW_LIST:
const List = { ...state, ...action.value }
return List
case types.STOP_LOADING:
const stopLoading = { ...state, ...action.value }
return stopLoading
default:
return state;
}
}
export const register = (globalState, globalActions) => {
globalState.viewSystem = initialState;
globalActions.viewSystem = actions;
}
This is StoreProvider to provide every component in the app and pass the state:
import React, { useReducer } from "react"
import { reducer, initialState, actions } from '../reducers'
export const StoreContext = React.createContext()
export const StoreProvider = props => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<StoreContext.Provider value={{ state, actions, dispatch }}>
{props.children}
</StoreContext.Provider>
)
}
This is the reducers index.js to clone many reducers for different views:
import { user as userData, reducer as loginReducer } from './loginReducer'
import { register as viewRegister, reducer as viewReducer } from './viewReducer'
import { register as groupRegister, reducer as groupsReducer } from './groupsReducer'
export const initialState = {};
export const actions = {};
userData(initialState, actions)
viewRegister(initialState, actions)
groupRegister(initialState, actions)
export const reducer = (state, action) => {
return {
credentials: loginReducer(state.credentials, action),
roleSystem: viewReducer(state.viewSystem, action),
groups: groupsReducer(state.groups, action)
}
}
Sorry about many files, but there is no other way to explain the situation. People who used to work with Redux can understand this approach. There is no problem with state => action => dispatch system until I try to fetch data with the initial render of the page (In this example I called it View).
The classical let didCancel = false approach did not work. The problem has been solved if I compare the state with the new fetched data. But when I added the loading, it triggers the useReducer and it re-renders the page and this causes an infinite loop.
UseRef and clearInterval do not prevent it and this error occurs:
Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.