Search code examples
reactjsuse-effectreact-context

Context API values are being reset too late in the useEffect of the hook


I have a FilterContext provider and a hook useFilter in filtersContext.js:

import React, { useState, useEffect, useCallback } from 'react'

const FiltersContext = React.createContext({})

function FiltersProvider({ children }) {
  const [filters, setFilters] = useState({})

  return (
    <FiltersContext.Provider
      value={{
        filters,
        setFilters,
      }}
    >
      {children}
    </FiltersContext.Provider>
  )
}


function useFilters(setPage) {
  const context = React.useContext(FiltersContext)

  if (context === undefined) {
    throw new Error('useFilters must be used within a FiltersProvider')
  }

  const {
    filters,
    setFilters
  } = context

  useEffect(() => {
    return () => {
      console.log('reset the filters to an empty object')
      setFilters({})
    }
  }, [setFilters])

  {... do some additional stuff with filters if needed... not relevant }

  return {
    ...context,
    filtersForQuery: {
      ...filters
    }
  }
}

export { FiltersProvider, useFilters }

The App.js utilises the Provider as:

import React from 'react'
import { FiltersProvider } from '../filtersContext'
    
const App = React.memo(
  ({ children }) => {
    ...
    ...
    return (
       ...
          <FiltersProvider>
             <RightSide flex={1} flexDirection={'column'}>
                <Box flex={1}>
                  {children}
                </Box>
             </RightSide>
          </FiltersProvider>
       ...
    )
  }
)

export default App

that is said, everything within FiltersProvider becomes the context of filters.

Now comes the problem description: I have selected on one page (Page1) the filter, but when I have to switch to another page (Page2), I need to flush the filters. This is done in the useFilters hook in the unmount using return in useEffect.

The problem is in the new page (Page2), during the first render I'm still getting the old values of filters, and than the GraphQL request is sent just after that. Afterwards the unmount of the hook happens and the second render of the new page (Page2) happens with set to empty object filters.

If anyone had a similar problem and had solved it?

first Page1.js:

 const Page1 = () => {
    ....
    const { filtersForQuery } = useFilters()
    
    const { loading, error, data } = useQuery(GET_THINGS, {
      variables: {
        filter: filtersForQuery
       }
      })
  ....
  }

second Page2.js:

 const Page2 = () => {
    ....
    const { filtersForQuery } = useFilters()
    
    console.log('page 2')

    const { loading, error, data } = useQuery(GET_THINGS, {
      variables: {
        filter: filtersForQuery
       }
      })
  ....
  }

Printout after clicking from page 1 to page 2:

1. filters {isActive: {id: true}}
2. filters {isActive: {id: true}}
3. page 2
4. reset the filters to an empty object
5. 2 reset the filters to an empty object
6. filters {}
7. page 2

Solution

  • For those in struggle:

    import React, { useState, useEffect, useCallback, **useRef** } from 'react'
    
    const FiltersContext = React.createContext({})
    
    function FiltersProvider({ children }) {
      const [filters, setFilters] = useState({})
    
      return (
        <FiltersContext.Provider
          value={{
            filters,
            setFilters,
          }}
        >
          {children}
        </FiltersContext.Provider>
      )
    }
    
    
    function useFilters(setPage) {
      const isInitialRender = useRef(true)
      const context = React.useContext(FiltersContext)
    
      if (context === undefined) {
        throw new Error('useFilters must be used within a FiltersProvider')
      }
    
      const {
        filters,
        setFilters
      } = context
    
      useEffect(() => {
        **isInitialRender.current = false**
        return () => {
          console.log('reset the filters to an empty object')
          setFilters({})
        }
      }, [setFilters])
    
      {... do some additional stuff with filters if needed... not relevant }
    
      return {
        ...context,
        filtersForQuery: { // <---- here the filtersForQuery is another variable than just filters. This I have omitted in the question. I will modify it. 
          **...(isInitialRender.current ? {} : filters)**
        }
      }
    }
    
    export { FiltersProvider, useFilters }
    

    What is done here: set the useRef bool varialbe and set it to true, as long as it is true return always an empty object, as the first render happens and/or the setFilters function updates, set the isInitialRender.current to false. such that we return updated (not empty) filter object with the hook.