How to use useParams inside a useEffect? - javascript

I want to use useParams inside of useEffect but I am getting error saying that "Uncaught TypeError: Cannot read properties of undefined (reading 'params')".
app.js
<Route>
<Route path="/product/:id" element={<ProductDetails />} />
</Route>
ProductDetails.js
const dispatch = useDispatch();
const { id } = useParams();
useEffect(() => {
dispatch(getProductDetails(match.params.id));
}, [dispatch, match.params.id]);`
It is giving me error as I have mentioned above.

It looks like you half converted some code/logic from react-router#5 where there was a match prop injected into the component to react-router#6 where you instead use the useParams hook. match is simply undefined so an error is thrown when attempting to access into it a params property.
The useParams hook replaces entirely props.match.params. The id route path parameter alone is the dependency now.
const dispatch = useDispatch();
const { id } = useParams();
useEffect(() => {
dispatch(getProductDetails(id));
}, [dispatch, id]);

I don't think you want to use a hook inside of a useEffect function. To answer your question
You could try this
useEffect(() => {
dispatch(getProductDetails(match?.params?.id));
}, [dispatch, match?.params?.id]);`
But you're using javascript not typescript, so you can try this.
const param = match && match.params ? match.params.id : ''
useEffect(() => {
if (param) {
dispatch(getProductDetails(param));
}
}, [dispatch, param]);

Related

How can I access useParams value inside redux selector as ownProps

I am using "reselect" library in redux to get the data. I wanna get data based on useParams() hook and then to pass it inside mapStatetoProps as ownProps and then to selectionCollection function which is a selector and accepts the value of useParams(). Apparently, it doesn't recognize the useParams() as ownProps and eventually returns undefined. btw, If I pass a string inside selectionCollection('someString), I receive the data but not using useParams() hook. I do receive the useParams value successfully but can't use it inside mapStateToProps which I think is a scope problem. I did define a global variable but didn't work.
import { connect } from 'react-redux';.
import { Outlet, useParams } from 'react-router-dom';
import { selectCollection } from '../../redux/shop/Shop-selectors';
const CollectionPage = ({ collection }) => {
const params = useParams();
const paramsId = params.collectionId;
console.log(collection);
return (
<div className="collection-page">
<p>THis is the collection page for {paramsId}</p>
<Outlet />
</div>
);
};
//-- If I log the 'paramsId', I get undefined since it's outside the function
const mapStateToProps = (state, ownProps) => ({
// here is where I wanna use the paramsId as ownProps
collection: selectCollection(ownProps.paramsId)(state),
});
export default connect(mapStateToProps)(CollectionPage);
I think you can achieve this in different ways but I suggest you two ways to use a params that comes from useParams Hook and mix it with react redux
1- Create another top level component to get the router params in that and pass them to the CollectionPage component, so in the mapStateToProps you can access those params like this:
const TopLevelCollectionPage = () => {
const params = useParams();
<CollectionPage params={params}></CollectionPage>;
};
const CollectionPage = ({ collection }) => {
return (
<div className="collection-page">
<p>THis is the collection page</p>
<Outlet />
</div>
);
};
const mapStateToProps = (state, ownProps) => ({
collection: selectCollection(ownProps.params.collectionId)(state),
});
export default connect(mapStateToProps)(CollectionPage);
2- using react redux hooks to get state inside of your component
const CollectionPage = () => {
const params = useParams();
const collection = useSelector((state) =>
selectCollection(params.collectionId)(state)
);
//or
//const collection = useSelector(selectCollection(params.collectionId));
};

Save value from custom hook to state

I am trying to save a value from a custom hook, which is fetching data for the server, to functional component state with useState, because I later need to change this value and after the change it needs to rerender. So desired behaviour is:
Set State variable to value from custom hook
Render stuff with this state variable
Modify state on button click
Rerender with new state
What I tried is:
Set the inital value of useState to my hook:
const [data, setData] = useState<DataType[] | null>(useLoadData(id).data)
but then data is always empty.
Set the state in a useEffect() hook:
useEffect(()=>{
const d = useLoadData(id).data
setData(d)
}, [id])
But this is showing me the Error warning: Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Doing this:
const [data, setData] = useState<DocumentType[]>([])
const dataFromServer = useLoadData(id).data
useEffect(()=>{
if (dataFromServer){
setData(dataFromServer)
}
}, [dataFromServer])
Leading to: ERROR: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
What would be a proper solution for my use case?
It looks like your custom hook returns a new array every time it is used.
Solution 1: change your hook to return a 'cached' instance of an array.
function useLoadData(id) {
const [data, setData] = useState([]);
useEffect(() => {
loadData(id).then(setData);
}, [id]);
// good
return data;
//bad
//return data.map(...)
//bad
//return data.filter(...)
//etc
}
codesandbox.io link
Solution 2: change your hook to accept setData as a parameter.
function useLoadData(id, setData) {
useEffect(() => {
loadData(id).then(setData);
}, [id]);
}
Here I am telling the hook where to store data so that both custom hook and a button in a component can write to a same place.
codesandbox.io link
Full example:
import React from "react";
import ReactDOM from "react-dom";
import { useState, useEffect } from "react";
// simulates async data loading
function loadData(id) {
return new Promise((resolve) => setTimeout(resolve, 1000, [id, id, id]));
}
// a specialized 'stateless' version of custom hook
function useLoadData(id, setData) {
useEffect(() => {
loadData(id).then(setData);
}, [id]);
}
function App() {
const [data, setData] = useState(null);
useLoadData(123, setData);
return (
<div>
<div>Data: {data == null ? "Loading..." : data.join()}</div>
<div>
<button onClick={() => setData([456, 456, 456])}>Change data</button>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));

How to avoid "React Hook useEffect has a missing dependency" and infinite loops

I have written a react-js component like this:
import Auth from "third-party-auth-handler";
import { AuthContext } from "../providers/AuthProvider";
export default function MyComponent() {
const { setAuth } = useContext(AuthContext);
useEffect(() => {
Auth.isCurrentUserAuthenticated()
.then(user => {
setAuth({isAuthenticated: true, user});
})
.catch(err => console.error(err));
}, []);
};
With the following AuthProvider component:
import React, { useState, createContext } from "react";
const initialState = { isAuthenticated: false, user: null };
const AuthContext = createContext(initialState);
const AuthProvider = (props) => {
const [auth, setAuth] = useState(initialState);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{props.children}
</AuthContext.Provider>
)
};
export { AuthProvider, AuthContext };
Everything works just fine, but I get this warning in the developer's console:
React Hook useEffect has a missing dependency: 'setAuth'. Either include it or remove the dependency array react-hooks/exhaustive-deps
If I add setAuth as a dependency of useEffect, the warning vanishes, but I get useEffect() to be run in an infinite loop, and the app breaks out.
I understand this is probably due to the fact that setAuth is reinstantiated every time the component is mounted.
I also suppose I should probably use useCallback() to avoid the function to be reinstantiated every time, but I really cannot understand how to use useCallback with a function from useContext()
If you want to run useEffect call just once when component is mounted, I think you should keep it as it is, there is nothing wrong in doing it this way. However, if you want to get rid of the warning you should just wrap setAuth in useCallback like you mentioned.
const setAuthCallback = useCallback(setAuth, []);
And then put in in your list of dependencies in useEffect:
useEffect(() => {
Auth.isCurrentUserAuthenticated()
.then(user => {
setAuth({isAuthenticated: true, user});
})
.catch(err => console.error(err));
}, [setAuthCallback]);
If you have control over AuthContext Provider, it's better to wrap your setAuth function inside.
After OP edit:
This is interesting, setAuth is a function from useState which should always be identical, it shouldn't cause infinite loop unless I'm missing something obvious
Edit 2:
Ok I think I know the issue. It seems like calling
setAuth({ isAuthenticated: true, user });
is reinstantianting AuthProvider component which recreates setAuth callback which causes infinite loop.
Repro: https://codesandbox.io/s/heuristic-leftpad-i6tw7?file=/src/App.js:973-1014
In normal circumstances your example should work just fine
This is the default behavior of useContext.
If you are changing the context value via setAuth then the nearest provider being updated with latest context then your component again updated due to this.
To avoid this re-rendering behavior you need to memorize your component.
This is what official doc says
Accepts a context object (the value returned from React.createContext)
and returns the current context value for that context. The current
context value is determined by the value prop of the nearest
<MyContext.Provider> above the calling component in the tree.
When the nearest <MyContext.Provider> above the component updates,
this Hook will trigger a rerender with the latest context value passed
to that MyContext provider. Even if an ancestor uses React.memo or
shouldComponentUpdate, a rerender will still happen starting at the
component itself using useContext.
Like this ?
function Button() {
let appContextValue = useContext(AppContext);
let theme = appContextValue.theme; // Your "selector"
return useMemo(() => {
// The rest of your rendering logic
return <ExpensiveTree className={theme} />;
}, [theme])
}
I did finally solve not using useCallback in MyComponent, but in ContextProvider:
import React, { useState, useCallback, createContext } from "react";
const initialState = { authorized: false, user: null };
const AuthContext = createContext(initialState);
const AuthProvider = (props) => {
const [auth, setAuth] = useState(initialState);
const setAuthPersistent = useCallback(setAuth, [setAuth]);
return (
<AuthContext.Provider value={{ auth, setAuth: setAuthPersistent }}>
{props.children}
</AuthContext.Provider>
)
};
export { AuthProvider, AuthContext };
I am not sure this is the best pattern, because code is not so straightforward and self-explaining, but it works, with no infinite loop nor any warning...

Using Hooks for fetch

I am trying to use hooks to fetch data from an endpoint and I keep getting an error saying
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Uncaught TypeError: Object(...) is not a function
I have tried fetching data without hooks and it works fine.
import React, { useEffect, useState } from 'react';
const fetchData = () => {
const [data, setData] = useState([]);
useEffect(() => {
const getData = async () => {
const response = await fetch('someendpoint');
const jsonResponse = await response.json();
setData(jsonResponse);
}
getData();
}, []);
return (
<div></div>
)
}
export default fetchData;
Rename the component to a valid name: FetchData with a capital letter to pass the linter check (matches inside capitalized function name scope a function with use prefix).
See User-Defined Components Must Be Capitalized.
When an element type starts with a lowercase letter, it refers to a built-in component like <div> or <span> and results in a string 'div' or 'span' passed to React.createElement.

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

Categories

Resources