Set the data in React Context from asynchronous API call - reactjs

I am trying to initialize a custom React context with data from back end, using a GET API request. However, the context is loaded before the API call finishe the data fetching.
What I've tried is to use a consumer to send data to the child component but I can only access the default value of the context which is set then the context is created.
Here is how I am trying to set my context data
import React, {useState,useEffect} from "react";
import {callAffiliateApi} from "./services/affiliateService/transactionContext";
export const AffiliateContext = React.createContext("Raaaaaaaaaaaa");
export const AffiliateProvider = ({children}) => {
const [data, setData] = useState(null);
useEffect(()=> {
async function fetchData() {
const newText = await callAffiliateApi();
setData(newText)
};fetchData()
},[])
console.log("Data in the context", data);
if(data != null){
return (
<AffiliateContext.Provider value={data}>
{children}
</AffiliateContext.Provider>
)}
else {
return (
<AffiliateContext.Provider value={"Loading..."}>
{children}
</AffiliateContext.Provider>)
}
}
And here is how I'm trying to access it in the child component
import {AffiliateContext} from "../../../../AffiliateContext";
class Profile extends Component {
constructor(props) {
super(props);
this.state = {
text: this.props.text,
user: this.props.user,
}
}
render() {
return (
<AffiliateContext.Consumer>
{data =>
<div>
{data}
</div>}
</AffiliateContext.Consumer>
)
}
}
export default Profile;
However, the only thing that gets displayed in the page is "Raaaaaaa", the default value of the component. How can I make the child component wait until the data finishes fetching from the API request?

try to use useContext its cleaner and try not to use the async inside the useEffect!
their related issues
import React, { useState,useEffect,useContext } from "react";
import { callAffiliateApi } from "./services/affiliateService/transactionContext";
const Context = React.createContext({});
const AffiliateContext = init => useContext(Context);
export const AffiliateProvider = ({children}) => {
const [data, setData] = useState(null);
const [loading,setLoading]=useState(false);
const getAffiliates = async ()=>{
setLoading(true)
const newText = await callAffiliateApi();
setData(newText)
setLoading(false)
}
useEffect(()=> {
getAffiliates()
},[])
return (
<AffiliateContext.Provider value={{loading,list:data}}>
{children}
</AffiliateContext.Provider>
)
}

Related

Wait for context value to load before making an API call

I have a user context that loads the user data. I am using that data to send API requests in useEffect. The time lag in the loading of the data is causing an undefined variable in my API request. How do I make the useEffect wait for the context variable to load before sending the request?
This is the UserContext.js:
import { createContext, useState } from "react";
const UserContext = createContext({});
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
This is the custom hook:
import { useContext } from "react";
import UserContext from "../context/UserProvider";
const useUser = () => {
return useContext(UserContext);
};
export default useUser;
And this is the API call in the profile page:
const { user } = useUser();
useEffect(() => {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}, []);
How can I ensure user data is loaded before I make a request throughout my app?
In react, Context APi static data will be Passed to the Children at initial Load. But if you are using asynchronous data in context api, you have to use useEffect and add context value as dependency..
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
// Updating Context value Asynchronously..
setTimeout(() => {
setUser("data");
}, [3000]);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
const { user } = useUser();
useEffect(() => {
// Call api only if user contains data.
if(user != {}) {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}
}, [user]);

Render page on react after loading json

I have load() function:
async function load() {
let url = `www.com/file.json`
let data = await (await fetch(url)).json()
return data
}
I need to render my page after loading my json file from server:
export const Page = async () => {
const data_ = await load()
return (
<div className="page">
content
</div>
)
}
How can i do it?
You can use the useEffect hook to make the call when the component mounts, it will be called once and fetch the data you need, then it will set it in the data state to be used in the return inside the divs.
As you did not provide the structure of the response I cannot give a more detailed explanation on how to render the data itself. Also id_ has no use in your example but I kept it there to closely resemble your example.
import React, {useState, useEffect} from "react";
export const Page = () => {
const [data, setData] = useState(null)
useEffect(() => {
const load = async (id_) => {
let url = `www.com/file.json`
let data = await (await fetch(url)).json()
const manipulatedData = ...
// do manipulation
setData(manipulatedData)
}
load()
}, [])
return (
<div className="page">
{data ? data : null}
</div>
);
}
export default Page;
You need to use useEffect and useState
Here's how you could achieve this:
import {useState, useEffect} from 'react';
export const Page = () => {
const [data, setData] = useState();
useEffect(() => {
async function load() {
let url = `www.com/file.json`;
let data = await (await fetch(url)).json();
setData(data);
}
load();
}, []);
return <div className="page">{JSON.stringify(data)}</div>;
}
Replace JSON.stringify with data.something to show a particular field in the data
A couple of tips:
React components cannot be async functions
useState carries variables you need to render the page
useEffect is a function that lets your component handle calling async code or any other kind of side effect
You can use the hook useEffect() & useState() to load your data :
function load() {
let url = `www.com/file.json`
let data = await (await fetch(url)).json()
return data
}
export const Page = async () => {
const [data, setData] = useState(null)
useEffect(() => {
load().then((_data) => {
setData(_data)
})
}, [])
if (!data) {
return <div>loading data...</div>
}
return (
<div className="page">
data is ready to use !
</div>
)
}

How to use context is a react class that is not a component

In my app I have a class that works with a server (sends and receives data). I need to pass to this class a token. I have a context that holds it. But I can't find a way to send it to my class.
This is a Context:
import React, { useState, createContext } from 'react';
import loginService from '../service/LoginService';
export const AuthContext = createContext();
const AuthContextProvider = (props) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userToken, setUserToken] = useState("");
const [userId, setUserId] = useState("");
const [userName, setUserName] = useState("");
const toggleAuth = () => {
if (!isAuthenticated) {
setUserToken(loginService.getUserToken());
setUserId(loginService.getUserId());
setUserName(loginService.getUserName());
console.log("The user id: " + userId);
setIsAuthenticated(true);
} else {
setUserToken(null);
setUserId(null);
setUserName(null);
setIsAuthenticated(false);
}
}
return (
<AuthContext.Provider value={{ isAuthenticated, toggleAuth }}>
{props.children}
</AuthContext.Provider>
);
}
export default AuthContextProvider;
And this is how I try to use the context in my class:
class MediaService {
static contextType = AuthContext;
galleryItems = [];
constructor() {
this.galleryItems = [];
}
addItem = (item) => {
axios.post("http://localhost:8080/image", item, {
headers: {
"Authorization": `Barier ${**this.context.userToken**}`
}
})
.then((response) => {
item.id = response.data.id;
this.galleryItems.push(item);
}).catch((err) => {
console.error(err);
});
}
}
But I get an error that this.context is undefined. How can I change my code to overcome this?
I know about useContext, but when I try to switch to the function I get another error that useContext can be used only in components, but this is not a component.
And I want to keep the database logic in a separate class, not in any component at all.

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

asynchronous context with useEffect in React

im trying to create an api request with the header value, that is received from a context component. However, as soon as the page component is loaded, it throws an Cannot read property '_id' of null exception. Is there a way to run the useEffect function, as soon as the context is loaded?
main component:
import React, { useState, useEffect, useContext } from "react";
import "./overview.scss";
/* COMPONENTS */;
import axios from 'axios';
import { GlobalContext } from '../../components/context/global';
const Overview = () => {
const [bookings, setBookings] = useState([]);
const [loaded, setLoaded] = useState(false);
const [user, setUser] = useContext(GlobalContext);
useEffect(() => {
axios
.get(`/api/v1/bookings/user/${user._id}`)
.then(res => setBookings(res.data))
.catch(err => console.log(err))
.finally(() => setLoaded(true));
}, [user]);
context component:
import React, {useState, useEffect, createContext} from 'react';
import jwt from 'jsonwebtoken';
/* GLOBAL VARIABLES (CLIENT) */
export const GlobalContext = createContext();
export const GlobalProvider = props => {
/* ENVIRONMENT API URL */
const [user, setUser] = useState([]);
useEffect(() => {
const getSession = async () => {
const user = await sessionStorage.getItem('authorization');
setUser(jwt.decode(user));
}
getSession();
}, [])
return (
<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>
);
};
The issue here is useEffect is running on mount, and you don't have a user yet. You just need to protect against this scenario
useEffect(() => {
if (!user) return;
// use user._id
},[user])
Naturally, when the Context fetches the user it should force a re-render of your component, and naturally useEffect should re-run as the dependency has changed.
put a condition before rendering you GlobalProvider, for example:
return (
{user.length&&<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>}
);
If user is not an array just use this
return (
{user&&<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>}
);

Resources