Re-fetch with useSWR hook when React Router Param changed - javascript

I've found many questions similar to this, but all of them use normal fetching which could be handled by an useEffect hook, but it's not possible with useSWR.
Heres the code:
// simplified Router component:
export default function Router(){
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<MainPage />} />
<Route path="post/:id" element={<Post />} />
</Route>
</Routes>
</BrowserRouter>
}
// Post.tsx
export default function Post(){
const {id} = useParams()
const i = parseInt(id)
const fetcher = (url) => axios.get(url, {id: i}).then(res => res.data)
const {data} = useSWR('http://localhost:80/post', fetcher)
return <div>
{/*display post content */}
</div>
}
I tried redirecting/reloading with useNavigate() but it didn't work.

You could revalidate (mutate) the call if the param changes inside useEffect.
const { data, mutate } = useSWR('http://localhost:80/post', fetcher);
useEffect(() => {
mutate('http://localhost:80/post', fetcher);
}, [param]);
or use the param as a key:
const { data, mutate } = useSWR(['http://localhost:80/post', param], fetcher);
and slightly modify the fetcher function:
const fetcher = ([url]) => axios.get(url, {id: i}).then(res => res.data)

The way to do this is to update the key that useSWR uses (the key is the first argument to useSWR) in which case it'll just refetch automatically whenever the id changes, with the correct loading/error/etc states
const {id} = useParams()
const {data} = useSWR(`http://localhost:80/post/?id=${parseInt(id)}`, (url) => axios.get(url).then(res => res.data))
Any dependency (like the id) that is used to make a request should be part of useSWR's key.
EDIT: Which, as the other answer pointed out, can be an array is well, if desired, instead of just a url
Highly recommend Understanding SWR

Related

React app keeps refreshing when changing i18next language in combination with authenticated routes

I have a react-app with authentication (cookie based) with a login route and a profile route. The profile route is fetching the profile data and puts the data in a form. One of the fields is a language field. I'm using the useEffect hook to watch the language property and use i18n.changeLanguage() to change the language.
For some reason the page keeps refreshing when I add this code. It must be a combination of this code together with the code I'm using the check if the user is authenticated to access the route. When I comment out the protectedRoute function or the useEffect hook it's working but I obviously need both.
A small breakdown of the protectedRoute function and authContext.
The routes are wrapped in an AuthProvider
const App = () => {
return (
<BrowserRouter>
<AuthProvider>
<Router />
</AuthProvider>
</BrowserRouter>
);
};
Inside the AuthProvider I have a user and isAuthenticated state. Both starting with a value of null. On mount a call with or without a cookie is done to the backend to get the user info. If a user object is returned with an id the isAuthenticated state is set to true.
const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
const [user, setUser] = useState(null);
useEffect(() => {
getUserInfo();
}, []);
const getUserInfo = async () => {
try {
const { data } = await authService.me();
setIsAuthenticated(true);
setUser(data);
} catch (error) {
setIsAuthenticated(false);
setUser({});
}
};
const setAuthInfo = (user) => {
setIsAuthenticated(!!(user && user.id));
setUser(user);
};
...
As long as isAuthenticated is null a loading state is rendered instead of a route.
if (authContext.isAuthenticated === null) {
return (
<div>
<span>Loading...</span>
</div>
);
}
const ProtectedRoutes = () => {
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace />
);
};
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<Navigate to="/profile" replace />} />
<Route path="/profile" element={<ProfileOverview />} />
</Route>
</Routes>
);
The profile page can be accessed when the isAuthenticated state is true. Inside the profile page the user profile information is fetched and with a reset set into the form state. This will trigger the useEffect hook watching the formData.language property which will set the language to the user's language. This leads to a continuous refresh and I can't find the reason why or what I'm doing wrong.
const Profile = () => {
const { i18n } = useTranslation();
const { formData, reset, handleSubmit, handleChange } = useForm({});
useEffect(() => {
console.log("Get profile data");
getProfileInfo();
}, []);
useEffect(() => {
i18n.changeLanguage(formData.language);
}, [formData.language]);
const getProfileInfo = async () => {
const { data } = await profileService.getProfileInfo();
reset(data);
};
const submit = (values) => {
console.log("submit");
};
...
Codesandbox demo over here. I have put a console.log inside the useEffect on the profile page so you can see that it keeps refreshing. Login can be done without credentials. All fetches are done with a setTimeout to fake real calls.
Taken from the Codesandbox demo and modified within Profile.js. The idea is to block additional requests to i18n.changeLanguage until the previous request has finished and the language was truly updated.
// Use a state variable to verify we aren't already loading
const [isLoadingLanguage, setLoadingLanguage] = useState(false);
useEffect(() => {
// Verify we aren't loading and are actually changing the language
if (!isLoadingLanguage && formData.language !== language) {
const load = async () => {
// Set the state as loading so we don't perform additional requests
setLoadingLanguage(true);
// Since this method returns a promise, and useEffect does not allow async/await easily,
// make and call an async method so we can await changeLanguage
await i18n.changeLanguage(formData.language);
// Originally, we wanted to set this again to update when the formData language updated, but we'll do in submit
// setLoadingLanguage(false);
};
load();
}
}, [i18n, formData.language]);
I would note, getting this to work in the sandbox required removing the line to i18n.changeLanguage so perpetual renders would not keep submitting requests. Then, adding back the useEffect above loaded the form and provided single submissions.
Edit: To prevent i18n.changeLanguage from rendering the component again we can do a few things.
Remove the setLoadingLanguage(false) from the useEffect so we don't trigger a language change until we submit
Add i18n.changeLanguage to the submit method AND localStorage.setItem("formData", JSON.stringify(formData) to retain the state when we do submit
const submit = (values) => {
localStorage.setItem("formData", JSON.stringify(formData));
i18n.changeLanguage(formData.language);
console.log("submit");
};
Retrieve any localStorage item for "formData" in place of the getProfileInfo
export default {
getProfileInfo() {
console.log({ localStorage });
const profileData =
localStorage.getItem("formData") !== null
? JSON.parse(localStorage.getItem("formData"))
: {
first_name: "John",
last_name: "Doe",
language: "nl"
};
const response = {
data: profileData
};
return new Promise((resolve) => {
setTimeout(() => resolve(response), 100);
});
}
};

React Router Dom, useNavigate not redirecting to the right url path

In older React Router Dom versions I was able to use this code to redirect if user is logged in:
history.push('/login?redirect=shipping')
Now in v6 I am using the useNavigate functions instead of history.push but it's not working as it is taking me to /login/shipping instead of /shipping. Currently this is my navigate code:
let navigateCart = useNavigate()
// some code here
navigateCart('/login?redirect=shipping') // the mistake is inside the parenthesis here but i dont know what it is!
This is my router config:
<BrowserRouter>
<Container>
<Routes>
<Route path="/" element={<HomeScreen />} exact />
<Route path="/login" element={<LoginScreen />} />
<Route path="/profile" element={<ProfileScreen />} />
<Route path="/shipping" element={<ShippingScreen />} />
</Routes>
</Container>
</BrowserRouter>
Login Screen Function :
function LoginScreen() {
let navigateLog = useNavigate()
let location = useLocation()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const dispatch = useDispatch()
const redirect = location.search ? location.search.split('=')[1] : '/'
const userLogin = useSelector(state => state.userLogin)
const { error, loading, userInfo } = userLogin
useEffect(() => {
if (userInfo) {
navigateLog(redirect)
}
}, [navigateLog, userInfo, redirect])
const submitHandler = (e) => {
e.preventDefault()
dispatch(login(email, password))
}
Change this line navigateLog(redirect) inside that useEffect in LoginScreen to this one:
navigateLog(`/${redirect}`);
In your case it's redirecting to /login/shipping instead of /shipping, cause it's like you are calling navigateLog("shipping"), and without / in front, it's used as a relative path. Which means it takes into account your current path, which is in your case happens to be/login.
Issue
The redirect target path is "shipping" instead of "/shipping". In react-router-dom#6 there are relative paths and absolute paths. The difference is simple, absolute paths start with a leading "/" character while relative paths do not. navigate("shipping") will append "shipping" to the end of the current pathname and navigate there.
Solution
Either prepend the leading "/" when navigating:
navigate(`/${navigateLog}`, { replace: true });
Or include it when you initially navigate:
navigateCart('/login?redirect=/shipping');
You'll likely also want to use the useSearchParams hook to access the redirect query parameter instead of relying on it to be at a specific string position.
function LoginScreen() {
const navigateLog = useNavigate();
const [searchParams] = useSearchParams();
...
const redirect = searchParams.get("redirect") || '/';
...
useEffect(() => {
if (userInfo) {
navigateLog(redirect, { redirect: true });
}
}, [navigateLog, userInfo, redirect])
...
Note that in issuing the imperative redirect that I've included an options object to the navigate function that specifies replace: true, this is to issue a REPLACE action instead of a PUSH action.

React Cannot read property 'props' of undefined in React reading id path variable from React Router [duplicate]

This question already has answers here:
How to pass params into link using React router v6?
(5 answers)
Closed 7 months ago.
I am trying to pass in a path variable, userId, as a prop to a React component rendered by React Router:
Profile.js
export const Profile = (props) => {
//array of compatible users fetched for a user.
const [userProfileInformation, setUserProfileInformation] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
getUserProfileInformation().then(() => {
setLoading(false);
});
}, []);
const getUserProfileInformation = async () => {
//Currently hardcoded value until authorisation implemented.
const userId = this.props.match.params.id;
const response = await UserService.getUserProfileInformation(userId)
.then(response => response.json())
.then(data => {
setUserProfileInformation(data);
});
}
if (isLoading) {
return (
<div id="loading">
<h2>Loading...</h2>
</div>
)
}
return (
<div>
<div className="profileCard">
<h2>{userProfileInformation.email}</h2>
</div>
</div>
)
}
The error occurs because of the 'userId = props.match.params.id' code.
App.js:
const root = ReactDOM.createRoot(
document.getElementById("root")
);
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/profile/:userId" element={<Profile />} />
</Routes>
</BrowserRouter>
);
Dashboard.js:
export const Dashboard = () => {
return(
<div>
<h2>Dashboard</h2>
<Button color="link"><Link to={`/profile/1`}>Profile</Link></Button>
</div>
);
}
The above temporarily uses a hardcoded value for the link, 1.
Edit:
Tried with const userId = props.match.params.id; and const userId = this.props.match.params.id and const userId = props.match.params.userId; and const userId = this.props.match.params.userId;
All give same result as before.
Thanks
The version of react-router-dom you are using is v6. In this version (which has lots of breaking changes you need to keep an eye out for), the params object is not accessible since route props are not passed to the element/component.
In order to access the userId, you now need to use the useParams hook.
Import it: import {useParams} from 'react-router-dom'
In place of const userId = props.match.params.userId, use const { userId } = useParams() and then use it like you did before.
If this answer helps, please accept it. Thanks!

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop

How do I prevent the following error:
Too many re-renders. React limits the number of renders to prevent an infinite loop.'
I just changed a class based component to functional component and its not working
My source code
import React, {Fragment, useState} from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Navbar from './Components/Layout/Navbar';
import Users from './Components/users/Users';
import User from './Components/users/User';
import Search from './Components/users/Search';
import Alert from './Components/Layout/Alert';
import About from './Components/pages/About';
import './App.css';
import Axios from 'axios';
const App = () => {
const [users, setUsers] = useState( [] );
const [user, setUser] = useState( {} );
const [repos, setRepos] = useState( [] );
const [loading, setLoading] = useState( false );
const [alert, setAlert] = useState( null );
// Search Github Users
const searchUsers = async text => {
setLoading(true);
const res = await Axios.get(`https://api.github.com/search/users?q=${text}&client_id=${process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`);
setUsers(res.data.items);
setLoading(false);
};
// GEt single github user
const getUser = async username => {
setLoading(true);
const res = await Axios.get(`https://api.github.com/users/${username}?&client_id=${process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`);
setUser(res.data);
setLoading(false);
};
// Get users repos
const getUserRepos = async username => {
setLoading(true);
const res = await Axios.get(`https://api.github.com/users/${username}/repos?per_page=5&sort=created:asc&client_id=${process.env.REACT_APP_GITHUB_CLIENT_ID}&client_secret=${process.env.REACT_APP_GITHUB_CLIENT_SECRET}`);
setRepos(res.data);
setLoading(false);
};
// Clear users from state
const clearUsers = () =>
setUsers([]);
setLoading(false);
// Set ALert
const showAlert = (msg, type) => {
setAlert({msg, type});
setTimeout(()=> setAlert(null),3000);
};
return (
<Router>
<div className="App">
<Navbar />
<div className="container">
<Alert alert={alert} />
<Switch>
<Route exact path='/' render={props => (
<Fragment>
<Search
searchUsers={searchUsers}
clearUsers={clearUsers}
showClear={ users.length>0? true : false }
setAlert={showAlert}
/>
<Users loading={loading} users={users} />
</Fragment>
)} />
<Route exact path = '/about' component={About} />
<Route exact path= '/user/:login' render={props => (
<User
{...props}
getUser={getUser}
getUserRepos={getUserRepos}
user={user}
repos={repos}
loading={loading} />
)} />
</Switch>
</div>
</div>
</Router>
);
}
export default App;
I just change a class based component to functional component and i get these error.
0
How do I prevent the following error:
Too many re-renders. React limits the number of renders to prevent an infinite loop.'
As from the comment,
There was error in the declaration of function clearUsers
const clearUsers = () => setUsers([]);
setLoading(false);
which should be.
const clearUsers = () => {
setUsers([]);
setLoading(false);
}
because of this small typo. The setLoading function was being called on the first render which would then call setLoading triggering react to call render again and in return call setLoading and caused the infinite renders.
I experienced the same error. I found my problem was having brackets at the end of a function(). Yet the function should have been called without brackets. Rookie error.
Before:
onClick={myFunction()}
After:
onClick={myFunction}
If you are using a button also check the caps of your 'onclick'.

React-Router not receiving props if it's the same path, but different param?

I have a path /player/:id/
If I navigate from say /player/12 to /player/13 it will error out, for example: cannot find foo of undefined. However if I navigate from a different path such as /heroes/100 to /player/12 it will load fine.
My suspicion is that when I link from the same route, but different param, the new page is not invoking my useEffect or setState hooks.
I feel like I'm missing something fundamental here, but I can't figure it out. If my suspicion is correct, does anyone know how i can force my component to use my UseEffect and UseState hooks before loading the page when going from the same route, but different params?
The params are correct, it's displaying the correct id in the url bar. If i refresh the page it'll load fine, but if i click the link it'll error out Cannot read property 'hero_id' of undefined
Thank you
Context:
playedAgainst.map(player => <Link key={Math.random()} to=
{"/players/" + props.invertedPlayers[player]}> <span> {player} </span></Link>
Each iteration creates a new Player name, with a Link. When i click on that link it says all "cannot read property hero_id of undefined". However, when i refresh the page it loads fine. I'm 100% sure the link path is correct. I used the exact same code in a different component that leads to the Player component.
(The Math.random is definitely sloppy, i'll remove that!)
<Header />
<Switch>
<Route exact path="/">
<Search players={this.state.players} />
<GameList players={this.state.players} data={this.state.games} />
<Footer />
</Route>
<Route exact path="/players/:id">
<Player players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
<Route exact path="/heroes/:id">
<Heroes players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
</Switch>
</Router>
EDIT -
I added the following hook to my Players component.
useEffect(() => {
console.log('useEffect fired');
}, [])
When i go from 1 id to another (same route, different param) the useEffect hook is not firing.
Seems like i need to get my useEffect to fire when following the same routes with different ids.
Accessing my Params via useParams():
let { id } = useParams();
const [player, setPlayerData] = useState({ data: null, image: null });
useEffect(() => {
const fetchData = async () => {
const response1 = await axios(`/api/players/${id}`);
const response2 = await axios(`/api/saveImage/${id}`)
setPlayerData({ data: response1.data, image: response2.data });
};
fetchData();
}, []);
EDIT 2 -
Added to ID to useEffect, but useEffect is still not rerunning for some reason:
let { id } = useParams();
const [player, setPlayerData] = useState({ data: null, image: null });
useEffect(() => {
const fetchData = async () => {
const response1 = await axios(`/api/players/${id}`);
const response2 = await axios(`/api/saveImage/${id}`)
setPlayerData({ data: response1.data, image: response2.data });
};
fetchData();
}, [id]);
EDIT 3 -
More context, hopefully this helps:
As soon as the component loads it pulls data from the API to load within the component. This works fine. When i click a ling to the same route, but different param, the useEffect hook to pull data doesn't run.
const Player = (props) => {
let { id } = useParams();
const [player, setPlayerData] = useState({ data: null, image: null });
useEffect(() => {
const fetchData = async () => {
const response1 = await axios(`/api/players/${id}`);
const response2 = await axios(`/api/saveImage/${id}`)
setPlayerData({ data: response1.data, image: response2.data });
};
fetchData();
}, [id]);
useEffect(() => {
console.log('count changed');
}, [])
console.log('test')
return (
<div className="historyContainer">
...............
{player.data && player.data.map((game, i) => {
const heroIdOfPlayer = game.players.find(p => p.account_id === +id).hero_id;
const teamOfPro = game.players.find(p => p.hero_id === +heroIdOfPlayer).team;
The part under the '.......' is where Cannot read property 'hero_id' of undefined is coming from, which only happens when clicking through the link.
A few lines further down:
{playedAgainst ?
playedAgainst.map(player => <Link key={Math.random()} to={"/players/" + props.invertedPlayers[player]}><span> {player} </span></Link>)
: ""}
This is how each link is generated. The path is correct. if i follow the path it'll say Cannot read property 'hero_id' of undefined, but when i refresh the page it will load fine.
EDIT - 4
Adding my App component in case this helps:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
games: [],
players: {},
};
}
componentDidMount() {
ReactGA.pageview(window.location.pathname)
this.loadGames();
this.loadPlayers();
this.interval = setInterval(() => {
this.loadGames();
}, 5000);
}
componentWillUnmount() {
this.loadGames();
clearInterval(this.interval);
}
async loadGames() {
const response = await fetch('/api/games');
const myJson = await response.json();
const sortedJson = myJson.sort((a, b) =>
a.spectators > b.spectators ? -1 : 1,
);
this.setState({ games: sortedJson });
}
async loadPlayers() {
const response = await fetch('/api/playersList');
const myJson = await response.json();
this.setState({ players: myJson });
}
render() {
return (
<Router history={history}>
<Header />
<Switch>
<Route exact path="/">
<Search players={this.state.players} />
<GameList players={this.state.players} data={this.state.games} />
<Footer />
</Route>
<Route exact path="/players/:id">
<Player players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
<Route exact path="/heroes/:id">
<Heroes players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
</Switch>
</Router>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
In your useEffect hook, pass the prop that changes as a dependency i.e.
useEffect(..., [id])
This will ensure useEffect runs everytime id changes, the problem at the minute is you pass [] which means useEffect will only run once, when the component is first mounted.

Categories