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

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
(location: SummariesPage.js) Click the button inside SummariesPage component
(location: SummariesPage.js) Call onApplyButtonIsClicked function in SummariesPage component
(location: SummariesPage.js) Change isLoading global state into true then call fetchAPISummaries function
(location: fetchAPISummaries.js) Call fetchAPICycles function
(location: fetchAPICycles.js) Call exportJSONToExcel function
(location: exportJSONToExcel.js) Export the JSON into an Excel file then change isLoading global state into false
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

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));
};

Related

React Context Value always default and not updated

I have created one wrapper component where I put my react context.
Inside that wrapper component I have used useEffect() hook where I fetch values from api and try to update context default values.
In my child component I tried to fetch context values but only default value of that context is fetched. So it seems that useEffect hook didnt updated my context object.
Here is wrapper component:
export const CorporateWrapper = ({ apiBaseUrl, children }) => {
const [corporateContextDefaults, setCorporateContextDefaults] = useState({});
useEffect(() => {
(async () => {
try {
const json = await fetchCorporateUserDetails(apiBaseUrl, getClientSideJwtTokenCookie());
if (json.success !== true) {
console.log(json.message);
return {
notFound: true,
};
}
console.log(json.data);
setCorporateContextDefaults({corporateId: json.data.corporate_id, corporateRole: json.data.corporate_role, corporateAdmin: json.data.corporate_role == 'Admin', corporateSuperAdmin: json.data.corporate_super_admin});
} catch (e) {
console.log(e.message);
}
})();
}, []);
return (
<CorporateProvider value={corporateContextDefaults}>
{children}
</CorporateProvider>
);
};
Here is CorporateProvider component:
import React, { useState, useContext } from "react";
const CorporateContext = React.createContext({corporateId: null, corporateRole: null,
corporateAdmin: null, corporateSuperAdmin: null});
const UpdateCorporateContext = React.createContext({});
export const useCorporateContext = () => {
return useContext(CorporateContext);
};
export const useUpdateCorporateContext = () => {
return useContext(UpdateCorporateContext);
};
export const CorporateProvider = ({ value, children }) => {
const [details, setDetails] = useState(value);
return (
<CorporateContext.Provider value={details}>
<UpdateCorporateContext.Provider value={setDetails}>
{children}
</UpdateCorporateContext.Provider>
</CorporateContext.Provider>
);
};
export default CorporateProvider;
Here is how I try to fetch context value from child component which is wrapped under wrapper component:
const { corporateId } = useCorporateContext();

React Native data in context is undefined on the first render

I use AppContext, when I fetch data from server I want it to save in context but on the first render it doesn't save. If I make something to rerender state data appears in context.
Here is my code:
useEffect(() => {
fetch('https://beautiful-places.ru/api/places')
.then((response) => response.json())
.then((json) => myContext.updatePlaces(json))
.then(() => console.log('jsonData', myContext.getPlaces()))
.catch((error) => console.error(error));
}, []);
My getPlaces and updatePlaces methods:
const [allPlaces, setAllPlaces] = useState();
const getPlaces = () => {
return allPlaces;
};
const updatePlaces = (json) => {
setAllPlaces(json);
};
const placesSettings = {
getPlaces,
updatePlaces,
};
Here is how I use AppContext:
<AppContext.Provider value={placesSettings}>
<ThemeProvider>
<LoadAssets {...{ assets }}>
<SafeAreaProvider>
<AppStack.Navigator headerMode="none">
<AppStack.Screen
name="Authentication"
component={AuthenticationNavigator}
/>
<AppStack.Screen name="Home" component={HomeNavigator} />
</AppStack.Navigator>
</SafeAreaProvider>
</LoadAssets>
</ThemeProvider>
</AppContext.Provider>;
Could you explain please why my console.log('jsonData', ...) returns undefined?
I don't understand because on previous .then I saved it.
Edit to note that the code below is not copy-paste ready. It is an example of how to attack the problem – you will need to implement it properly in your project.
The 'problem' is that hooks are asynchronous – in this specific case, your useEffect further uses an asynchronous fetch too.
This means that the data that is returned by the fetch will only be available after the component has rendered, and because you're not updating state/context using a hook, the context won't update.
The way to do this requires a few changes.
In your context implementation, you should have a setter method that sets a state variable, and your getter should be that state variable.
placesContext.js
import React, { createContext, useState } from "react";
export const placesContext = createContext({
setPlaces: () => {},
places: [],
});
const { Provider } = placesContext;
export const PlacesProvider = ({ children }) => {
const [currentPlaces, setCurrentPlaces] = useState(unit);
const setPlaces = (places) => {
setCurrentPlaces(places);
};
return (
<Provider value={{ places: currentPlaces, setPlaces }}>{children}</Provider>
);
};
Wrap your App with the created Provider
App.js
import { PlacesProvider } from "../path/to/placesContext.js";
const App = () => {
// ...
return (
<PlacesProvider>
// Other providers, and your app Navigator
</PlacesProvider>
);
}
Then, you should use those variables directly from context.
MyComponent.js
import { placesContext } from "../path/to/placesContext.js";
export const MyComponent = () => {
const { currentPlaces, setPlaces } = useContext(placesContext);
const [hasLoaded, setHasLoaded] = useState(false);
useEffect(() => {
async function fetchPlacesData() {
const placesData = await fetch('https://beautiful-places.ru/api/places');
if (placesData) {
setPlaces(placesData);
} else {
// error
}
setHasLoaded(true);
}
!hasLoaded && fetchPlacesData();
}, [hasLoaded]);
return (
<div>{JSON.stringify(currentPlaces)}</div>
)
};

React Hooks + Mobx => Invalid hook call. Hooks can only be called inside of the body of a function component

I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}

React hooks & Context: Error when using context inside a child component with useEffect

I've created a react function component for the context as follows:
const ItemContext = createContext()
const ItemProvider = (props) => {
const [item, setItem] = useState(null)
const findById = (args = {}) => {
fetch('http://....', { method: 'POST' })
.then((newItem) => {
setItem(newItem)
})
}
let value = {
actions: {
findById
},
state: {
item
}
}
return <ItemContext.Provider value={value}>
{props.children}
</ItemContext.Provider>
}
In this way, I have my context that handles all the API calls and stores the state for that item. (Similar to redux and others)
Then in my child component further down the line that uses the above context...
const smallComponent = () =>{
const {id } = useParams()
const itemContext = useContext(ItemContext)
useEffect(()=>{
itemContext.actions.findById(id)
},[id])
return <div>info here</div>
}
So the component should do an API call on change of id. But I'm getting this error in the console:
React Hook useEffect has a missing dependency: 'itemContext.actions'. Either include it or remove the dependency array react-hooks/exhaustive-deps
If I add it in the dependency array though, I get a never ending loop of API calls on my server. So I'm not sure what to do. Or if I'm going at this the wrong way. Thanks.
=== UPDATE ====
Here is a jsfiddle to try it out: https://jsfiddle.net/zx5t76w2/
(FYI I realized the warning is not in the console as it's not linting)
You could just utilize useCallback for your fetch method, which returns a memoized function:
const findById = useCallback((args = {}) => {
fetch("http://....", { method: "POST" }).then(newItem => {
setItem(newItem);
});
}, []);
...and put it in the useEffect:
...
const { actions, state } = useContext(ItemContext)
useEffect(() => {
actions.findById(id)
}, [id, actions.findById])
...
Working example: https://jsfiddle.net/6r5jx1h7/1/
Your problem is related to useEffect calling your custom hook again and again, because it's a normal function that React is not "saving" throughout the renders.
UPDATE
My initial answer fixed the infinite loop.
Your problem was also related to the way you use the context, as it recreates the domain objects of your context (actions, state, ..) again and again (See caveats in the official documentation).
Here is your example in Kent C. Dodds' wonderful way of splitting up context into state and dispatch, which I can't recommend enough. This will fix your infinite loop and provides a cleaner structure of the context usage. Note that I'm still using useCallback for the fetch function based on my original answer:
Complete Codesandbox https://codesandbox.io/s/fancy-sea-bw70b
App.js
import React, { useEffect, useCallback } from "react";
import "./styles.css";
import { useItemState, ItemProvider, useItemDispatch } from "./item-context";
const SmallComponent = () => {
const id = 5;
const { username } = useItemState();
const dispatch = useItemDispatch();
const fetchUsername = useCallback(async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/" + id
);
const user = await response.json();
dispatch({ type: "setUsername", usernameUpdated: user.name });
}, [dispatch]);
useEffect(() => {
fetchUsername();
}, [fetchUsername]);
return (
<div>
<h4>Username from fetch:</h4>
<p>{username || "not set"}</p>
</div>
);
};
export default function App() {
return (
<div className="App">
<ItemProvider>
<SmallComponent />
</ItemProvider>
</div>
);
}
item-context.js
import React from "react";
const ItemStateContext = React.createContext();
const ItemDispatchContext = React.createContext();
function itemReducer(state, action) {
switch (action.type) {
case "setUsername": {
return { ...state, username: action.usernameUpdated };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function ItemProvider({ children }) {
const [state, dispatch] = React.useReducer(itemReducer, {
username: "initial username"
});
return (
<ItemStateContext.Provider value={state}>
<ItemDispatchContext.Provider value={dispatch}>
{children}
</ItemDispatchContext.Provider>
</ItemStateContext.Provider>
);
}
function useItemState() {
const context = React.useContext(ItemStateContext);
if (context === undefined) {
throw new Error("useItemState must be used within a CountProvider");
}
return context;
}
function useItemDispatch() {
const context = React.useContext(ItemDispatchContext);
if (context === undefined) {
throw new Error("useItemDispatch must be used within a CountProvider");
}
return context;
}
export { ItemProvider, useItemState, useItemDispatch };
Both of these blog posts helped me a lot when I started using context with hooks initially:
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/how-to-use-react-context-effectively
OK, I didn't want to write an answer as Bennett basically gave you the fix, but I think it is missing the part in the component, so here you go:
const ItemProvider = ({ children }) => {
const [item, setItem] = useState(null)
const findById = useCallback((args = {}) => {
fetch('http://....', { method: 'POST' }).then((newItem) => setItem(newItem))
}, []);
return (
<ItemContext.Provider value={{ actions: { findById }, state: { item } }}>
{children}
</ItemContext.Provider>
)
}
const smallComponent = () => {
const { id } = useParams()
const { actions } = useContext(ItemContext)
useEffect(() => {
itemContext.actions.findById(id)
}, [actions.findById, id])
return <div>info here</div>
}
Extended from the comments, here's the working JSFiddle

How to combine custom hook for data fetching and context?

I have a custom hook to fetch data on form submit
export const getIssues = ({ user, repo }) => {
const [issues, setIssues] = useState([]);
const handleInputChange = (e) => {
e.preventDefault();
axios.get(`https://api.github.com/repos/${user}/${repo}/issues`)
.then((response) => {
setIssues(response.data);
})
.catch((err) => console.log(err));
};
return {
issues,
onSubmit: handleInputChange,
};
};
In my component I call it like this
const response = getIssues({ user: user.value, repo: repo.value })
return (
<form className={css['search-form']} {...response}>...</form>
)
The problem is that I want to get my issues value from the hook in another component. For that I wanted to use Context. But I have no idea how to do it.
I could call this function and pass it to Provider, but I can't call it without arguments. So I kind of stuck.
All the help will be much appreciated.
You are right by saying you need React.Context to handle this situation.
You need to wrap your components into this context.
import React from "react";
const IssuesStateContext = React.createContext();
const IssuesDispatchContext = React.createContext();
function issuesReducer(state, action) {
switch (action.type) {
case "setIssues": {
return [...action.payload];
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function IssuesProvider({ children }) {
const [state, dispatch] = React.useReducer(issuesReducer, []);
return (
<IssuesStateContext.Provider value={state}>
<IssuesDispatchContext.Provider value={dispatch}>
{children}
</IssuesDispatchContext.Provider>
</IssuesStateContext.Provider>
);
}
function useIssuesState() {
const context = React.useContext(IssuesStateContext);
if (context === undefined) {
throw new Error("useIssuesState must be used within a IssuesProvider");
}
return context;
}
function useIssuesDispatch() {
const context = React.useContext(IssuesDispatchContext);
if (context === undefined) {
throw new Error("useIssuesDispatch must be used within a IssuesProvider");
}
return context;
}
export { IssuesProvider, useIssuesState, useIssuesDispatch };
By using this separation in context you will be able to set issues coming from github in one component and render them in a completely different one.
Example:
App.js
ReactDOM.render(
<IssuesProvider>
<Component1 />
<Component2 />
</IssuesProvider>
)
Component 1
import React from 'react'
import { useIssuesDispatch } from './issues-context'
function Component1() {
const dispatch = useIssuesDispatch()
// fetch issues
// .then dispatch({ type: 'setIssues', payload: response })
// render
}
Component 2
import React from 'react'
import { useIssuesState } from './issues-context'
function Component2() {
const issues = useIssuesState()
// if issues.length > 0 ? render : null
}
You can write a Issues context provider that will provide {issues,useIssues} where issues are the issues and useIssues is a function that takes {user,repo}.
export const Issues = React.createContext();
export default ({ children }) => {
const [issues, setIssues] = useState([]);
const useIssues = ({ user, repo }) => {
useEffect(() => {
axios
.get(
`https://api.github.com/repos/${user}/${repo}/issues`
)
.then(response => {
setIssues(response.data);
})
.catch(err => console.log(err));
}, [user, repo]);
return issues;
};
return (
<Issues.Provider value={{ issues, useIssues }}>
{children}
</Issues.Provider>
);
};
The component that has all the components that need issues can import this issues provider:
import IssuesProvider from './IssuesProvider';
export default () => (
<IssuesProvider>
<ComponentThatNeedsIssues />
<ComponentThatSetsAndGetsIssues />
</IssuesProvider>
);
For a component that needs to set issues you can get useIssues from context:
const { useIssues } = useContext(Issues);
const issues = useIssues({user,repo});
For a component that only needs issues:
const { issues } = useContext(Issues);
To see it all work together there is a codepen here

Resources