Why is useEffect() not working in my code - javascript

const [open, setOpen] = useState(false);
useEffect(() => {
if(!token){
return <Navigate to="/auth/login"/>
}
getMe(token)
}, [token, getMe])
return (
<RootStyle>
<DashboardNavbar onOpenSidebar={() => setOpen(true)} />
<DashboardSidebar isOpenSidebar={open} onCloseSidebar={() => setOpen(false)} />
<MainStyle>
<Outlet />
</MainStyle>
</RootStyle>
);
}
const mapStateToProps = ({ auth }) => ({
token: auth.token ? auth.token.token : null
})
const mapDispatchToProps = (dispatch) => ({
getMe: (token) => dispatch(fetchMe(token)),
})
The code above is trying to check if there is token, if not user is redirected to login page else a function that calls the user object from database runs. The token is a destructured prop.
But the problem is the entire block seems not to be noticed by browser for some reason. When i move the getMe() function outside useEffect is works. debugger isn't even noticed! what could be the problem here?
Running the code below without useEffect will work. But there might be lots of re-renders which might lead to a bug.
if(!token){
return <Navigate to="/auth/login"/>
}
getMe(token)
That is why i am wraping all of the above code to useEffect() in such a way that only when the component mounts and if token changes the function getMe() should run.
useEffect is not working at all. even if i just put a console statement

Are you sure that your token has a value and that it really changes to trigger the useEffect? Do a simple console.log of the token at the top of the useEffect.
What is the result of the useEffect right now? Does it navigate to '/auth/login' as if the token was empty? Also, you could post more of your code to let us see the destructuring of your props.

You don't need a return for useEffect, just <Navigate to="/auth/login"/> should work

You can do a empty return in your useEffect. And when returning the components to render check if the token is set, if not return the Navigate component
useEffect(() => {
if (!token) return;
getMe(token);
// or even
// if (token) getMe(token);
}, [token, getMe]);
return !token ? (
<Navigate to="/auth/login" />
) : (
<RootStyle>
<DashboardNavbar onOpenSidebar={() => setOpen(true)} />
<DashboardSidebar
isOpenSidebar={open}
onCloseSidebar={() => setOpen(false)}
/>
<MainStyle>
<Outlet />
</MainStyle>
</RootStyle>
);

Related

React state not updating inside custom route

I am having an issue where my react state is not updating.
I am trying to make a role-based protected route, following this tutorial style https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5, using the following component:
const MasterRoute = ({ children }) => {
const [role, setRole] = useState('');
const [isLoading, setIsLoading] = useState(false);
const checkAuth = async () => {
setIsLoading(true);
let response = await getRole();
setRole(response.role);
setIsLoading(false);
}
useEffect(() => {
checkAuth();
}, [])
useEffect(() => {
console.log(role);
}, [role])
return role === 'ADMIN' ? children : <Navigate to="/" />;
}
Logging the role in the useEffect function displays an empty result in the console.
Logging the variable response directly after the await function displays the correct response retrieved from the server.
I've tried to console log the role directly after the checkAuth() function in useEffect(), but also obtained an empty line in the console.
What could be the problem?
This component is used as the following in App.js file:
<Route
element={
<MasterRoute>
<Dashboard child={<Admin />}></Dashboard>
</MasterRoute>
}
path={'/roles'}
></Route>
Issue
It seems the general problem is that the initial role state is '', and since '' === 'ADMIN' evaluates false the <Navigate to="/" /> is rendered and the route changes. In other words, the route changed and MasterRoute likely isn't being rendered when the checkAuth call completes.
Solution
You could use that isLoading state to conditionally render null or some loading indicator while the auth/role status us checked. You'll want MasterRoute to mount with isLoading initially true so no routing/navigation action is taken on the initial render cycle.
Example:
const MasterRoute = ({ children }) => {
const [role, setRole] = useState('');
const [isLoading, setIsLoading] = useState(true); // <-- initially true
const checkAuth = async () => {
setIsLoading(true);
let response = await getRole();
setRole(response.role);
setIsLoading(false);
}
useEffect(() => {
checkAuth();
}, []);
useEffect(() => {
console.log(role);
}, [role]);
if (isLoading) {
return null; // or loading indicator/spinner/etc
}
return role === 'ADMIN' ? children : <Navigate to="/" replace />;
}

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

Return from a funtion only when API returns a success response from backend in React

I need a help on regarding private routing in react-router-6.Actually, I want to return a component only when the backend API returns success response.
please see the following code snipet,
export default function PrivateOutlet() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser())
.then((res) => {
localStorage.setItem('id', res.id);
})
.catch(error => console.log(error) );
}, [);
return (localStorage.getItem('id') !== "undefined") ? <Outlet /> : <Navigate to="/login" />
};
Here, the problem is , before returning success response from the getCurrentUser() API this function PrivateOutlet returns value.
can you please help on this?
The localStorage seems completely extraneous/superfluous if what you really want is to use the getCurrentUser action to get an id. Use a local state id that doesn't match the authenticated or unauthenticated id value. Wait until the id state is populated to render the Outlet or redirect.
Example:
export default function PrivateOutlet() {
const [id, setId] = React.useState();
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser())
.then((res) => {
setId(res.id); // valid id or null
})
.catch(error => {
console.log(error);
setId(null);
);
}, []);
if (id === undefined) return null; // or loading indicator/spinner/etc
return (id)
? <Outlet />
: <Navigate to="/login" replace />
};
If wanting to continue using localStorage, then use a local "loading" state and conditionally render null or some loading indicator until the current user id is fetched. Also, I'm not sure if it was a typo, but you very likely meant to compare the value from localStorage against undefined and not the string literal "undefined". This still works because the loading state update triggers a rerender and the component can read the id value just set in localStorage.
Example:
export default function PrivateOutlet() {
const [isLoaded, setIsLoaded] = React.useState(false);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser())
.then((res) => {
localStorage.setItem('id', res.id);
})
.catch(error => {
console.log(error);
localStorage.removeItem('id');
)
.finally(() => setIsLoaded(true));
}, []);
if (!isLoaded) return null; // or loading indicator/spinner/etc
return (localStorage.getItem('id') !== undefined)
? <Outlet />
: <Navigate to="/login" replace />
};

Right way to use REST API in react?

I am using a free account on weatherstack.com to get weather info on a specified city. The relevant component from the App is the following
const WeatherInfo = (props) => {
const [weather, setWeather] = useState()
const url = 'http://api.weatherstack.com/current?access_key='
+ process.env.REACT_APP_API_KEY + '&query=' + props.city
axios.get(url)
.then(response => {setWeather(response.data)})
return (
<div>
<p>Weather in {props.city}</p>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</div>
)
}
This fails with TypeError: weather.current is undefined because I believe the axios.get is asyncronously called so the return happens before the setWeather() call inside the .then(). So I replaced the return statement with the following:
if (weather === undefined) {
return (
<div>Loading...</div>
)
}
else {
return (
<div>
<p>Weather in {props.city}</p>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</div>
)
}
This succeeds briefly and then fails with the same error as previous. I guess I must have some fundamental misunderstanding of the correct way to wait for a response before rendering.
Question
What is the correct way to wait for REST call when using react?
Whenever you set state, your functional component will re-render, causing the body of the function to execute again. This means that when your axios call does eventually complete, doing setWeather(response.data) will cause your functional component body to run again, making you do another get request.
Since you only want to run your axios get request once, you can put your axios get request inside of the useEffect() hook. This will allow you to only run your get request when the component initially mounts:
import React, {useState, useEffect} from "react";
...
const WeatherInfo = (props) => {
const [weather, setWeather] = useState();
useEffect(() => {
const url = 'http://api.weatherstack.com/current?access_key=' + process.env.REACT_APP_API_KEY + '&query=' + props.city
const cancelTokenSource = axios.CancelToken.source();
axios.get(url, {cancelToken: cancelTokenSource.token})
.then(response => {setWeather(response.data)});
return () => cancelTokenSource.cancel();
}, [props.city, setWeather]);
return weather ? (
<div>
<p>Weather in {props.city}</p>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</div>
) : <div>Loading...</div>;
}
The above useEffect() callback will only run when the things in the dependency array (second argument of useEffect) change. Since setWeather is a function and doesn't change, and props.city only changed when the prop is changed, the callback is only executed when your component initially mounts (provided the city prop isn't changing). The useEffect() hook also allows you to return a "clean-up" function, which, in this case, will get called when your component unmounts. Here I have used Axios's CancelToken to generate a source token so that you can cancel any outgoing requests if your component unmounts during the request.
First of all wrap axios in useEffect:
useEffect(() => {
axios.get(url)
.then(response => {setWeather(response.data)})
});
then wrap return part with weather in condition:
return (
<div>
<p>Weather in {props.city}</p>
{
weather && (
<>
<p><b>Temperature:</b> {weather.current.temperature} celsius</p>
<img src={weather.current.weather_icons[0]} alt="Country flag" width="150"></img>
<p><b>Wind:</b> {weather.current.wind_speed} mph direction {weather.current.wind_dir}</p>
</>
)
}
</div>
)
Your code will continuously make the axios call on every rerender. You need to put that call into a function and call if once on Component load. Also your render function should check to see if the state is set. Here is a basic example and a Sandbox:
https://codesandbox.io/s/zen-glitter-1ci2j?file=/src/App.js
import React from "react";
import "./styles.css";
const axioscall = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ city: "Scottsdale", tempt: 75 });
}, 1000);
});
};
export default (props) => {
const [weather, setWeather] = React.useState(null);
React.useEffect(() => {
axioscall()
.then((result) => setWeather(result));
}, []);
return (
<div>
{weather ? (
<>
<p>Weather in {weather.city}</p>
<p>
<b>Temperature:</b> {weather.temp} F
</p>
</>
) : (
<div>Loading...</div>
)}
</div>
);
};

Private route using React, React Hooks

Trying to make authenticated route using react hooks, with the snippet below, it appears when i refreshed the page while the token is available it doesn't get picked up in the useEffect at the first render. What i'm doing wrong here
const AuthRoute = ({ component: Component, ...rest }) => {
const context = useContext(AuthStateContext)
const token = window.localStorage.getItem('token')
useEffect(() => {
if (token) {
context.setLogin()
}
console.log("TOKEN ->", token)
}, [])
return (
<Route
{...rest}
render={props => (
context.isAuthenticated ?
<Component {...props} />
: <Redirect to={{
pathname: "/",
state: { from: props.location }
}} />
)}
/>
)
}
I assume context.setLogin() will set context.isAuthenticated to true. If that's the case, then it explains.
Callback function of useEffect(callback) is always called asynchronously after each render. As of first render, setLogin() is only called after value of isAuthenticated is read, which at the time of accessing should be false.
So render logic goes to the second branch <Redirect /> which immediately takes user to other place.
To achieve this, you can defer the rendering util state of isAuthenticated is decided.
(Here, I assume you want to check and set isAuthenticated once early up in the rendering tree, then broadcast the isAuthenticated through AuthStateContext object for other parts of the app to notice.)
I suggest this pattern:
const AuthRoute = ({ component: Component, ...rest }) => {
const context = useContext(AuthStateContext)
const [authChecked, setAuthChecked] = useState(false)
useEffect(() => {
const token = window.localStorage.getItem('token')
if (token) context.setLogin()
setAuthChecked(true)
console.log("TOKEN ->", token)
}, [])
if (!authChecked) return null // <-- the trick!
return (
<Route
{...rest}
render={props => (
context.isAuthenticated ?
<Component {...props} />
: <Redirect to={{
pathname: "/",
state: { from: props.location }
}} />
)}
/>
)
}
Side note, if context.setLogin() only asynchronously sets the state of isAuthenticated, then you need to add in a callback or promise pattern like
context.setLogin(() => setAuthChecked(true))
// or
context.setLogin().then(() => setAuthChecked(true))

Categories