0
votes

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.
1

1 Answers

0
votes

I would try to split up your concerns to dispatch the startFetchProcess action on the initial render, then fetch when the loading state updates:

useEffect(() => {
  viewContextRef.current.startFetchProcess()
}, [])



 useEffect(() => {
 // Fetch Data
  async function fetchData () {
    // This causes the loop
    // moved to the dependency array
    const url = 'http://example.com/fetch/data/'

  // ..... //

  function doStartFetch () {
    roleContext.state.isLoading && fetchData()
  }

  const startingFetch = setInterval(doStartFetch, 500)
  // aborting request when cleaning
  return () => {
    clearInterval(startingFetch)
  }
}, [roleContext.state.isLoading])