Prevent making further calls on failing refresh token call - javascript

I have setup axios to use interceptor which does follows,
Make API call to an endpoint
If it fails with status 401, make call to refresh-token
If this refresh call fails with status 401 redirect user to login page.
Happening issues,
It does not stops on failing first api call, it makes all apis calls: /students, /fees/total and /courses.
Result of above, it makes multiple calls to refresh-token image attached below
If refresh token api fails it also not redirecting to /login; url changes but redirection does not happen.
When I refresh the page, only then redirection happens.
I will list files in order,
Dashboard.jsx
It uses custom axios hook useAxios to make three api calls to fetch widget data.
Each api call must be authenticated.
import { Box, Stack } from '#mui/material';
import { useAxios } from '../../api/use-axios';
import { NewWidget } from '../../components/widget/NewWidget';
import ApiConfig from '../../api/api-config';
const Dashboard = () => {
const { response: studentResponse } = useAxios(ApiConfig.STUDENT.GET_STUDENTS);
const { response: courseResponse } = useAxios(ApiConfig.COURSE.GET_COURSES);
const { response: feesResponse } = useAxios(ApiConfig.FEES.GET_TOTAL);
return (
<Box padding={2} width="100%">
<Stack direction={'row'} justifyContent="space-between" gap={2} mb={10}>
<NewWidget type={'student'} counter={studentResponse?.data?.length} />
<NewWidget type={'course'} counter={courseResponse?.data?.length} />
<NewWidget type={'earning'} counter={feesResponse?.data} />
</Stack>
</Box>
);
};
export default Dashboard;
use-axios.js
It is a custom axios hook, which attaches interceptor on the response as well as request.
import { useState, useEffect } from 'react';
import axios from 'axios';
import history from '../utils/history';
import refreshToken from './refresh-token';
const Client = axios.create();
Client.defaults.baseURL = 'http://localhost:3000/api/v1';
const getUser = () => {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
};
const updateLocalStorageAccessToken = (accessToken) => {
const user = getUser();
user.accessToken = accessToken;
localStorage.setItem('user', JSON.stringify(user));
};
Client.interceptors.request.use(
(config) => {
const user = getUser();
config.headers.Authorization = user?.accessToken;
return config;
},
(error) =>
// Do something with request error
Promise.reject(error)
);
Client.interceptors.response.use(
(response) => response,
async (error) => {
// Reject promise if usual error
if (error.response.status !== 401) {
return Promise.reject(error);
}
const user = getUser();
const status = error.response ? error.response.status : null;
const originalRequest = error.config;
if (status === 401) {
refreshToken(user.refreshToken)
.then((res) => {
console.log('response', res);
const { accessToken } = res.data.data;
Client.defaults.headers.common.Authorization = accessToken;
// update local storage
updateLocalStorageAccessToken(accessToken);
return Client(originalRequest);
})
.catch((err) => {
console.log(err);
if (err.response.status === 401) {
localStorage.setItem('user', null);
history.push('/login');
}
return Promise.reject(err);
});
}
return Promise.reject(error);
}
);
export const useAxios = (axiosParams, isAuto = true) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const fetchData = async (params) => {
try {
const result = await Client.request({
...params,
method: params.method || 'GET',
headers: {
accept: 'application/json',
},
});
setResponse(result.data);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isAuto) fetchData(axiosParams);
}, [axiosParams, isAuto]); // execute once only
return { fetch: () => fetchData(axiosParams), response, error, loading };
};
refresh-token.js
import Client from './client';
const refreshToken = (refreshToken) =>
Client({
url: '/auth/refresh-token',
method: 'POST',
data: {
refreshToken,
},
});
export default refreshToken;
client.js
import axios from 'axios';
const Client = axios.create();
Client.defaults.baseURL = 'http://localhost:3000/api/v1';
export default Client;
Errors,
It is supposed to stop on first api call fail, and it should try to refresh token. If refreshing also fails with status 401, it should go back to login page.
So only two API calls: one actual and one to refresh.
Update:
{
...,
"react-router-dom": "^6.2.2",
}
history.js
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
App.js
import { useContext } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Home from './pages/home/Home';
import Login from './pages/login/Login';
import Single from './pages/single/Single';
import './style/dark.scss';
import { userInputs } from './formSource';
import { AuthContext } from './context/AuthContext';
import Student from './pages/student/Student';
import Course from './pages/course/Course';
import AddNewCourse from './pages/course/AddNewCourse';
import AddNewStudent from './pages/student/AddNewStudent';
import Fees from './pages/fees/Fees';
import SingleStudent from './pages/student/SingleStudent';
import ThemeProvider from './theme';
import history from './utils/history';
function App() {
const { currentUser } = useContext(AuthContext);
const RequireAuth = ({ children }) => (currentUser ? children : <Navigate to="/login" />);
return (
<ThemeProvider>
<BrowserRouter history={history}>
<Routes>
<Route path="/">
<Route
index
element={
<RequireAuth>
<Home />
</RequireAuth>
}
/>
<Route path="login" element={<Login />} />
<Route path="students">
<Route
index
element={
<RequireAuth>
<Student />
</RequireAuth>
}
/>
<Route path=":studentId" element={<SingleStudent />} />
<Route
path="new"
element={
<RequireAuth>
<AddNewStudent inputs={userInputs} />
</RequireAuth>
}
/>
</Route>
<Route path="courses">
<Route
index
element={
<RequireAuth>
<Course />
</RequireAuth>
}
/>
<Route
path=":courseId"
element={
<RequireAuth>
<Single />
</RequireAuth>
}
/>
<Route
path="new"
element={
<RequireAuth>
<AddNewCourse inputs={userInputs} title="Add New Course" />
</RequireAuth>
}
/>
</Route>
<Route
path="fees"
element={
<RequireAuth>
<Fees />
</RequireAuth>
}
/>
</Route>
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
export default App;

Related

Everytime I refresh the page, isAuth goes to false. Its messing up my routing

I have multiple flags that route my user to specific pages depending on the flag. I am storing the token in sessionStorage and using it to authenticate. But whenever I remove redirect my page doesnt route correctly. If I remove redirect and refresh auth goes false and then to true and doesnt route correctly. I dont understand what im missing.
Routes.jsx
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import { useAuth } from '../Contexts/AuthContext';
import GridContainer from '../Components/Grid';
import Navbar from '../Components/Navbar';
import Login from '../Pages/Authentication/Login';
import Signup from '../Pages/Authentication/Signup';
import ForgotPassword from '../Pages/Authentication/ForgotPassword';
import UpdatePassword from '../Pages/Authentication/UpdatePassword';
import ConnectedTellUsAboutYourBusiness from '../Pages/AboutYourBusiness/ConnectedTellUsAboutYourBusiness';
import Portal from '../Pages/Portal/Portal';
import { AdvertisersProvider } from '../Contexts/AdvertisersContext';
import { ProfileProvider } from '../Contexts/UserProfileContext';
import { CampaignsProvider } from '../Contexts/CampaignsContext';
import AccountManagment from '../Pages/Authentication/AccountManagement';
const Routes = () => {
const { currentUser } = useAuth();
const { isAuthenticated, emailIsVerified, accountIsComplete } = { ...currentUser };
let routes;
if (isAuthenticated && emailIsVerified && accountIsComplete) {
routes = (
<ProfileProvider>
<Switch>
<Route exact path="/updatePassword">
<UpdatePassword />
</Route>
<Route path="/portal">
<AdvertisersProvider>
<CampaignsProvider>
<Portal />
</CampaignsProvider>
</AdvertisersProvider>
</Route>
<Redirect to="/portal" />
</Switch>
</ProfileProvider>
);
}
if (
(isAuthenticated && !emailIsVerified && !accountIsComplete) ||
(isAuthenticated && !emailIsVerified && accountIsComplete)
) {
routes = (
<ProfileProvider>
<Switch>
<Route exact path="/accountManagement">
<AccountManagment />
</Route>
<Redirect to="/accountManagement" />
</Switch>
</ProfileProvider>
);
}
if (isAuthenticated && emailIsVerified && !accountIsComplete) {
routes = (
<ProfileProvider>
<Switch>
<Route path="/aboutYourBusiness/form">
<ConnectedTellUsAboutYourBusiness />
</Route>
<Redirect to="/aboutYourBusiness/form" />
</Switch>
</ProfileProvider>
);
}
if (!isAuthenticated) {
routes = (
<Switch>
<Route exact path="/aboutYourBusiness/plan">
<ConnectedTellUsAboutYourBusiness />
</Route>
<Route exact path="/login">
<Login />
</Route>
<Route exact path="/signup">
<Signup />
</Route>
<Route exact path="/forgotPassword">
<ForgotPassword />
</Route>
<Redirect to="/login" />
</Switch>
);
}
return (
<Router>
<GridContainer pageHeight="100%">
<Navbar />
</GridContainer>
{routes}
</Router>
);
};
export default Routes;
authContext.jsx
import React, { useContext, useState, useEffect } from 'react';
import propTypes from 'prop-types';
import usePost from '../Hooks/usePost';
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const { post } = usePost('/login');
const defaultCurrentUser = {
isAuthenticated: false,
emailIsVerified: false,
accountIsComplete: false,
accessToken: null,
tokenExpiry: null,
};
const [currentUser, setCurrentUser] = useState({ ...defaultCurrentUser });
function logout() {
sessionStorage.removeItem('token');
sessionStorage.removeItem('tokenExpiry');
return setCurrentUser({
...defaultCurrentUser,
});
}
const login = async (email, password) => {
const loginResponse = await post(
{
email,
password,
},
'POST'
);
const token = loginResponse.data.bearerToken;
const tokenExpiry = loginResponse.data.tokenExpiresOn;
const isAuthenticated = loginResponse.data.authenticated;
const emailIsVerified = true;
const accountIsComplete = true;
if (isAuthenticated && token) {
// set token to sessionStorage
sessionStorage.setItem('token', token);
sessionStorage.setItem('tokenExpiry', tokenExpiry);
setCurrentUser({
isAuthenticated,
emailIsVerified, // true until we implement
accountIsComplete, // true until we implement
accessToken: token,
tokenExpiry,
});
}
return loginResponse;
};
const token = sessionStorage.getItem('token');
const tokenExpiry = sessionStorage.getItem('tokenExpiry');
const currentDate = new Date().toISOString();
useEffect(() => {
if (token) {
if (currentDate < tokenExpiry) {
// token is valid
setCurrentUser({
...currentUser,
isAuthenticated: true,
emailIsVerified: true,
accountIsComplete: true,
accessToken: token,
tokenExpiry,
});
} else {
// token is expired logout user
logout();
}
}
}, []);
console.log(currentUser, 'current user');
const value = {
currentUser,
setCurrentUser,
logout,
login,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
AuthProvider.propTypes = {
children: propTypes.node.isRequired,
};

How can I redirect a user to login when a certain link is clicked using JWT

I am trying to make it so that when a user clicks on a certain Link (Upload Component), it will redirect them to to the login component and am not sure how to accomplish this task in React. I was directed to another answered question, but it hasn't helped me as I am still confused on what I need to do with my own set up. I understand I need to make my own protected route (maybe), but I saw others accessing useContext and I do not have any file with context. I am using Version 6 in react dom. I am also using React router and redux in my project so I know I need to access the state somehow, just not sure how to wrap my mind around it so if anyone could help, I would appreciate it. The user is being stored in local storage with JWT authentication.
App.js:
import React, {useState, useEffect, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'; //Switch was replaced by Routes in react-router-dom v6
import './App.css';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Home from './components/pages/Home';
import Uploads from './components/pages/Uploads';
import Account from './components/pages/Account';
import Login from './components/pages/Login';
import LogOut from './components/Logout';
import Register from './components/pages/Register';
import Profile from './components/pages/Profile';
import EditProfile from './components/pages/EditProfile';
import BoardAdmin from './components/pages/BoardAdmin';
import BoardModerator from './components/pages/BoardModerator';
import BoardUser from './components/pages/BoardUser';
import {logout} from './slices/auth';
import EventBus from './common/EventBus';
const App = () => {
const [showModeratorBoard, setShowModeratorBoard] = useState(false);
const [showAdminBoard, setShowAdminBoard] = useState(false);
const {user: currentUser} = useSelector((state) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
if (currentUser) {
setShowModeratorBoard(currentUser.roles.includes('ROLE_MODERATOR'));
setShowAdminBoard(currentUser.roles.includes('ROLE_ADMIN'));
} else {
setShowModeratorBoard(false);
setShowAdminBoard(false);
}
}, [currentUser]);
return (
<>
<Router>
<Navbar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/create/upload' element={<Uploads />} />
<Route path='/my-account' element={<Account />} />
<Route path='/login' element={<Login />} />
<Route path='/logout' element={<LogOut />} />
<Route path='/register' element={<Register />} />
<Route path='/profile' element={<Profile />} />
<Route path='/profile/edit' element={<EditProfile />} />
<Route path='/user' element={<BoardUser />} />
<Route path='/mod' element={<BoardModerator />} />
<Route path='/admin' element={<BoardAdmin />} />
</Routes>
<Footer />
</Router>
</>
);
};
export default App;
Auth.js:
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit';
import {setMessage} from './messages';
import AuthService from '../services/auth.service';
const user = JSON.parse(localStorage.getItem('user'));
export const register = createAsyncThunk(
'auth/register',
async ({username, email, password}, thunkAPI) => {
try {
const response = await AuthService.register(username, email, password);
thunkAPI.dispatch(setMessage(response.data.message));
return response.data;
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
thunkAPI.dispatch(setMessage(message));
return thunkAPI.rejectWithValue();
}
}
);
export const login = createAsyncThunk(
'auth/login',
async ({username, password}, thunkAPI) => {
try {
const data = await AuthService.login(username, password);
return {user: data};
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
thunkAPI.dispatch(setMessage(message));
return thunkAPI.rejectWithValue();
}
}
);
export const logout = createAsyncThunk('auth/logout', async () => {
await AuthService.logout();
});
const initialState = user
? {isLoggedIn: true, user}
: {isLoggedIn: false, user: null};
const authSlice = createSlice({
name: 'auth',
initialState,
extraReducers: {
[register.fulfilled]: (state, action) => {
state.isLoggedIn = false;
},
[register.rejected]: (state, action) => {
state.isLoggedIn = false;
},
[login.fulfilled]: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload.user;
},
[login.rejected]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
[logout.fulfilled]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
},
});
const {reducer} = authSlice;
export default reducer;
It looks like adding the if statement in my upload file is making it so that a logged in user can access the h1 element that is in the file, but not an unauthenticated user. So its working to an extent, just not redirecting back to login.
Upload.jsx:
import React from 'react';
import {Link} from 'react-router-dom';
import {useSelector} from 'react-redux';
function Uploads() {
const {user: currentUser} = useSelector((state) => state.auth);
if (!currentUser) {
return <Link to='/login' />;
}
return (
<>
<h1 className='page'>UPLOADS</h1>
</>
);
}
export default Uploads;
Rendering the Link doesn't imperatively navigate, use the Navigation component.
Example:
import React, { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
function Uploads() {
const { user: currentUser } = useSelector((state) => state.auth);
if (!currentUser) {
return <Navigate to='/login' replace />;
}
return (
<>
<h1 className='page'>UPLOADS</h1>
</>
);
}
export default Uploads;
Typically you would protect the route instead. If you wanted to do that:
import { Navigate, Outlet } from 'react-router-dom';
import { useSelector } from 'react-redux';
const PrivateRoutes = () => {
const { user: currentUser } = useSelector((state) => state.auth);
return currentUser
? <Outlet />
: <Navigate to="/login" replace />;
}
...
<Routes>
<Route path='/' element={<Home />} />
<Route element={<PrivateRoutes />}>
<Route path='/create/upload' element={<Uploads />} />
... any other protected routes ...
</Route>
... other unprotected routes ...
</Routes>

why this promise then execute twice in codesandbox? [duplicate]

Why does the useCallback hook execute twice? I got a warning advising me to use useCallback so I'm trying to do so. From my understanding useCallback will only execute whenever the object we pass to the array is updated. So my goal is for the websocket to connect once a token is loaded. It 'mostly' works; the socket is connected twice, the callback is running twice.
const setupSocket = () => {
if (token && !socket && authenticated) {
console.log(token, authenticated, socket === null);
const newSocket = io(ENDPOINT, {
query: {
token,
},
});
newSocket.on("disconnect", () => {
setSocket(null);
setTimeout(setupSocket, 3000);
});
newSocket.on("connect", () => {
console.log("success, connected to socket");
});
setSocket(newSocket);
}
};
useCallback(setupSocket(), [token]);
App.js
import React, { useEffect, useState, useCallback } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import "./App.css";
//Pages
import Home from "./pages/home.jsx";
import LoginContainer from "./pages/login/login.container";
import Signup from "./pages/signup";
import PostDetailContainer from "./pages/post-detail/post-detail.container.js";
import jwtDecode from "jwt-decode";
import ProfileContainer from "./pages/profile/profile.container";
import AboutContainer from "./pages/about/about.container";
//Components
import Navbar from "./components/Navbar";
import AuthRoute from "./utils/AuthRoute";
//Redux
import { connect } from "react-redux";
//SocketIO
import io from "socket.io-client";
//Actions
import {
clearUserData,
getUserFromToken,
setAuthentication,
} from "./redux/actions/userActions";
function App({ user: { authenticated }, clearUserData, getUserFromToken }) {
const [token, setToken] = useState(localStorage.IdToken);
const [socket, setSocket] = useState(null);
const ENDPOINT = "http://localhost:3001";
const setupSocket = () => {
if (token && !socket && authenticated) {
const newSocket = io(ENDPOINT, {
query: {
token,
},
});
newSocket.on("disconnect", () => {
setSocket(null);
setTimeout(setupSocket, 3000);
});
newSocket.on("connect", () => {
console.log("success, connected to socket");
});
setSocket(newSocket);
}
};
useCallback(setupSocket(), [token]);
useEffect(() => {
if (token) {
//decode token
const decodedToken = jwtDecode(token);
//token is expired
if (decodedToken.exp * 1000 < Date.now()) {
//remove token from local storage
localStorage.removeItem("IdToken");
setToken(null);
clearUserData();
} else {
if (!authenticated) {
setAuthentication();
getUserFromToken(token);
}
if (authenticated) return;
//get user
}
}
}, [token, authenticated, clearUserData, getUserFromToken]);
return (
<div className="App">
<Router>
<Navbar />
<div className="container">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/login" component={LoginContainer} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/profile" component={ProfileContainer} />
<Route exact path="/about" component={AboutContainer} />
<AuthRoute
exact
path="/message/:username"
component={Message}
authenticated={authenticated}
/>
<AuthRoute
exact
path="/posts/:postId"
component={PostDetailContainer}
authenticated={authenticated}
/>
</Switch>
</div>
</Router>
</div>
);
}
const mapStateToProps = (state) => ({
user: state.user,
});
const mapDispatchToProps = {
clearUserData,
setAuthentication,
getUserFromToken,
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Using React.Strict will make your code run twice. Check out here for more information about this.
Your code
useCallback(setupSocket(), [token]);
Try this
useCallback(setupSocket, [token]);
If that was a typo, then I hope you already got the issue. If not and you need explanation, then here it is.
Assume you have a methods like this
function foo () {
console.log('I am foo');
return 'I am foo'; // Incase 'foo' is not void and returns something
};
Way 1: You are executing 'foo' and storing return value in 'refFoo'.
var refFoo = foo();
Way 2: You are creating reference of 'foo' as "refFoo".
var refFoo = foo;

React Private Route Redirect Condition Is Ignored

Every time I try to navigate to a page after the token expires, I am not redirected to my landing page. Ideally, I would like to be redirected to my landing page after my token expires. My status variable does update when the token expires but gets ignored when rendering the Redirect component. I think my Private Route may be written wrong.
App.js
import { BrowserRouter as Router, Route, Switch, Redirect } from "react-router-dom";
import LandingPage from './pages/LandingPage';
import ViewAllRecipe from './pages/ViewAllRecipe';
import AddRecipe from './pages/AddRecipe';
import EditRecipe from './pages/EditRecipe';
import SingleRecipe from './pages/SingleRecipe';
import axios from 'axios';
var status;
async function validToken(){
var token = localStorage.getItem('token');
try{
const res = await axios.post('/api/auth/verify',
{token: token},
{headers: {'Content-Type': 'application/json'}},
);
if(res.data.status===200){
status = 200;
}
else{
localStorage.removeItem('token');
status= 401;
}
}
catch(err){
localStorage.removeItem('token');
status=500;
}
}
function PrivateRoute({ children, ...rest }) {
validToken();
return (
<Route
{...rest}
render={() =>
status!==200 ? (
<Redirect
to="/landing"
/>
):(
children
)
}
/>
);
}
function App() {
return (
<Router>
<Switch>
<Route exact path="/landing" component={LandingPage}></Route>
<PrivateRoute exact path="/addRecipe" component={AddRecipe}></PrivateRoute>
<PrivateRoute exact path="/viewRecipes" component={ViewAllRecipe}></PrivateRoute>
<PrivateRoute exact path="/recipe/edit/:editRecipe" component={(props) => <EditRecipe {...props}/>}></PrivateRoute>
<PrivateRoute exact path="/recipe/view/:id" component={(props) => <SingleRecipe {...props}/>}></PrivateRoute>
</Switch>
</Router>
);
}
export default App; ```
Try this:
import React, { useState, useEffect } from 'react';
import {
BrowserRouter as Router,
Route,
Switch,
Redirect,
} from 'react-router-dom';
import LandingPage from './pages/LandingPage';
import ViewAllRecipe from './pages/ViewAllRecipe';
import AddRecipe from './pages/AddRecipe';
import EditRecipe from './pages/EditRecipe';
import SingleRecipe from './pages/SingleRecipe';
import axios from 'axios';
async function getStatus() {
var token = localStorage.getItem('token');
try {
const res = await axios.post(
'/api/auth/verify',
{ token: token },
{ headers: { 'Content-Type': 'application/json' } },
);
if (res.data.status === 200) {
return 200;
} else {
localStorage.removeItem('token');
return 401;
}
} catch (err) {
localStorage.removeItem('token');
return 500;
}
}
function PrivateRoute({ children, ...rest }) {
const [status, setStatus] = useState(null);
useEffect(() => {
(async () => setStatus(await getStatus()))();
}, []);
return (
status && (
<Route
{...rest}
render={() => (status !== 200 ? <Redirect to="/landing" /> : children)}
/>
)
);
}
function App() {
return (
<Router>
<Switch>
<Route exact path="/landing" component={LandingPage}></Route>
<PrivateRoute
exact
path="/addRecipe"
component={AddRecipe}></PrivateRoute>
<PrivateRoute
exact
path="/viewRecipes"
component={ViewAllRecipe}></PrivateRoute>
<PrivateRoute
exact
path="/recipe/edit/:editRecipe"
component={props => <EditRecipe {...props} />}></PrivateRoute>
<PrivateRoute
exact
path="/recipe/view/:id"
component={props => <SingleRecipe {...props} />}></PrivateRoute>
</Switch>
</Router>
);
}
export default App;
The useEffect with [] as the second parameter gets called only on the first render and it runs a function that checks the status and sets the state with that status. If the status is null you don't render anything because the async function hasn't run yet so you don't know the status (you could render a loading instead). When the status is the in the state it triggers a re-render with the status in the state

useEffect has missing dependency

For the life of me I can't seem to remove the ESlinting warning about my useEffect having a missing dependency fetchProfile(). When I add fetchProfile to the dependency array, I get an endless loop. I would really appreciate any suggestions that could help me stifle this warning. The code is as follows:
import React, { useEffect, useContext, useReducer } from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import './App.css'
import { MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles/'
import UserContext from './contexts/UserContext'
import jwtDecode from 'jwt-decode'
import axios from 'axios'
// utils
import reducer from './utils/reducer'
import themeFile from './utils/theme'
import AuthRoute from './utils/AuthRoute'
import UnAuthRoute from './utils/UnAuthRoute'
// Components
import NavBar from './components/NavBar'
// Pages
import Home from './pages/Home'
import Login from './pages/Login'
import Profile from './pages/Profile'
import SignUp from './pages/SignUp'
import Admin from './pages/Admin'
import Dashboard from './pages/Dashboard'
import Alumni from './pages/Alumni'
// context
import { ProfileContext } from './contexts/ProfileContext'
const theme = createMuiTheme(themeFile)
// axios.defaults.baseURL = `https://us-central1-jobtracker-4f14f.cloudfunctions.net/api`
const App = () => {
const initialState = useContext(UserContext)
const [user, setUser] = useContext(ProfileContext)
const [state, dispatch] = useReducer(reducer, initialState)
const fetchProfile = async token => {
await axios
.get(`/user`, {
headers: {
Authorization: `${token}`
}
})
.then(res => {
setUser(res.data)
})
.catch(err => console.log({ err }))
}
// keeps userContext authorized if signed in
useEffect(
_ => {
const token = localStorage.FBIdToken
if (token && token !== 'Bearer undefined') {
const decodedToken = jwtDecode(token)
if (decodedToken.exp * 1000 < Date.now()) {
localStorage.removeItem('FBIdToken')
dispatch({ type: 'LOGOUT' })
} else {
dispatch({ type: 'LOGIN' })
state.isAuth && fetchProfile(token)
}
} else {
dispatch({ type: 'LOGOUT' })
localStorage.removeItem('FBIdToken')
}
},
[state.isAuth]
)
return (
<MuiThemeProvider theme={theme}>
<UserContext.Provider value={{ state, dispatch }}>
<div className="App">
<Router>
<NavBar isAuth={state.isAuth} />
<div className="container">
<Switch>
<Route exact path="/" component={Home} />
<UnAuthRoute
path="/signup"
component={SignUp}
isAuth={state.isAuth}
/>
<UnAuthRoute
path="/login"
component={Login}
isAuth={state.isAuth}
/>
<AuthRoute
path="/profile"
component={Profile}
isAuth={state.isAuth}
/>
<AuthRoute
path="/dashboard"
component={Dashboard}
isAuth={state.isAuth}
/>
<Route path="/admin" component={Admin} isAuth={state.isAuth} />
<AuthRoute
path="/users/:id"
component={Alumni}
isAuth={state.isAuth}
/>
</Switch>
</div>
</Router>
</div>
</UserContext.Provider>
</MuiThemeProvider>
)
}
export default App
You can do one of two things:
Move fetchProfile out of the component entirely, and use its result instead of having it call setUser directly.
Memoize fetchProfile so you only create a new one when something it depends on changes (which is...never, because fetchProfile only depends on setUser, which is stable). (You'd do this with useMemo or its close cousin useCallback, probably, though in theory useMemo [and thuse useCallback] is for performance enhancement, not "semantic guarantee.")
For me, #1 is your best bet. Outside your component:
const fetchProfile = token => {
return axios
.get(`/user`, {
headers: {
Authorization: `${token}`
}
})
}
then
useEffect(
_ => {
const token = localStorage.FBIdToken
if (token && token !== 'Bearer undefined') {
const decodedToken = jwtDecode(token)
if (decodedToken.exp * 1000 < Date.now()) {
localStorage.removeItem('FBIdToken')
dispatch({ type: 'LOGOUT' })
} else {
dispatch({ type: 'LOGIN' })
if (state.isAuth) { // ***
fetchProfile(token) // ***
.then(res => setUser(res.data)) // ***
.catch(error => console.error(error)) // ***
} // ***
}
} else {
dispatch({ type: 'LOGOUT' })
localStorage.removeItem('FBIdToken')
}
},
[state.isAuth]
)
Since the action is asynchronous, you might want to cancel/disregard it if the component re-renders in the meantime (it depends on your use case).

Categories

Resources