I need to render the component after the useEffect checks if an user has the required role, but it always redirect me because it execute first the render function and then the useEffect
Here's my code:
import { Route, Redirect } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { checkIfUserHasRequiredRole } from '../../utility/utility';
import PathConst from '../../utils/PathConst';
const AuthRoute = ({
Component, path, exact = false, isAuthenticated, requiredRoles,
}) => {
const [userHasRequiredRole, setUserHasRequiredRole] = useState(false);
const roles = useSelector((state) => state.role.roles);
const isAuthed = isAuthenticated;
useEffect(() => {
if (roles) {
const userRole = checkIfUserHasRequiredRole(requiredRoles, roles);
setUserHasRequiredRole(userRole);
}
}, [roles]);
return (
<Route
exact={exact}
path={path}
render={() => (
isAuthed && userHasRequiredRole ? (<Component />)
: (
<Redirect
to={PathConst.toLoginPage}
/>
))}
/>
);
};
export default AuthRoute;
The function 'checkIfUserHasRequiredRole' returns true but 'useHasRequiredRole' is still false when it trigger the if statement. I also tried using the same useEffect function with no dependencies.
How can I manage it?
You could do with your setup, but that requires additional render so that userHasRequiredRole is updated and take effect.
Since you can figure out what you need based on roles, you could,
import { useHistory } from "react-router-dom";
const history = useHistory();
useEffect(() => {
if (roles) {
const userRole = checkIfUserHasRequiredRole(requiredRoles, roles);
if (this role is not good) {
history.push(PathConst.toLoginPage)
}
}
}, [roles])
Related
How can I make the admin go to login first before going to other components? So I've been getting an error about
TypeError: Cannot read properties of null (reading 'isAdmin')
I know where this came from, it's from my
const admin = useSelector((state) => state.user.currentUser.isAdmin)
I was thinking that forcing the admin to redirect to other page, but when I try to use ternary operator, the program itself already calls the useSelector.
Is there any way I can approach this problem?
import { useSelector } from 'react-redux'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import './App.css'
// import Sidebar from './components/sidemenu/Sidebar'
import Home from './pages/home/Home'
import Login from './pages/login/Login'
// import NewProduct from './pages/newProduct/NewProduct'
// import NewUser from './pages/newUser/NewUser'
// import ProductList from './pages/product/ProductList'
// import Product from './pages/productItem/Product'
// import User from './pages/user/User'
// import UserList from './pages/userList/UserList'
function App() {
const admin = useSelector((state) => state.user.currentUser.isAdmin)
// const admin = false
return (
<Router>
<Switch>
<Route path="/">{admin ? <Home /> : <Login />}</Route>
</Switch>
</Router>
)
}
export default App
You should make your code safe.
For modern js you could use the optional chaining operator
const admin = useSelector((state) => state.user?.currentUser?.isAdmin)
A more traditional approach to do the same thing
const admin = useSelector((state) => state.user && state.user.currentUser && state.user.currentUser.isAdmin)
You can first check if currentUser exists.
If currentUser = null that means user is not authenticated and you can redirect them to Login
function App() {
const isLoggedIn = Boolean(useSelector((state) => state.user.currentUser))
const admin = isLoggedIn ? useSelector((state) => state.user.currentUser.isAdmin) : false
return (
<Router>
<Switch>
<Route path="/">{admin ? <Home /> : <Login />}</Route>
</Switch>
</Router>
)
}
you can use react hooks to handle authentication handling in all components.
for example you have context provider like this :
import React, { createContext, useContext, useEffect, useState } from 'react';
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [{ user, isLogin, isAdmin }, updateStatus] = useState({ isLogin: false, isAdmin: false });
// some functions to updates states
useEffect(() => {
if (isLogin && /* user is not in login page */) {
// redirect to login page
}
}, [isLogin])
if (!isLogin || !isAdmin)
return <></>
}
return (
<AuthContext.Provider value={{ user, isAdmin }}>{children}
</AuthContext.Provider>
}
(this context is very basic)
and you can wrap this provider in app.js
You can change
const admin = useSelector((state) => state.user.currentUser.isAdmin)
to
const admin = useSelector((state) => state.user?.currentUser?.isAdmin)
to fix this issue. It definitely works.
I have a question about react router. I'm using different paths for the same component. The path is different since a changeable route parameter is given inside. So, does the change of the parameter causes the component to remount/unmount? If yes, does the remount of the component cause an initial render again? (I'm using functional components. I'm building a chatapp. There will be different chatrooms and the user can navigate to them from the home component.)
import './Home.css'
import {BrowserRouter as Router, Route, Switch, useHistory} from 'react-router-dom';
import { Link } from 'react-router-dom/cjs/react-router-dom.min';
import ChatRoom from './ChatRoom';
import { useEffect, useState } from 'react';
const Home = ({db, auth, user}) => {
const history = useHistory();
const [user1, setUser1] = useState('Loading...');
const logOut = () => {
auth.signOut().then(() => {
history.replace("/");
});
}
useEffect(() => {
db.collection('users').get().then(snapshot => {
snapshot.docs.forEach(doc => {
if (doc.id === user.uid) {
setUser1(doc.data().username);
}
})
}).catch(error => {
console.log(error);
})
}, [])
return (
<Router>
<div>
<Link to="/rooms/ChatA">ChatA</Link>
<Link to="/rooms/ChatB">ChatB</Link>
<Switch>
<Route path="/rooms/:ChatID">
<ChatRoom db={db} auth={auth}/>
</Route>
<Switch>
</div>
</Router>
)
}
export default Home;
This is the component where the user will navigate to particular chatroom from.
import React, { useState, useEffect, useRef } from 'react'
import { useParams } from 'react-router-dom';
import {firebase} from '../../Firebase/firebase';
const ChatRoom = ({ db, auth }) => {
const { chatID } = useParams();
const [user, setUser] = useState(null);
const [docs, setDocs] = useState(null)
const [msg, setMsg] = useState('');
const dummy = useRef();
const sendMsg = (e) => {
e.preventDefault();
db.collection(chatID).add({
username: user,
text: msg,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
}).then(() => {
dummy.current.scrollIntoView({ behavior: 'smooth' });
})
setMsg('');
}
const deleteMsg = (id) => {
db.collection(chatID).doc(id).delete();
}
useEffect(() => {
auth.onAuthStateChanged(user => {
if (user) {
db.collection('users').get().then(snapshot => {
snapshot.docs.forEach(doc => {
if (doc.id === user.uid) {
setUser(doc.data().username);
}
})
})
}
})
db.collection(chatID).orderBy('createdAt').onSnapshot(snapshot)
=> {
setDocs(snapshot.docs);}
}, [])
return (
<div>
//some contents
</div>
);
}
export default ChatRoom;
This is the component which will be mounted according to which chatroom the user will navigate to.
I'll repeat my question: Will the change of the route parameter value cause a remount of the ChatRoom component and cause another initial render respectively?
For a small project of mine, I'm trying to implement the most basic authentication as possible, using the React context API without Redux.
import { createContext, useContext, useState } from 'react'
export const AuthContext = createContext()
export const useAuth = () => {
const context = useContext(AuthContext)
if(context === null) throw new Error('Nope')
return context
}
export const AuthProvider = (props) => {
const [authenticated, setAuthenticated] = useState(false)
const login = () => {
setAuthenticated(true)
localStorage.setItem(storageKey, true)
}
const logout = () => {
setAuthenticated(false)
localStorage.setItem(storageKey, false)
}
return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}
export default AuthContext
I created a context, and wrapped my <App /> component in it like so; <AuthProvider></App></AuthProvider>. Because I want to keep the authenticated state, I used the browser's local storage, for storing a simple boolean value.
import PrivateRoute from './PrivateRoute'
import { useAuth } from './context/AuthContext'
import { AuthPage } from './pages'
import {
BrowserRouter,
Switch,
Route,
} from 'react-router-dom'
import { useEffect } from 'react'
const App = () => {
const { login, authenticated } = useAuth()
useEffect(() => {
const token = localStorage.getItem('user')
if(token && token !== false) { login() }
})
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/auth" component={AuthPage} />
<Route exact path='/'>
Dashboard
</Route>
</Switch>
</BrowserRouter>
)
}
export default App
Then, in my <App /> component, I tried invoking the login callback, given from the AuthProvider, which made me assume that made me login during page refreshes. When I try to access the authenticated variable in the current component, it does work. It shows that I am authenticated.
However when I try to set up a PrivateRoute, which only authenticated users can go to like this:
import {
Route,
Redirect
} from 'react-router-dom'
import { useAuth } from './context/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { authenticated } = useAuth()
if(authenticated) {
return <Route {...rest} render={(props) => <Component {...props} />} />
}
return <Redirect to={{ pathname: '/login' }} />
}
export default PrivateRoute
It does not work. It just redirects me to the login page. How does this come? The PrivateRoute component is getting rendered from the <App /> component. Also, what would be the solution to this problem?
Rather than running a useEffect on every rerender to check if user should be logged in, you should better initialize your authenticated state with the values from your localStorage:
const storageKey = 'user'
const initialState = JSON.parse(localStorage.getItem(storageKey)) ?? false
export const AuthProvider = (props) => {
const [authenticated, setAuthenticated] = useState(initialState)
const login = () => {
setAuthenticated(true)
localStorage.setItem(storageKey, true)
}
const logout = () => {
setAuthenticated(false)
localStorage.setItem(storageKey, false)
}
return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}
Thanks to Yousaf for the explaination in the comments and the HospitalRun project on GitHub, I made a loading state in the <App /> component.
import { useAuth } from './context/AuthContext'
import { useEffect, useState } from 'react'
import Router from './Router'
const App = () => {
const [ loading, setLoading ] = useState(true)
const { login } = useAuth()
const token = localStorage.getItem('user')
useEffect(() => {
if(token && token !== false) {
login()
}
setLoading(false)
}, [loading, token, login])
if (loading) return null
return <Router />
}
export default App
Here I only let anything render, after the login function was called.
if (loading) return null
If this could be done any better, feedback would still be appriciated!
I just started playing with context today and this is my usercontext
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserContext = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
export default UserContext;
this is how im using it in any component that needs the currently logged in user
const user = useContext(userContext)
my question is whenever the user logs in or logs out I have to refresh the page in order to see the change in the browser. is there any way that I can do this where there does not need to be a reload. also any general tips on react context are appreciated
(EDIT)
this is how Im using the UserContext if it helps at all
const App = () => {
return (
<BrowserRouter>
<UserContext>
<Switch>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
component={route.component}
/>
))}
</Switch>
</UserContext>
</BrowserRouter>
);
};
Where is your context consumer?
The way it is set up, any userContext.Consumer which has a UserContext as its ancestor will re render when the associated user is loaded, without the page needing to be reloaded.
To make it clearer you should rename your UserContext component to UserProvider and create a corresponding UserConsumer component:
import { createContext, useEffect, useState } from "react";
import axios from "axios";
export const userContext = createContext({});
const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
useEffect(() => {
axios.get("/api/auth/user", { withCredentials: true }).then((res) => {
console.log(res);
// setting the state here will trigger a re render of this component
setUser(res.data.user);
});
}, []);
return <userContext.Provider value={user}>{children}</userContext.Provider>;
};
const UserConsumer = ({ children }) => {
return (
<userContext.Consumer>
{context => {
if (context === undefined) {
throw new Error('UserConsumer must be used within a UserProvider ')
}
// children is assumed to be a function, it must be used
// this way: context => render something with context (user)
return children(context)
}}
</userContext.Consumer>
);
};
export { UserProvider, UserConsumer };
Usage example:
import { UserConsumer } from 'the-file-containing-the-code-above';
export const SomeUiNeedingUserInfo = props => (
<UserConsumer>
{user => (
<ul>
<li>{user.firstName}</>
<li>{user.lastName}</>
</ul>
)}
</UserConsumer>
)
To be fair, you could also register to the context yourself, this way for a functional component:
const AnotherConsumer = props => {
const user = useContext(userContext);
return (....);
}
And this way for a class component:
class AnotherConsumer extends React.Component {
static contextType = userContext;
render() {
const user = this.context;
return (.....);
}
}
The benefit of the UserConsumer is reuasability without having to worry if you're in a functional or class component: it will used the same way.
Either way you have to "tell" react which component registers (should listen to) the userContext to have it refreshed on context change.
That's the whole point of context: allow for a small portion of the render tree to be affected and avoid prop drilling.
Starting with GamePage, it provides 2 routes which renders the components GameList and GameDetailPage. Both work fine at first but When i refresh the page for Gamelist component, it still rerenders the page but when i refresh the page for GameDetailPage, i get the error TypeError: Cannot read property 'Location' of undefined. I do not understand why it is unable to fetch data from state whenever i refresh.
gamepage.jsx
import React from "react";
import GamesList from "../../components/games-list/game-list.component";
import { Route } from "react-router-dom";
import GameDetailPage from "../gamedetailpage/gamedetailpage.component";
import {firestore,convertCollectionsSnapshotToMap} from '../../firebase/firebase.utils'
import {connect} from 'react-redux'
import {updateFootballGames} from '../../redux/games/games.actions'
class GamePage extends React.Component {
unsubscribeFromSnapshot=null;
//whenever the component mounts the state will be updated with the football games.
componentDidMount(){
const {updateFootballGames}=this.props
const gameRef=firestore.collection('footballgames')
gameRef.onSnapshot(async snapshot=>{
const collectionsMap=convertCollectionsSnapshotToMap(snapshot)
updateFootballGames(collectionsMap)
})
}
render() {
const { match } = this.props;
return (
<div className="game-page">
<h1>games page</h1>
<Route exact path={`${match.path}`} component={GamesList} />
<Route path={`${match.path}/:linkUrl`} component={GameDetailPage}
/>
</div>
);
}
}
const mapStateToProps=state=>({
games:state.games.games
})
const mapDispatchToProps=dispatch=>({
updateFootballGames:collectionsMap=>
dispatch(updateFootballGames(collectionsMap))
})
export default connect(mapStateToProps, mapDispatchToProps)(GamePage);
gamedetailpage.component.jsx
import React from "react";
import { connect } from "react-redux";
import GamePreview from '../../components/game-preview/game-preview.component'
import GameDetails from '../../components/game-details/game-details.component'
const GameDetailPage = (props) => {
const {games, match} = props
const urlparam =match.params.linkUrl
// const games_array = Object.entries(games)
const gameObj=games[urlparam]
console.log('prop',gameObj)
return (
<div className="game-list">
<GameDetails game = {gameObj}/>
</div>
);
};
const mapStateToProps = (state) => ({
games: state.games.games,
});
export default connect(mapStateToProps)(GameDetailPage);
game_details.component.jsx
import React from 'react';
const GameDetails = (props) => {
console.log(props.game.Location)
return(
<div>
Location:{props.game.Location}
<br/>
Price:{props.game.Price}
</div>
)
}
export default GameDetails;
gamelist.component.jsx
import React from "react";
import './game-list.styles.scss'
import GamePreview from "../game-preview/game-preview.component";
import {connect} from 'react-redux'
const GameList=(props)=>{
const {games}=props
console.log(games)
const game_list=Object.entries(games)
console.log(game_list)
return (
<div className="game-list">
{game_list.map(game =>
<GamePreview game = {game[1]}/>)}
</div>
);
}
const mapStateToProps=state=>({
games:state.games.games
})
export default connect(mapStateToProps)(GameList);
gamepreview.component.jsx
import React from "react";
import "./game-preview.styles.scss";
import { withRouter, Route } from "react-router-dom";
import GamePreviewDetail from "../game-preview-detail/game-preview-detail.component";
const GamePreview = (props) => {
const { Location, Time, linkUrl, Price } = props.game;
const { history, match } = props;
return (
<div
className="game-preview"
onClick={() => history.push(`${match.url}/${linkUrl}`)}
>
<div className="game-preview-image">
<p>Picture goes here</p>
</div>
{/* <GamePreviewDetail name = {Location} price={Price}/> */}
<p>Location:{Location}</p>
<p>Price:{Price}</p>
</div>
);
};
export default withRouter(GamePreview);
app.js
import React from 'react';
import './App.css';
//import dependencies
import { Route, Switch } from "react-router-dom";
//import pages
import HomePage from './pages/homepage/homepage'
import GamesPage from './pages/gamespage/gamespage'
import SignInSignUp from './pages/signin-signup-page/signin-signup-page'
import GameDetailPage from "./pages/gamedetailpage/gamedetailpage.component";
import Header from './components/header/header.component';
import { auth, createUserProfileDocument } from './firebase/firebase.utils';
class App extends React.Component{
constructor() {
super();
this.state = {
currentUser: null
}
}
unsubscribeFromAuth = null
componentDidMount() {
this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => {
if (userAuth) {
const userRef = await createUserProfileDocument(userAuth);
// check if the snapshot has changed (subscribe)
// get the user that we just created or that already exists in the db
userRef.onSnapshot(snapshot => {
this.setState({
currentUser: {
id: snapshot.id,
...snapshot.data()}
})
})
} else {
this.setState({currentUser: userAuth})
}
})
}
componentWillUnmount() {
this.unsubscribeFromAuth();
}
render(){
return(
<div>
<Header currentUser = {this.state.currentUser}/>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/games" component={GamesPage} />
<Route exact path="/signin" component={SignInSignUp} />
</Switch>
</div>
)
}
}
export default App;
I would try using useParams hook instead. Then capturing any changes of linkUrl with useEffect hook. Also introducing gameObj with useState.
useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
If you're familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Try to modify <GameDetailPage /> component as the following:
import React, { useState, useEffect } from 'react';
import { useParams } from "react-router-dom";
// other imports
const GameDetailPage = (props) => {
const { games } = props;
let { linkUrl } = useParams();
const [ gameObj, setGameObj ] = useState(null);
useEffect(() => {
if (games) {
const newGameObj = games[linkUrl];
console.log('game object', newGameObj);
setGameObj(newGameObj);
}
}, [games, linkUrl]);
return <div className="game-list">
{ gameObj && <GameDetails game={ gameObj } /> }
</div>
}
+1 - null check:
Also you can see a null check in the return statement for gameObj which helps rendering only that case once you have a value in games array with found linkUrl value.
I hope this helps!