React + Redux dispatching 3 times an action at render of App.js - reactjs

I'm having some problems with this slice of code, I'm using Redux for state management. The behavior I expect is to use the dispatcher (the one with the comment) once the user is logged in (so just once!) but it is called three times at every render of App.js
The action contains an API call, so I got three API calls! I don't get how to make it trigger just once. Any idea of the problem?
import React from 'react';
import {
Route, Switch, Redirect,
} from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import Login from './components/auth/login';
import Dashboard from './components/Dashboard';
import * as actions from './store/actions/indexActions';
export default function App() {
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const jwt = useSelector((state) => state?.auth?.user?.token);
const dispatch = useDispatch();
if (isAuthenticated) {
dispatch(actions.getRole(jwt)); //THIS DISPATCHER IS CALLED 3 TIMES
}
return(
<Provider store={store}>
<BrowserRouter basename="/">
<Switch>
<Route path="/login" exact>
{isAuthenticated ? <Redirect to="/" /> : <Login />}
</Route>
<Route exact path="/">
{isAuthenticated ? <Dashboard /> : <Redirect to="/login" />}
</Route>
</Switch>
</BrowserRouter>
</Provider>
)
}

You currently dispatch it every time the component renders.
If you want it to be called only when the user gets logged in, you need to wrap the call in a useEffect like this:
useEffect(() =>
if (isAuthenticated) {
dispatch(actions.getRole(jwt));
}
}, [isAuthenticated]);
Now, the code inside the useEffect will run only when isAuthenticated changes.

Related

Passing props through route in reactjs

I am new to reactjs and have simple question. I want to pass auth props to Login component and want to update its value from within Login component. How can I pass auth to Login component and update its value from Login component.
Secondly, I have one protected route and only logged-in users can open that component. I added Private method to implement this. Is this right approach of making some components protected ?
import "./App.css";
import {
BrowserRouter,
Routes,
Route,
Switch,
Navigate,
NavLink,
} from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import RequestDemo from "./pages/RequestDemo";
import ProtectedRoute from "./Protected.Route";
const auth = true; //your logic
const Private = ({ Component }) => {
return auth ? <Component /> : <Navigate to="/login" />;
};
function App() {
return (
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/Login" element={<Login />} />
<Route
path="/RequestDemo"
element={<Private Component={RequestDemo} />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
For auth variable I would suggest using as a State or context, below I have provided an example using context, you can do it with State also
import './App.css';
import { BrowserRouter,Routes, Route, Switch, Navigate, NavLink } from 'react-router-dom';
import Home from './pages/Home';
import Login from './pages/Login';
import RequestDemo from './pages/RequestDemo';
import ProtectedRoute from './Protected.Route';
import { useContext } from 'react';
// In the login Component
const LoginComponent = () => {
const authContext = useContext(MyContext);
const handleLogin = () => {
authContext.onAuthChange(true); // this will make the user login that change the value of auth to true
}
return (
<div>Login JSX</div>
)
}
const MyContext = React.createContext(null);
const Private = ({Component}) => {
const authContext = useContext(MyContext);
return authContext.auth ? <Component /> : <Navigate to="/login" />
}
function App() {
const [auth, setAuth] = React.useState(true);
const handleAuthChange = (newAuthState) => {
setAuth(newAuthState);
}
return (
<MyContext.Provider value={{
auth,
onAuthChange: handleAuthChange
}}>
<BrowserRouter>
<Routes>
<Route exact path='/' element={<Home />} />
<Route exact path='/Login' element={<Login />} />
<Route path='/RequestDemo' element={<Private Component={RequestDemo} />} />
</Routes>
</BrowserRouter>
</MyContext.Provider>
);
}
export default App;
1. In your case first you have to change auth from const to let as you want to change auth. Then you have to pass auth and an function which can update auth in the same file(where auth is declared) hence you will pass 2 props auth and function which updates auth.
From your code syntax I'm assuming you are using v6.
import "./App.css";
import {
BrowserRouter,
Routes,
Route,
Switch,
Navigate,
NavLink,
} from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import RequestDemo from "./pages/RequestDemo";
import ProtectedRoute from "./Protected.Route";
let auth = true; // you can mutate let and not constant
const authUpdate = (argument) => {
// update auth in this function and pass this function to the component
console.log(auth + 1);
// if you want some argument or parameter you can do so by declaring argument
console.log(argument); // only if you want to send some argument or else no need
};
const Private = ({ Component }) => {
return auth ? <Component /> : <Navigate to="/login" />;
};
function App() {
return (
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Home />} />
<Route
exact
path="/Login"
element={<Login auth={auth} authUpdate={authUpdate} />} // passed auth and authUpdate as props
/>
<Route
path="/RequestDemo"
element={<Private Component={RequestDemo} />}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
2. There's more than one way for protected route. Yes, you can do it like that as mentioned in code snippet.
PS: I suggest you to use state for auth if you want to trigger re-rendering and want to update auth value at parent and child component level both. If component doesn't re-render's then prop/variable won't be updated
import { useState } from "react";
const [auth,setauth]=useState(true)
const authUpdate=()=>{
// pre-steps
setauth(auth=>!auth)
// you should avoid passing setauth directly as a prop
// down the line in child component you can forget what setauth does and you can set something different
}
If you want to use certain variable/state in all child component you can look into Store or Global State concept. You can use react-redux or useContext which does not need to pass props from parent component to child component (prop drilling)

react - react router v6- privateRoute - infinite loop or not rendering

Apologies for this seemingly repated question but it is really biting me. I am trying to use the new ReactRouter v6 private routes and I think the best practice for me would be to make a call to the server to make sure the token is valid and has not been revoked. I am being badly beatean by an infinite loop entering the private route with the typical error
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
my private route looks like this:
import React, {useCallback, useEffect, useState} from "react"
import {Outlet, Navigate} from "react-router-dom"
import {Auth} from "../../api/authApi"
const PrivateRoute = () => {
const [auth, setAuth] = useState(false)
const checkAuth = useCallback(() => {
let authApi = new Auth()
authApi.isAuth().then(isAuthorized => (
setAuth(isAuthorized)
))
}, [])
useEffect(() => {
checkAuth()
}, [checkAuth])
return auth ? <Outlet /> : <Navigate to="/login" />;
}
export default PrivateRoute
and my routes are:
function App() {
return (
<HashRouter>
<Routes>
<Route exact path="/login" element={<LoginPage />} />
<Route exact path="/register" element={<RegisterPage />} />
<Route path="/" element={
<PrivateRoute><HomePage /></PrivateRoute>
} />
</Routes>
</HashRouter>
)
}
export default App
I changed the PrivateRoute component to this:
import React, {useEffect, useRef} from "react"
import {Outlet, Navigate} from "react-router-dom"
import {Auth} from "../../api/authApi"
const PrivateRoute = () => {
let auth = useRef(false)
useEffect(() => {
const checkAuth = () => {
let authApi = new Auth()
authApi.isAuth().then(isAuthorized => (
auth.current = isAuthorized
))
}
checkAuth()
}, [])
return (auth.current ? <Outlet /> : <Navigate to="/login" />)
}
export default PrivateRoute
but still have the same issue. Is it something I am missing?
I tried it and found two issues:
The initial state auth is false so it will navigate to /login and unmount PrivateRoute at the first time, and then the component PrivateRoute(unmounted) got api response, I think maybe that's why you got warning. (I made an solution at the bottom)
The Route and Outlet components are used in the wrong way.
<Route path="/" element={
<PrivateRoute><HomePage /></PrivateRoute>
} />
should be modified to
<Route element={<PrivateRoute />}>
<Route path="/" element={<Home />} />
</Route>
The Code Sample :

How to send the last value of State as a property

I just learn react and this is the problem I haven't been able to figure out for hours.
I have the App component and I want to pass data about user login to the ProtectedRoute component so I can handle access to the admin page.
In the App there is a function which manages lifting data up from the Login component (based on Firebase user authentication).
The problem is, the userLogged state is set too late so Protected route receives default value of the state.
How can I send the last state to the ProtectedRoute component?
import {useState} from "react"
import './App.css'
import Login from "./components/admin/Login"
import Homepage from "./components/homepage/Homepage"
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute"
import AdminPage from "./components/admin/AdminPage"
function App() {
const [userLogged, setUserLogged] = useState()
const getUser = (data) => {
setUserLogged(data)
console.log("data: " + data)
console.log("state: " + userLogged)
}
return (
<>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/login" element={<Login sendUser={getUser}/>} />
<Route element={<ProtectedRoute isLoggedIn={userLogged} />}>
<Route path="/admin" element={<AdminPage />} />
</Route>
</Routes>
</>
);
}
export default App
This is the ProtectedRoute component, where I tried to add useEffect. It still doesn't work this way.
import { Navigate, Outlet } from "react-router-dom"
import {useState, useEffect} from "react"
const ProtectedRoute = (props) => {
const [isLogged, setIsLogged] = useState("")
useEffect(() => {
setIsLogged(props.isLoggedIn);
console.log(isLogged);
}, [props.isLoggedIn]);
return isLogged ? <Outlet /> : <Navigate to="/" />;
};
export default ProtectedRoute;

Issue updating state to reflect that the user is logged in. Navigating to '/home/ redirects me back to the login page even though state shows as true

I have the initial login configured where a user is able to login and get redirected to home, however, I'm running into an issue updating the state using { useEffect }. The state [isAuthenticated] appears to be updating correctly after viewing the console (see screenshot below), however, when I try to navigate to '/home' the protected route restricts me for some reason.
Console.log
False represents the initial state of [ isAuthenticated ] and True represents the state after { useEffect } runs.
App.js
import "./App.css";
import Login from "./Components/Login";
import Home from "./Components/Home";
import ProtectedRoute from "./Components/ProtectedRoute";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
async function setLoginStatus() {
try {
await axios.post("/checkIfSessionExists").then((response) => {
if (response.status === 200) {
setIsAuthenticated(true);
console.log(isAuthenticated)
} else {
setIsAuthenticated(false);
}
});
} catch (error) {
console.log(error)
}
}
setLoginStatus();
}, [isAuthenticated]);
return (
<div className="App">
<Router>
<Switch>
<Login
exact
path="/"
component={Login}
setIsAuthenticated={setIsAuthenticated}
isAuthenticated={isAuthenticated}
/>
<ProtectedRoute
exact
path="/home"
component={Home}
isAuthenticated={isAuthenticated}
/>
</Switch>
</Router>
</div>
);
}
export default App;
ProtectedRoute.js
import { Route, Redirect } from "react-router-dom";
import axios from "axios";
import { Component, useState } from "react";
const ProtectedRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/" />
)
}
/>
);
export default ProtectedRoute;
Here are the steps of what happens which leads your code to not work the way you expected:
initially isAuthenticated is false
so the user will be routed to the Login component no matter what url they input
after the component was rendered, useEffect runs. you send a request and then set isAuthenticated to true
now the component rerenders, but the user stays in the Login component
The problem is that you need to reroute any users that are authenticated in to a different route (in your case, Home) after they were in the Login component to authenticate
The following example works and will do the following:
if the user is authenticated:
the user can navigate to the /home route
if they go to a different route, it will redirect them to /home
if the user is NOT authenticated:
the user can navigate to the / route
if they go to a different route, it will redirect them to /
I am using <>...</> which is equivalent to <React.Fragment>, which allows you to include adjacent elements in it, which is otherwise not allowed
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
...
return (
<div className="App">
<Router>
<Switch>
{isAuthenticated
?
// the user is autenticated
<>
<Route path="/home" component={Home} />
<Redirect to="/home" />
</>
:
// the user is not autenticated
<>
<Route path="/" component={Login} />
<Redirect to="/" />
</>
}
</Switch>
</Router>
</div>
);
}

React router dom redirect problem. Changes url, does not render component

Problem: When I use history.push(), I can see that browser changes url, but it does not render my component listening on the path. It only renders if I refresh a page.
App.js file:
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from "react-redux";
import PropTypes from "prop-types";
//Components
import LoginForm from "../LoginForm/LoginForm";
import PrivateRoute from "../PrivateRoute/PrivateRoute";
import ServerList from "../ServerList/ServerList";
const App = ({ store }) => {
const isLoggedIn = localStorage.getItem("userToken");
return (
<Router>
<Provider store={store}>
<div className="App">
{isLoggedIn !== true && (
<Route exact path="/login" component={LoginForm} />
)}
<PrivateRoute
isLoggedIn={!!isLoggedIn}
path="/"
component={ServerList}
/>
</div>
</Provider>
</Router>
);
};
App.propTypes = {
store: PropTypes.object.isRequired
};
export default App;
Inside my LoginForm, I am making a request to an API, and after doing my procedures, I use .then() to redirect my user:
.then(() => {
props.history.push("/");
})
What happens: Browser changes url from /login to /, but component listening on / route is not rendered, unless I reload page.
Inside my / component, I use useEffect() hook to make another request to API, which fetches data and prints it inside return(). If I console.log inside useEffect() it happens twice, I assume initial one, and when I store data from an API inside component's state using useState() hook.
EDIT: adding PrivateRoute component as requested:
import React from "react";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ component: Component, isLoggedIn, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLoggedIn === true ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/login" }} />
)
}
/>
);
};
export default PrivateRoute;
What I tried already:
1) Wrapping my default export with withRouter():
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
2) Creating custom history and passing it as prop to Router.
react-router-dom version is ^5.0.1. react-router is the same, 5.0.1
You have at two mistakes in your code.
You are not using <switch> component to wrap routes. So all routes are processed at every render and all components from each <route> are rendered.
You are using local store to exchange information between components. But change in local store is invisible to react, so it does not fire component re-rendering. To correct this you should use local state in App component (by converting it to class or using hooks).
So corrected code will look like
const App = ({ store }) => {
const [userToken, setUserToken] = useState(localStorage.getItem("userToken")); // You can read user token from local store. So on after token is received, user is not asked for login
return (
<Router>
<Provider store={store}>
<div className="App">
<Switch>
{!!userToken !== true && (
<Route exact path="/login"
render={props => <LoginForm {...props} setUserToken={setUserToken} />}
/>
)}
<PrivateRoute
isLoggedIn={!!userToken}
path="/"
component={ServerList}
/>
</Switch>
</div>
</Provider>
</Router>
);
};
And LoginForm should use setUserToken to change user token in App component. It also may store user token in local store so on page refresh user is not asked for login, but stored token is used.
Also be sure not to put anything between <Switch> and </Switch> except <Route>. Otherwise routing will not work.
Here is working sample

Resources