React Native data in context is undefined on the first render - reactjs

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

Related

React Hook Function in UseEffect with infinite loop

I am trying to call a hook in my App.js file using a hook. All the logic works, but I'm getting a warning error in console "React Hook useEffect has a missing dependency: 'initAuth'." I know there are a lot of issues on this ,but I'm not sure if this is related to the hook or the complexity I am doing at the high level of my app. The intent is to use the "initAuth" function to look at my local storage and get my user token, name, etc... I only want this on a hard page refresh, so it should only run once.
If I add initAuth (the function) or the authObject ( object), I get infinite loops.
function App() {
const { initAuth, authObject } = useAuth();
useEffect(() => {
initAuth();
}, []);
// this throws the warning. I need to add dependency
}
If you only want this effect to run once when the component first loads, then you can ignore the warning. You can disable the warning so it doesn't keep showing up in the console with the following:
useEffect(() => {
initAuth();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
This is how I would implement this hook :
function App() {
const { initialized, authObject, initAuth } = useAuth();
useEffect(() => {
if (!initialized) {
initAuth();
}
}, [initialized, initAuth]);
...
}
Or, better yet :
function App() {
const authObject = useAuth(); // let useAuth initialize itself
...
}
Typically, useAuth seems to be a multi-purpose hook, being used by various components, so it makes no sense to allow multiple components to call initAuth; the hook should only return the current state.
Preferably, you should implement that hook with a context
function App() {
return (
<AuthProvider>
<AppContent />
</AuthProvider>
);
}
function AppContent() {
const authObject = useAuth();
...
}
The contract, therefore, goes to the AuthProvider, and notifies every component using useAuth on state changes.
From OP's own answer, added some suggested improvements :
import React, { createContext, useContext, useState, useMemo } from "react";
const AuthContext = createContext({
isLoggedIn:false /* :Boolean */,
authObject:null /* :Object */,
login: (
username /* :String */,
password /* :String */
) /* :Preomise<Boolean> */ => {
throw new Error('Provider missing');
}
]);
const AuthContextProvider = ({ children }) => {
// init state with function so we do not trigger a
// refresh from useEffect. Use useEffect if the
// initial state is asynchronous
const [state, setState] = useState(() => {
const authObject = localStorage.getItem("authObject");
const isLoggedIn = !!authObject;
return { isLoggedIn, authObject };
});
// avoid refresh if state does not change
const contextValue = useMemo(() => ({
...state, // isLoggedIn, authObject
login: async (username, password) => {
// implement auth protocol, here
// do not expose setState directly in order to
// control what state is actually returned
// setState({ isLoggedIn:..., authObject:... });
// return true|false
}
}), [state]);
return (
<AuthContext.Provider value={ contextValue }>
{ children }
</AuthContext.Provider>
);
};
/**
Usage: const { isLoggedIn, authObject, login } = useAuthContext();
*/
const useAuthContext = () => useContext(AuthContext);
export { useAuthContext, AuthContextProvider };
Thanks to Yanick's comment, this is how I initiated to provider to set my authorization. My login function uses an auth service for http call, but I use this context function to set the data properly.
import React, { useContext, useMemo, useState } from "react";
import http from "services/http";
const AuthContext = React.createContext({});
const AuthContextProvider = ({ children }) => {
const [state, setState] = useState(() => {
const authObject = JSON.parse(localStorage.getItem("authObject"));
if (authObject) {
//sets axios default auth header
http.setJwt(authObject.token);
}
const isLoggedIn = !!authObject;
return { isLoggedIn, authObject };
});
// avoid refresh if state does not change
const contextValue = useMemo(
() => ({
...state, // isLoggedIn, authObject
login(auth) {
localStorage.setItem("authObject", JSON.stringify(auth));
http.setJwt(auth.token);
setState({ authObject: auth, isLoggedIn: true });
return true;
},
logout() {
http.setJwt("");
localStorage.removeItem("authObject");
setState({ authObject: null, isLoggedIn: false });
},
}),
[state]
);
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
};
const useAuthContext = () => useContext(AuthContext);
export { useAuthContext, AuthContextProvider };
And my App.js simply uses the ContextProvider, no need to run useEffect anymore on App.js.
<AuthContextProvider>
<ThemeProvider theme={darkState ? dark() : light()}>
<CssBaseline>
<BrowserRouter>
//...app.js stuff
</BrowserRouter>
</CssBaseline>
</ThemeProvider>
</AuthContextProvider>
In any component, I can now get access to isLoggedIn or authObject using a call like:
const { isLoggedIn } = useAuthContext();

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

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

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