React state not updating inside custom route - javascript

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

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

Private Route Conditional Operator in ReactJS

Seems like a trivial issue.
I have an app, where people can subscribe through "stripe". Would like to give access to a few URLs based on subscription, otherwise taking them back to the "profile" page.
A couple of things are not working.
Firebase query to get subscription is not giving results on the subscription. Somehow the onSnapShot does not fetch anything. Probably since UID is null at the start of rendering of page.
Conditional operator on is not working. Not sure what the problem is on this one.
function PaidRoutes(props) {
const [subscription, setSubscription] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const unsub = auth.onAuthStateChanged((authObject) => {
unsub();
if (authObject) {
setLoading(true);
const uid = auth.currentUser?.uid;
console.log('UID ==>', uid);
let docRef = query(collection(db, 'customers', uid, 'subscription'));
console.log('DOC REF ==> ', docRef);
onSnapshot(docRef, (snap) => {
snap.forEach((doc) => {
console.log('Role of Subscription', doc.data().role);
setSubscription(doc.data().role);
});
});
} else {
console.log('not logged in');
setLoading(false);
}
});
return () => {
unsub();
};
}, []);
return (
<Route
{...props}
render={(props) =>
subscription ? (
<Component {...props} />
) : (
<Redirect to='/profileScreen' />
)
}
/>
);
Thanks Drew for comment. The installed version of react-router-dom is 5.2.0
Issue(s)
Potential issues I see in the PaidRoute code:
The unsubscribe function is called in the auth state handler, this might allow the auth check to work once on an initial auth change, but then will unsubscribe itself and stop working until the component remounts.
The loading state is initially false so any check based on it on the initial render won't do what you want. The loading also isn't used to hold off on the conditional rendering.
The subscription state is initially an empty array which is still truthy, so the conditional logic checking it will likely allow access to the protected route anyway, regardless of any auth status.
Solution
Don't unsubscribe from the auth change listener in the callback.
Start with loading initially true to handle the initial render cycle. Conditionally render null or some loading indicator.
Start with null subscription state.
Code:
function PaidRoutes(props) {
const [subscription, setSubscription] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((authObject) => {
if (authObject) {
setLoading(true);
const uid = auth.currentUser?.uid;
console.log('UID ==>', uid);
const docRef = query(collection(db, 'customers', uid, 'subscription'));
console.log('DOC REF ==> ', docRef);
onSnapshot(docRef, (snap) => {
snap.forEach((doc) => {
console.log('Role of Subscription', doc.data().role);
setSubscription(doc.data().role);
});
});
} else {
console.log('not logged in');
setSubscription(null); // <-- reset auth state
}
setLoading(false); // <-- clear loading state outside if-else
});
return unsubscribe;
}, []);
if (loading) {
return null; // or loading indicator, spinner, etc...
}
return subscription ? (
<Route {...props} />
) : (
<Redirect to='/profileScreen' />
);
}

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

Custom React Hook Causing Memory Leak Error

I had created a custom hook to fetch data for a single item but for some reason its causing this error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at PostPage (http://localhost:3000/static/js/bundle.js:2530:81)
at Routes (http://localhost:3000/static/js/bundle.js:48697:5)
at div
at Router (http://localhost:3000/static/js/bundle.js:48630:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:48110:5)
Here is the code
const useFetchMultiSingle = (mainUrl, secondaryKey, mainUrlPath, secondaryUrlPath, path) => {
const [mainUrlDataSingle, setMainUrlDataSingle] = useState(null);
const [secondaryUrlDataSingle, setSecondaryUrlDataSingle] = useState(null);
const [loadingMultiUrlSingle, setLoadingMultiUrlSingle] = useState(false);
const [errorMultiUrlSingle, setErrorMultiUrlSingle] = useState(null);
useEffect(() => {
const apiOnePromise = axios.get(`${mainUrl}/${mainUrlPath}?secondary_id=${path}`);
const apiTwoPromise = axios.get(`http://localhost:5555/${secondaryUrlPath}?id=${path}`);
setLoadingMultiUrlSingle(true);
Promise.all([apiOnePromise, apiTwoPromise])
.then(values => {
const response01 = values[0].data;
const response02 = values[1].data;
setMainUrlDataSingle(response01);
setSecondaryUrlDataSingle(response02);
})
.catch((err) => {
setErrorMultiUrlSingle(err);
})
.finally(() => {
setLoadingMultiUrlSingle(false);
})
}, [mainUrl, secondaryKey, mainUrlPath, secondaryUrlPath, path]);
const refetch = () => {
const apiOnePromise = axios.get(`${mainUrl}/${mainUrlPath}?secondary_id=${path}`);
const apiTwoPromise = axios.get(`http://localhost:5555/${secondaryUrlPath}?id=${path}`);
setLoadingMultiUrlSingle(true);
Promise.all([apiOnePromise, apiTwoPromise])
.then(values => {
const response01 = values[0].data;
const response02 = values[1].data;
setMainUrlDataSingle(response01);
setSecondaryUrlDataSingle(response02);
})
.catch((err) => {
setErrorMultiUrlSingle(err);
})
.finally(() => {
setLoadingMultiUrlSingle(false);
})
};
return { mainUrlDataSingle, secondaryUrlDataSingle, loadingMultiUrlSingle, errorMultiUrlSingle, refetch };
};
This hook fetches the data from the main url and then fetches the data from the second url based on the first api response data
Here is how i am currently calling the hook. I am just using console.log to output the api results
import React from 'react'
import { useLocation } from 'react-router-dom';
import { useFetchMultiSingle } from '../utilities/useFetchMultiUrl';
const PostPage = () => {
const location = useLocation();
const path = location.pathname.split("/")[2];
const { mainUrlDataSingle, secondaryUrlDataSingle, loadingMultiUrlSingle, errorMultiUrlSingle } = useFetchMultiSingle(process.env.REACT_APP_S_API_URL, process.env.REACT_APP_API_KEY, "posts", "post", path);
if (loadingMultiUrlSingle) return <h1>Loading</h1>
if (mainUrlDataSingle && secondaryUrlDataSingle) console.log(mainUrlDataSingle && secondaryUrlDataSingle);
if (errorMultiUrlSingle) console.log(errorMultiUrlSingle);
return (
<div>
Single Post
</div>
)
}
export default PostPage;
PROBLEM SOLVED
I don't know why but my problem had to do with the skeleton loading components that were causing the problem i had initially used a single skeleton component to load in a parent if statement which then i made it conditional with each of its respective actual component it had nothing to do with the hook itself.
Here is what i initially did
if (isLoading) return <Skeleton/>
return (
<Router>
<Routes>
<Route exact path="/" element={<Component/>}/>
<Routes>
<Router/>
)
And here was my fix
return (
<Router>
<Routes>
<Route exact path="/" element={ isLoading ? <Skeleton/> : <Component/>}/>
<Routes>
<Router/>
)

How to get data from server and use a hook to retrieve it in multiple components?

I have a rather basic use-case: I want to get the user info from the server when the app loads and then using a hook to get the info in different components.
For some reason, I run into an infinite loop and get Error: Maximum update depth exceeded.
getMe gets called recursively until the app crashes.
Is that a correct hook behavior?
This is the relevant part of the hook:
export default function useUser () {
const [user, setUser] = useState(null)
const [authenticating, setAuthenticating] = useState(true)
// ...
const getMe = (jwt) => {
console.log('set user')
axios.get(baseURL + endpoints.currentUser, { headers: {
'X-Access-Token': jwt,
'Content-Type': 'application/json'
}}).then(response => {
setUser({
name: response.data.name,
img: response.data.avatar_url
})
})
}
useEffect(() => {
getMe(jwt)
}, [])
return { user, authenticating }
}
This is the first call
function App () {
const { user, authenticating } = useUser()
const c = useStyles()
return (
authenticating ? (
<div className={c.wrapper}>
<Loader size={60}/>
</div>
) : (
<div className={c.wrapper}>
<div className={c.sidebar}>
<img className={c.lamp} src={ user ? user.img : lamp } />
And I also call need the user in the Router component
const Routes = () => {
const { user } = useUser()
return (
<Router history={history}>
<Switch>
// ...
<Route
path={pages.login}
render={routeProps => (
user ?
<Redirect to={pages.main}/> :
<Login {...routeProps}/>
)}
/>
You shouldn't be requesting the server each time you call the hook since it pretty much unnecessary. You could use Redux or context for this (for this paradigm redux would be better). However, if you insist on this method, it seems you have to wrap your getMe function in a useCallback hook since it must be re-rendering each time the function runs.
Read more on the useCallback hook:
https://reactjs.org/docs/hooks-reference.html#usecallback
You're now making a request via the useEffect in your custom hook - why not let the component do that programatically?
Change getMe to a useCallback function and export it:
export default function useUser () {
const [user, setUser] = useState(null)
const [authenticating, setAuthenticating] = useState(true)
// ...
const getMe = useCallback((jwt) => {
console.log('set user')
axios.get(baseURL + endpoints.currentUser, { headers: {
'X-Access-Token': jwt,
'Content-Type': 'application/json'
}}).then(response => {
setUser({
name: response.data.name,
img: response.data.avatar_url
})
})
}, [])
return { user, authenticating, doFetch: getMe }
}
..and use that function in your components (import doFetch and call it on mount), e. g.:
function App () {
const { user, authenticating, doFetch } = useUser()
const c = useStyles()
useEffect(() => doFetch(), [doFetch])
return (
authenticating ? (
<div className={c.wrapper}>
<Loader size={60}/>
</div>
) : (
<div className={c.wrapper}>
<div className={c.sidebar}>
<img className={c.lamp} src={ user ? user.img : lamp } />
You now avoid the infinite loop and your component takes control of the request logic instead of the reusable hook.

Categories