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

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;

Related

React application shows the homepage briefly, then redirects to the login page if the user is not authenticated [duplicate]

This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed 22 days ago.
I am using React.js and react-router-dom version 6. I have set up a private Outlet and made redirecting functionality works.
The problem is if the user is not authenticated and tries to go to the homepage, React shows the homepage but immediately sends the user to the login page. Everything happens in a second. I don't want to show the homepage.
// private Outlet
import axios from "axios";
import { useEffect, useState } from "react";
import { Navigate, Outlet } from "react-router-dom";
export default function PrivateOutlet() {
const [user, setUser] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const getUser = async () => {
try {
const { data } = await axios.get(
`${process.env.REACT_APP_API_URL}auth/login/success`,
{
withCredentials: true,
}
);
console.log(data.isLoggedIn);
setUser(data);
} catch (error) {
setUser(false);
}
};
useEffect(() => {
getUser();
setIsLoading(false);
}, []);
if (isLoading) return <h1>loading</h1>;
return user ? <Outlet /> : <Navigate to="/login" />;
}
// APP.js
import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import PrivateOutlet from "./components/PrivateOutlet";
function App() {
return (
<BrowserRouter>
<div>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<PrivateOutlet />}>
<Route path="/" element={<Home />} />
</Route>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
In your useEffect, you are calling setIsLoading(false) just after getUser(), so the loading state is changed before the API call completes. You could move setIsLoading(false) inside getUser in a finally block, like so:
import axios from "axios";
import { useEffect, useState } from "react";
import { Navigate, Outlet } from "react-router-dom";
export default function PrivateOutlet() {
// set the user to null at first
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const getUser = async () => {
try {
const { data } = await axios.get(`${process.env.REACT_APP_API_URL}auth/login/success`, {
withCredentials: true,
});
console.log(data.isLoggedIn);
setUser(data);
} catch (error) {
// you can set some error message state here and show it to the user if you want.
console.log(error);
} finally {
setIsLoading(false);
}
};
getUser();
}, []);
if (isLoading) return <h1>loading</h1>;
return user ? <Outlet /> : <Navigate to="/login" />;
}
Notice I also changed the initial value of the user state to null, and changing this value only when the request is a success.

Why is useParams returning undefined even with the correct syntax?

I know this question has been asked a lot, but I read every question and answer and my problem is not gone.
I'm trying to access http://localhost:3000/1 and the useParams should receive 1 to fetch data from an API, but I'm receiving undefined.
In Component.tsx I'm using console.log to receive the useParams but I'm getting undefined. All related files are posted as I'm using BrowserRouter correctly and other related React Router imports.
What is wrong in my code? Why am I not getting the correct params?
Component.tsx:
import { createContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { CharacterSchema, PageSchema } from "../interfaces/characterInterfaces";
import { IGetAllCharacters } from "../interfaces/contextInterfaces";
import { IChildren } from "../interfaces/reactInterfaces";
import api from "../services/api";
export const GetAllCharactersContext = createContext<IGetAllCharacters>({} as IGetAllCharacters);
export const GetAllCharactersInfo = ({ children }: IChildren) => {
const [charactersList, setCharactersList] = useState<CharacterSchema[]>([]);
let navigate = useNavigate();
const { page } = useParams();
console.log(page);
useEffect(() => {
api
.get(`/characters?page=${page}`)
.then((res) => {
setCharactersList(res.data.data);
window.localStorage.setItem("lastPage", String(page));
return res;
})
.catch((err) => console.error(err));
}, [page]);
const nextPage = () => {
navigate(`/2`);
};
const prevPage = () => {
navigate(`/1`);
};
return (
<GetAllCharactersContext.Provider value={{ charactersList, nextPage, prevPage }}>
{children}
</GetAllCharactersContext.Provider>
);
};
AllRoutes.tsx:
const AllRoutes = () => {
return (
<Routes>
<Route path="/" element={<Navigate to="/1" />} />
<Route path="/:page" element={<Home />} />
<Route path="*" element={<Home />} />
</Routes>
);
};
export default AllRoutes;
index.tsx:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import Providers from "./contexts/Providers";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<React.StrictMode>
<BrowserRouter>
<Providers>
<App />
</Providers>
</BrowserRouter>
</React.StrictMode>
);
Having this <Route path="/:page" element={<Home />} /> will let you consume page with useParams only in Home, the component rendered by Route for this specific url.
A way to accomplish what you want is to move the fetching part inside Home. To do so export as part of the context a function that fetches and updates the state:
// ⚠️ import what's needed
export const GetAllCharactersContext = createContext<IGetAllCharacters>({} as IGetAllCharacters);
export const GetAllCharactersInfo = ({ children }: IChildren) => {
const [charactersList, setCharactersList] = useState<CharacterSchema[]>([]);
const navigate = useNavigate();
const fetchCharactersList = useCallback((page) => {
api
.get(`/characters?page=${page}`)
.then((res) => {
setCharactersList(res.data.data);
window.localStorage.setItem("lastPage", String(page));
})
.catch((err) => console.error(err));
}, []);
const nextPage = () => {
navigate(`/2`);
};
const prevPage = () => {
navigate(`/1`);
};
return (
<GetAllCharactersContext.Provider value={{ charactersList, fetchCharactersList, nextPage, prevPage }}>
{children}
</GetAllCharactersContext.Provider>
);
};
And move that useEffect you had in the provider inside Home. Something like this:
// ⚠️ import what's needed
export default function Home() {
const { fetchCharactersList, charactersList } = useContext(GetAllCharactersInfo);
const { page } = useParams();
useEffect(() => {
if (!page) return;
fetchCharactersList(page);
}, [page]);
// render data
return <div></div>;
}
Just tested this myself. useParams needs to be used inside the <Route> where the param is specified. It doesn't just pull them from the url. If you want to keep the structure the same I would suggest exposing a page setter from your context and setting it inside one of the components that can access the hook.
Alternatively you could use useSearchParams which actually takes data from the url search params itself, but you wouldn't be able to have it on the url then.

Prevent making further calls on failing refresh token call

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;

How to toggle class using React Hooks as per localstorage value in App.js?

I want to add class="main" to the div with id="mainDiv" only when the user is logged in and remove it when user logs out. Please someone help me.
Here is code for App.js:
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Login from './components/Login/Login';
import Navbar from './components/Navbar/Navbar';
import Homepage from './components/Homepage/Homepage';
import Contentpage from './components/Contentpage/Contentpage';
import TerminalUI from './components/Terminal/Terminal';
import './App.css';
function App() {
const [status, setStatus] = useState(false); //loggedout
const user = window.localStorage.user;
const manipulateClass = () => {
let x = document.getElementById('mainDiv');
if (localStorage.getItem('user')) {
x.className = 'main';
} else {
x.classList.remove('main');
}
};
useEffect(() => {
console.log('your window local storage :: ', user);
if (user) {
setStatus(true);
manipulateClass();
console.log('Here $tatu$ is ', status);
} else {
manipulateClass();
console.log('i am log out');
}
},[status]);
return (
<React.Fragment>
<Router>
<Navbar />
<div id="mainDiv">
<Switch>
<Route exact path="/" component={Homepage} />
<Route path="/login" component={Login} />
<Route path="/chapters" component={Contentpage} />
<Route path="/terminal" component={TerminalUI} />
</Switch>
</div>
</Router>
</React.Fragment>
);
}
export default App;
The simplest way is to do it like this:
<div className={status && 'main'}>
It's best if you change your mindset to State -> UI.
Do not mutate the DOM yourself, in an imperative manner.
So, instead of
const user = window.localStorage.user;
you could have a piece of state that update every time local storage change:
const [user, setUser] = useState(undefined);
// listen for any local storage changes.
// make sure that `user` has always the latest value
useEffect(() => {
function syncUserState() {
const user = localStorage.getItem('user')
setUser(user)
}
window.addEventListener('storage', syncUserState)
return () => {
window.removeEventListener('storage', syncUserState)
}
}, [])
now that you have this, you can derive the other things that you need from it
// if `user` exist, the the class will we main, otherwise it will empty
const mainClass = user ? `main` : ``
// ...
<div id="mainDiv" className={mainClass}>
From the state, you get the UI.

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