Search code examples
reactjsreact-hooksreact-contextreact-functional-componentuse-context

Access React Context value outside of the body of a function component


Case

I want to make isLoading (global state using React Context) value and changeIsLoading function (its changing function from IsLoadingContext.js file) becomes accessible to all files (function components and simple javascript functions).

I know that React Hooks can only be called inside of the body of a function component.

Question: So in my case here, how could I called isLoading and changeIsLoading inside a util file (non-function component or just a simple javascript function)?

What should I change from the code?

Code flow

  1. (location: SummariesPage.js) Click the button inside SummariesPage component
  2. (location: SummariesPage.js) Call onApplyButtonIsClicked function in SummariesPage component
  3. (location: SummariesPage.js) Change isLoading global state into true then call fetchAPISummaries function
  4. (location: fetchAPISummaries.js) Call fetchAPICycles function
  5. (location: fetchAPICycles.js) Call exportJSONToExcel function
  6. (location: exportJSONToExcel.js) Export the JSON into an Excel file then change isLoading global state into false
  7. IsLoadingContextProvider component will be rerendered and the isLoading value in SummariesPage will be true

Error logs

Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

The code

IsLoadingContext.js:

import React, { useState } from 'react'

const IsLoadingContext = React.createContext()

const IsLoadingContextProvider = (props) => {
    const [isLoading, setIsLoading] = useState(false)

    const changeIsLoading = (inputState) => {
        setIsLoading(inputState)
    }

    return(
        <IsLoadingContext.Provider
            value={{
                isLoading,
                changeIsLoading
            }}
        >
            {props.children}
        </IsLoadingContext.Provider>
    )
}

export { IsLoadingContextProvider, IsLoadingContext }

SummariesPage.js:

import React, { useContext } from 'react'

// CONTEXTS
import { IsLoadingContext } from '../../contexts/IsLoadingContext'

// COMPONENTS
import Button from '@material-ui/core/Button';

// UTILS
import fetchAPISummaries from '../../utils/export/fetchAPISummaries'

const SummariesPage = () => {
    const { isLoading, changeIsLoading } = useContext(IsLoadingContext)

    const onApplyButtonIsClicked = () => {
        changeIsLoading(true)
        fetchAPISummaries(BEGINTIME, ENDTIME)
    }

    console.log('isLoading', isLoading)

    return(
        <Button
            onClick={onApplyButtonIsClicked}
        >
            Apply
        </Button>
    )
}

export default SummariesPage

fetchAPISummaries.js:

// UTILS
import fetchAPICycles from './fetchAPICycles'

const fetchAPISummaries = (inputBeginTime, inputEndTime) => {
    const COMPLETESUMMARIESURL = .....
    
    fetch(COMPLETESUMMARIESURL, {
        method: "GET"
    })
    .then(response => {
        return response.json()
    })
    .then(responseJson => {
        fetchAPICycles(inputBeginTime, inputEndTime, formatResponseJSON(responseJson))
    })
}

const formatResponseJSON = (inputResponseJSON) => {
    const output = inputResponseJSON.map(item => {
        .....
        return {...item}
    })
    return output
}

export default fetchAPISummaries

fetchAPICycles.js

// UTILS
import exportJSONToExcel from './exportJSONToExcel'

const fetchAPICycles = (inputBeginTime, inputEndTime, inputSummariesData) => {
    const COMPLETDEVICETRIPSURL = .....

    fetch(COMPLETDEVICETRIPSURL, {
        method: "GET"
    })
    .then(response => {
        return response.json()
    })
    .then(responseJson => {
        exportJSONToExcel(inputSummariesData, formatResponseJSON(responseJson))
    })
}

const formatResponseJSON = (inputResponseJSON) => {
    const output = inputResponseJSON.map(item => {
        .....
        return {...item}
    })
    return output
}

export default fetchAPICycles

exportJSONToExcel.js

import { useContext } from 'react'

import XLSX from 'xlsx'

// CONTEXTS
import { IsLoadingContext } from '../../contexts/IsLoadingContext'

const ExportJSONToExcel = (inputSummariesData, inputCyclesData) => {
    const { changeIsLoading } = useContext(IsLoadingContext)

    const sheetSummariesData = inputSummariesData.map((item, index) => {
        let newItem = {}
        .....
        return {...newItem}
    })

    const sheetSummaries = XLSX.utils.json_to_sheet(sheetSummariesData)
    
    const workBook = XLSX.utils.book_new()

    XLSX.utils.book_append_sheet(workBook, sheetSummaries, 'Summaries')
    
    inputCyclesData.forEach(item => {
        const formattedCycles = item['cycles'].map((cycleItem, index) => {
            .....
            return {...newItem}
        })

        const sheetCycles = XLSX.utils.json_to_sheet(formattedCycles)
        XLSX.utils.book_append_sheet(workBook, sheetCycles, item['deviceName'])
    })

    XLSX.writeFile(workBook, `......xlsx`)

    changeIsLoading(false)
}

export default ExportJSONToExcel

Solution

  • I believe the real problem you are facing is managing the asynchronous calls. It would be much readable if you use async/await keywords.

        const onApplyButtonIsClicked = async () => {
            changeIsLoading(true)
            await fetchAPISummaries(BEGINTIME, ENDTIME)
            changeIsLoading(false)
        }
    

    You will need to rewrite fetchAPICycles to use async/await keywords instead of promises.

    const fetchAPICycles = async (
      inputBeginTime,
      inputEndTime,
      inputSummariesData
    ) => {
      const COMPLETDEVICETRIPSURL = ...;
    
      const response = await fetch(COMPLETDEVICETRIPSURL, {
        method: "GET",
      });
      const responseJson = await response.json();
      exportJSONToExcel(inputSummariesData, formatResponseJSON(responseJson));
    };