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]);
Related
This is an API call and in console, i get all products . But when I use the same getProducts function in components I got undefined in console
export const getProducts = ()=> async(dispatch)=>{
try {
const data = await fetch("http://localhost:80/api/products/getallproducts",{
method:"GET",
headers:{
"Content-Type":"application/json"
}
});
const res = await data.json();
console.log(res);
dispatch({type:"SUCCESS_GET_PRODUCTS",payload:res});
} catch (error) {
dispatch({type:"FAIL_GET_PRODUCTS",payload:error.response});
}
}
I use it on Home page and got undefined instead of products as i am using same function of getProducts
import React, { useEffect } from 'react'
import Categories from '../components/Categories'
import Banner1 from '../components/Banner1'
import MaterialUiaresoul from '../components/MaterialUiaresoul'
import ProductSlide from '../components/ProductSlide'
import FeaturedProducts from '../components/FeaturedProducts'
import { useDispatch, useSelector } from 'react-redux'
import { getProducts } from '../redux/actions/action'
const Home = () => {
const products = useSelector(state => state.getproductsdata);
console.log(products)
const dispatch = useDispatch();
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
return (
<>
<MaterialUiaresoul/>
<ProductSlide/>
<Banner1/>
<Categories/>
<FeaturedProducts />
</>
)
}
export default Home
You are trying to dispatch something that is not redux action.
Let's see, you are trying to call this line dispatch(getProducts());
After getProduct call, it will return a new async function, that doesn't called and expect dispatch to be passed in it.
Normally actions look like this:
export function addTodo(text) {
return { type: ADD_TODO, text }
}
Its just a function that return a plain object with type as a required property.
When dealing with api calls using redux, its better to look into some libraries that will help you, such as redux-thunk or redux-saga for example. Redux actions sync by default and async behavior can be reached with use of some middlewares.
In your example, you can make your code work as expected if you will run your getProduct function, and then run response from it with dispatch passed as first argument:
const dispatch = useDispatch();
const createApiCall = getProduct();
createApiCall(dispatch)
I'm still not sure whether it will work and recommend you to look at redux-thunk. Its pretty easy to learn and use.
I am trying to pull information from one component's API call to then use that data in another API call in a separate component. However, I am unsure how to export and use the data from the first API call in the second component.
App.js
import './App.css';
import FetchMatch from './fetch-match/fetch.match';
import FetchPlayer from './fetch-player/fetch.player';
function App() {
return (
<div className="App">
<h1>Hello world</h1>
<FetchPlayer></FetchPlayer>
<FetchMatch></FetchMatch>
</div>
);
}
export default App;
fetch.player then makes the first API call to get a users specific ID which will be used in the second API call too fetch that users match history.
fetch.player.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const FetchPlayer = () => {
const [playerData, setPlayerData] = useState([]);
const userName = 'users name';
const userTagLine = '1234';
const apiKey = '???';
useEffect( () => {
axios.get(`https://americas.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${userName}/${userTagLine}?api_key=${apiKey}`)
.then(response => {
console.log(response.data)
setPlayerData([response.data])
})
.catch(error => console.log(error))
}, []);
return (
<div>
{playerData.map( data => (
<div>
<p>{data.puuid}</p>
<p>{data.gameName}#{data.tagLine}</p>
</div>
))}
</div>
)
}
export default FetchPlayer;
not much here but just in case...
fetch.match.js
import React, { useState } from 'react';
// Somehow take in the puuid set in the state of fetch.player to make a second API call below
const FetchMatch = () => {
const [matchData, setMatchData] = useState([]);
return (
<div>
// players match list goes here
</div>
)
}
export default FetchMatch;
I am unsure if I should make a separate function instead which would allow me to create consts to handle both API calls in a single file. Or if there is a way to pass the state from fetch.player as a prop to fetch.match from App.js. I have tried to do the former but it either doesn't work or I am messing up the syntax (most likely this)
If you render both component parallelly in a parent component, they are called sibling components.
Data sharing in sibling components can be done by multiple ways (Redux, Context etc) but the easiest and simplest way (the most basic way without 3rd party API) involves the use of parent as a middle component.
First you create the state in the parent component and provide it as props to the child component which need the data from its sibling (in your case is FetchMatch).
import React from 'react';
import './App.css';
import FetchMatch from './fetch-match/fetch.match';
import FetchPlayer from './fetch-player/fetch.player';
function App() {
const [data,setData] = React.useState();
return (
<div className="App">
<h1>Hello world</h1>
<FetchPlayer></FetchPlayer>
<FetchMatch data={data} ></FetchMatch>
</div>
);
}
export default App;
Provide the function to setData as a props to the child component which will fetch the initial API (in your case is FetchPlayer)
<FetchPlayer onPlayerLoad={(data) => setData(data)} />
Then, in that child component when you finish calling the API and get the result, pass that result to the onPlayerLoad function which will call the setData function with the result as parameters. It will lead to state change and re-rendering of the second FetchMatch component feeding the props data with API results.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const FetchPlayer = ({onPlayerLoad}) => {
const [playerData, setPlayerData] = useState([]);
const userName = 'users name';
const userTagLine = '1234';
const apiKey = '???';
useEffect( () => {
axios.get(`https://americas.api.riotgames.com/riot/account/v1/accounts/by-riot-id/${userName}/${userTagLine}?api_key=${apiKey}`)
.then(response => {
console.log(response.data)
setPlayerData([response.data])
onPlayerLoad(response.data)
})
.catch(error => console.log(error))
}, []);
return <></>;
Coming to FetchMatch, you will have the data in its second rendering.
import React, { useState } from 'react';
// Somehow take in the puuid set in the state of fetch.player to make a second API call below
const FetchMatch = ({data}) => {
const [matchData, setMatchData] = useState([]);
//console.log(data);
return (
<div>
// players match list goes here
</div>
)
}
export default FetchMatch;
Now, you can do whatever you want with the shared data in second component which in your case is trigger match API. 🎉
I'm trying to combine several contexts that are feed with some async operations, in my app's pages.
I would like to combine these contexts without using the Context.Provider because it could be verbose. For example,
<Route path="/discover">
<MainContainer extraClass="discover-container" hasHeader={true}>
<UserContext>
<ContentContextProvider>
<NotificationContext>
<Discover />
</NotificationContext>
</ContentContextProvider>
</UserContext>
</MainContainer>
</Route>
In each of these Context I wrapper the child with the context. Fe,
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const ContentContext = React.createContext({});
export const ContentContextProvider = ({ children }) => {
const { allContent, setAllContents } = useAllContent([]);
return (
<ContentContext.Provider value={{ allContent, setAllContents }}>
{children}
</ContentContext.Provider>
);
};
export default ContentContext;
This works, but as I mentioned before is very verbose so i would like to use the Contexts like an objetcs to combine between them.
I tried:
import { useState, useEffect, useCallback } from "react";
import { DataStore, Predicates } from "#aws-amplify/datastore";
import { Content } from "models";
const useAllContent = (initialValue) => {
const [allContent, setContent] = useState(initialValue);
const setAllContents = useCallback(async () => {
const contents = await DataStore.query(Content, Predicates.ALL);
setContent(contents);
}, []);
useEffect(() => {
if (allContent === 0) setAllContents();
}, [allContent, setAllContents]);
return { allContent, setAllContents };
};
export default useAllContent;
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const { allContent, setAllContents } = useAllContent([]);
const ContentContext = React.createContext({ allContent, setAllContents });
export default ContentContext;
But I break the rule × Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
How could i achieve it?
Combining instances of React.Context in the way you describe would require manipulating the values they contain. But there is no way to access the value of a Context without using its corresponding Provider. I sympathize with the dislike of verboseness, but it is unavoidable in this case.
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
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