React JS useState initial value not updating inside JS fetch API - javascript

I am novice to React JS. I have useState and fetchAPI inside contextAPI hooks but the initial state is not updating.
// code
import React,{useState, createContext} from 'react'
export const contextApi = createContext()
export const ContextApiProvider = (props) => {
const [query, setQuery] = useState('chicken')
const [recipes, setRecipes] = useState([])
const api_props = {
APP_ID: '84cf712e',
APP_KEY:'asdcb2b8b842f3e543casjakfa710de4fb343592a64d',
APP_QUERY: query
}
fetch(`https://api.edamam.com/search?q=${api_props.APP_QUERY}&app_id=${api_props.APP_ID}&app_key=${api_props.APP_KEY}`)
.then(res => res.json()).then(data => setRecipes(data.hits))
return (
<contextApi.Provider value={{recipes}}>
{props.children}
</contextApi.Provider>
)
}

First look up the useEffect hook that is where you want to do your data fetching. From there you could set the state using the setState hook that you are running. This might create an endless loop because your are setting state which reruns the component which then trys to set state again.
Hope that helps let me know if you have questions.

Related

What is the React Hooks lifecycle for useEffect, useState, and useContext for multiple components?

TLDR: My problem is: useState in the context is not re-rendering in time for useContext from another component to get the updated state values.
If you don't want to read the full explanation, a brief explanation of the 3 hooks in different components would be very much appreciated too. Thanks!
TokenProvider: context
TeeToken, AccessToken, IdToken, and RefreshToken: view components that display context
App.js
<TokenProvider>
<TeeToken />
<AccessToken />
<IdToken />
<RefreshToken />
...
</TokenProvider>
TokenProvider.js
import React, { useState, useEffect } from 'react';
import TokenContext from '../shared/TokenContext';
const TokenProvider = ({ children }) => {
const [teeToken, setTeeToken] = useState('');
const [accessToken, setAccessToken] = useState('');
const [idToken, setIdToken] = useState('');
const [refreshToken, setRefreshToken] = useState('');
const [tokens, setTokens] = useState('');
// for data fetching logic
const [responseData, setResponseData] = useState('');
const [uniqueId, setUniqueId] = useState('');
const loadToken = () => {
const { oauth } = responseData;
setUniqueId(responseData.reduxRequestId);
setTeeToken(oauth.teeToken);
setAccessToken({ decoded: jwt.decode(oauth.accessToken), encoded: oauth.accessToken });
setIdToken({ decoded: jwt.decode(oauth.idToken), encoded: oauth.idToken });
setRefreshToken(oauth.refreshToken);
};
if (responseData) {
if (uniqueId !== responseData.reduxRequestId) {
loadToken();
}
}
useEffect(() => {
setTokens({ teeToken, accessToken, idToken, refreshToken });
}, [teeToken]);
const context = {
tokens,
setResponseData,
}
return <TokenContext.Provider value={context}>{children}</TokenContext.Provider>;
};
export default TokenProvider;
I omitted a lot of the fetching part of the code for the sake of this question, but the point is I use useState to set all TeeToken, AccessToken, IdToken, and RefreshToken constant values from the fetched data and put them all nicely into tokens. Tokens is then put into the context.
I'm using useEffect because putting useState of tokens can not get the updated versions of all the various tokens because useState is an asynchronous action. So I'm waiting for the component to re-render, then tokens can get all the various updated values and re-render again to update the context.
However, the other component that uses useContext gets the un-updated version of the tokens. Please help how I could make the other component get the updated version of the tokens! Thanks.
EDIT: technically my issue can be solved by just passing all various tokens into context instead of keeping it in tokens, but I would still appreciate it if somebody could give me an explanation on the React Hooks lifecycle

How to pass route params in Context API

I'm using the context API and I have this in my context file:
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${props.match.alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (props.match) getSingleCountryData();
}, [props.match]);
In the component I'm using, it doesn't work because it doesn't know what the props.match.alpha3Code is. How can I can pass the value? The alpha3Code is coming from the URL: localhost:3000/country/asa where asa is the alpha3Code, how can I get this value?
Basically, what I'm trying to do is. I have a list of countries I listed out on the home page. Now I'm trying to get more information about a single country. The route is /country/:alpha3Code where alpha3Code is gotten from the API.
FWIW, here is my full context file:
import React, { useState, createContext, useEffect } from 'react';
import axios from 'axios';
export const CountryContext = createContext();
export default function CountryContextProvider(props) {
const [countries, setCountries] = useState([]);
const [country, setCountry] = useState([]);
useEffect(() => {
const getCountryData = async () => {
const result = await axios.get(
'https://cors-anywhere.herokuapp.com/https://restcountries.eu/rest/v2/all'
);
setCountries(result.data);
};
getCountryData();
}, []);
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${props.match.alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (props.match) getSingleCountryData();
}, [props.match]);
return (
<CountryContext.Provider value={{ countries, country }}>
{props.children}
</CountryContext.Provider>
);
}
In the component I'm using the country, I have:
const { country } = useContext(CountryContext);
I know I can do this from the component itself, but I'm learning how to use the context API, so I'm handling all API calls in my context.
The API I'm making use of is here
Codesandbox Link
Project Github link
You can update the context from a component using it by passing down a setter function which updates the context state.
export default function CountryContextProvider({ children }) {
const [countries, setCountries] = useState([]);
const [country, setCountry] = useState([]);
const [path, setPath] = useState('');
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(`your/request/for/${path}`);
setCountry(result.data);
}
if(path) getSingleCountryData();
}, [path]);
return (
<CountryContext.Provider value={{ countries, country, setPath }}>
{children}
</CountryContext.Provider>
);
}
Now use setPath to update the request endpoint with the route match once this component is mounted.
const Details = ({ match }) => {
const {
params: { alpha3Code }
} = match;
const { country, setPath } = useContext(CountryContext);
useEffect(() => {
setPath(alpha3Code);
}, [alpha3Code]);
return (
<main>Some JSX here</main>
);
};
export default withRouter(Details);
Linked is a working codesandbox implementation
In the component I'm using, it doesn't work because it doesn't know
what the props.match.alpha3Code is. How can I can pass the value? The
alpha3Code is coming from the URL: localhost:3000/country/asa where
asa is the alpha3Code, how can I get this value?
I guess the root of your problem is this one. You have no idea which the aplha3Code parameter comes from. I have dived into your GitHub repo to make it clearer.
First, match is one of react-router provided terms. When you use something like props.match, props.history, props.location, you must have your component wrapped by the withRouter, which is a Higher Order Component provided by react-router. Check it out at withRouter. For example, below is the withRouter usage which is provided by react-router:
// A simple component that shows the pathname of the current location
class ShowTheLocation extends React.Component {
render() {
const { match, location, history } = this.props;
return <div>You are now at {location.pathname}</div>;
}
}
const ShowTheLocationWithRouter = withRouter(ShowTheLocation);
ShowTheLocation is wrapped by the withRouter HOC, which will pass all the route props (match, history, location...) to ShowTheLocation through props. Then inside ShowTheLocation, you are able to use something like props.match. Clear enough?
So back to your problem! You have not wrapped any components by withRouter yet, have you? Stick to it and have some fun! You will figure it out soon!
Also, please be aware that you must place your component under the BrowserRouter to be able to use the react-router things
If you want to go with Hooks, please take a look at this super useful one:
https://usehooks.com/useRouter/
It wraps all the useParams, useLocation, useHistory, and use useRouteMatch hooks up into a single useRouter that exposes just the data and methods we need. Then, for example, inside your component, do it like this:
import { useRouter } from "./myCustomHooks";
const ShowMeTheCode = () => {
const router = useRouter();
return <div>This is my alpha3Code: {router.math.params.alpha3Code}</div>;
}
Update 1 from Peoray's reply:
This is where the problem occurs:
https://github.com/peoray/where-in-the-world/blob/cb09871fefb2f58f5cf0a4f1db3db2cc5227dfbe/src/pages/Details.js#L6
You should avoid calling useContext() straightly like that. Have a look at my example below:
// CountryContext.js
import { useContext, createContext } from "react";
const CountryContext = createContext();
export const useCountryContext = () => useContext(CountryContext);
Instead, you should wrap it by a custom hook like useCountryContext above. And then, inside your Details component, import it and do like:
import React, from 'react';
import { useCountryContext } from '../contexts/CountryContext';
const Details = (props) => {
const { country } = useCountryContext();
...
}
Update 2 from Peoray's reply:
Although I have stated it in advance for you, I just feel like you did not make enough effort to go through what I said.
Also, please be aware that you must place your component under the
BrowserRouter to be able to use the react-router things
In your codesandbox, it shows the Cannot read property 'match' of undefined error. Okay, as I said above, you have not moved the ContextCountryProvider to under the BrowserRouter to get the useRouter work.
I have fixed it for you, and the screen popped out, please check it at updated codesanbox here. You will get what you need at App.js file.
Although it still throws some Axios bugs there, I think my job is done. The rest is up to you.
You might use useParams hook to get everything you need inside your context provider. Docs
Something like this:
import useParams in file where your Provider component is
in your CountryContextProvider add this at the top of the component:
const { alpha3Code } = useParams();
update useEffect which needs props.match
useEffect(() => {
async function getSingleCountryData() {
const result = await axios(
`https://restcountries.eu/rest/v2/alpha/${alpha3Code.toLowerCase()}`
);
console.log(result)
setCountry(result.data);
}
if (alpha3Code) getSingleCountryData(); // or if you need `match` - do not destructure useParams()
}, [alpha3Code]);

How to prevent infinite loop caused by onAuthStateChanged Firebase Auth

I have a provider component that sets the initial auth context from firebase-auth.
Everything works fine until I try to add persistence in the form of setting up an observer with onAuthStateChanged. This checks for auth and I update my state via dispatch method.
But this is causing an infinite loop. I added an unsubscribe function call, but this makes no difference,
Can anyone advise? thanks
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
//firebaseauth reducer
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
const initialState = { user: {} };
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(firebaseAuth, initialState);
const value = { state, dispatch };
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: "HYDRATE_AUTH",
payload: user,
});
});
unsubscribe();
return <Auth.Provider value={value}>{props.children}</Auth.Provider>;
};
What is happening is:
The component renders
It sets the auth listener
The listener fires and sets the state of the component
State update causes the component to rerender
The component adds another listener and everything repeats
Unsubscribing doesn't help because the component keeps rerendering and adding a new listener every time.
We can tell the component to set the listener only once by using useEffect():
AuthContext.js
import React from "react";
import * as firebase from "firebase/app";
//firebaseauth reducer
import { firebaseAuth } from "../reducers/AuthReducer";
export const Auth = React.createContext();
const initialState = { user: {} };
export const AuthProvider = (props) => {
const [state, dispatch] = React.useReducer(firebaseAuth, initialState);
const value = { state, dispatch };
React.useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
dispatch({
type: "HYDRATE_AUTH",
payload: user,
});
});
}, []);
return <Auth.Provider value={value}>{props.children}</Auth.Provider>;
};
By providing an empty dependency array to useEffect(), we tell it to run the callback only once, when the component initially renders, so the auth listener is set only once.
Thanks to #tomek-ch -- an alternate solution would be to prevent the re-render by storing the state (eg: keep the user bits in the component state) and then do not update the state if its already there (or the same).
In my case I just keep the user the first time, set a boolean state flag, and ignore subsequent events.

Redux - Hooks: Using React Hooks with Redux Hooks, create infinite loop or nothing at all

I know this question has been answered a bunch of times already. I just cannot find the answer that solves my problem, leading me to believe, that I am either stupid or my problem has not been had because it is even more stupid than me.
So aside from that, this is my problem:
I am trying to create a functional component that takes some information from a redux state in order to render the correct language labels into a login form.
Let's start with the code:
import React, { useState, useEffect } from "react";
import { Paper, TextField } from "#material-ui/core";
import { changeLanguage } from "../redux/actions";
import { useDispatch, useSelector } from "react-redux";
const Login = () => {
const dispatch = useDispatch();
const [language, setLanguage] = useState("de");
const [strings, setStrings] = useState({});
useEffect(() => {
console.log("I have been called", language, strings);
setLanguage(["de", "en"].includes(navigator.language.split("-")[0]) ? navigator.language.split("-")[0] : "de");
}, [language]);
dispatch(changeLanguage(language));
const str = useSelector(state => state.base.strings.login);
console.log(str);
setStrings(str); // <- THIS ONE IS CAUSING THE ERROR
return (
<div className={"App"}>
<Paper>
<TextField label={strings.username}/><br/>
<TextField label={strings.password} type={"password"}/>
</Paper>
</div>
);
};
export default Login;
This is what I used to get the app working at all. I realize that setting the strings on every render will cause an infinite loop. That much is clear. However when using this code:
import React, { useState, useEffect } from "react";
import { Paper, TextField } from "#material-ui/core";
import { changeLanguage } from "../redux/actions";
import { useDispatch, useSelector } from "react-redux";
const Login = () => {
const dispatch = useDispatch();
const [language, setLanguage] = useState("de");
const [strings, setStrings] = useState({});
useEffect(() => {
console.log("I have been called", language, strings);
setLanguage(["de", "en"].includes(navigator.language.split("-")[0]) ? navigator.language.split("-")[0] : "de");
dispatch(changeLanguage(language));
const str = useSelector(state => state.base.strings.login);
console.log(str);
setStrings(str);
}, [language]);
return (
<div className={"App"}>
<Paper>
<TextField label={strings.username}/><br/>
<TextField label={strings.password} type={"password"}/>
</Paper>
</div>
);
};
export default Login;
I get this error:
/src/App/pages/login.js
Line 17:15: React Hook "useSelector" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
Search for the keywords to learn more about each error.
And yes, I do understand what it is telling me but I do believe that useEffect is a React Hook or am I missing something here?
I am simply looking for a way to get this to work. It cannot be that hard because I can make it work with class components no problem.
If the question is stupid, please elaborate on why instead of just voting it down. This would be a lot more helpful in developing and understanding for the matter.
I have consulted the docs for two hours and tried a bunch of stuff. Nothing has worked.
Thank you for taking the time!
useEffect(() => {
console.log("I have been called", language, strings);
setLanguage(["de", "en"].includes(navigator.language.split("-")[0]) ? navigator.language.split("-")[0] : "de");
dispatch(changeLanguage(language));
const str = useSelector(state => state.base.strings.login);
console.log(str);
setStrings(str);
}, [language]);
You are using one hook inside another.That is not allowed.Only place you can place hooks is inside Functional Component and outside any method.For more info https://reactjs.org/docs/hooks-rules.html

React custom hooks in callback

I'm trying to use my custom hook inside the callback logic like this:
import React, { useEffect, useState } from 'react';
import useDataChange from '../../hooks/useDataChange';
const SomeComponent = () => {
return (
<Table
handleTableChange={data => useDataChange(data)}
/>
);
};
export default SomeComponent;
And my custom hooks (just to simplify) looks like that:
const useDataChange = data => {
console.log(data);
};
export default useDataChange;
In short, custom hook supposed to be fired when data from table is changed (ie. when handleTableChange in Table component is fired). Instead I'm getting:
React Hook "useDataChange" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
How can I use it when table data is changed?
The key to understanding hooks is to extract pieces of react code out of components. So your first step would be to get it working inside the component
const SomeComponent = () => {
const [data, setData] = useState([])
return (
<Table
handleTableChange={setData}
/>
);
};
Based on your code, I'm not seeing where you'd need a hook or side effect. But let's pretend that you do want to run some simple side effect:
const SomeComponent = () => {
const [data, setData] = useState([])
const [modifiedData, setModifiedData] = useState([])
useEffect(() => {
//here we're just going to save the current data stream into a new state variable for simplicity
setModifiedData(data)
}, [data])
return (
<Table
handleTableChange={setData}
data={modifiedData}
/>
);
};
So now we have some logic that runs a side effect. Now you can extract it to its own hook.
const useModifiedData = (data) => {
const [modifiedData, setModifiedData] = useState(data)
useEffect(() => {
setModifiedData(data)
}, [data])
return modifiedData
}
const SomeComponent = () => {
const [data, setData] = useState([])
const modifiedData = useModifiedData(data)
return (
<Table
handleTableChange={setData}
data={modifiedData}
/>
);
};
Here you have a hook that lives outside the component logic, so it can now go in its own file and be used across your project.
Like it says React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks. React has this limitation so that it can track the state and effects. In your case you can define you custom hook to return a function which does the desired work, instead of directly doing it in your hook.
In this case your custom hook file will look something like this-
const useDataChange = () => data => {
console.log(data);
};
export default useDataChange;
Then in your component you can use it like this -
import React, { useEffect, useState } from 'react';
import useDataChange from '../../hooks/useDataChange';
const SomeComponent = () => {
const callback = useDataChnage();
return (
<Table handleTableChange={callbackdata} />
);
};
export default SomeComponent;

Categories

Resources