I use 'React Context' to pass state user to the child components.
The problem: Everytime you reload the page, the state user value is null. This cause the page briefly redirect to /login before redirecting to '/dashboard`. This will prevent user from accessing a page manually.
The goal: How to wait for state user before rendering?
App.js
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) setUser(user);
else setUser(null);
});
}, []);
return (
<Router>
<AuthDataContext.Provider value={user}>
<Layout>
<Routes />
</Layout>
</AuthDataContext.Provider>
</Router>
);
}
routes
export const Routes = () => {
const user = useContext(AuthDataContext);
if (user) {
return (
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route path="/list" component={List} />
<Redirect to="/dashboard" />
</Switch>
);
} else {
return (
<Switch>
<Route path="/login" component={LogIn} />
<Route path="/register" component={Register} />
<Route path="/passwordreset" component={PasswordReset} />
<Redirect to="/login" />
</Switch>
);
}
};
authdata
import React from "react";
export const AuthDataContext = React.createContext(null);
edit: quick fix
App.js
const [user, setUser] = useState("first");
routes
else if (user === "first") return null
You can have a component that initiates API call and renders some kind of loader while user is waiting for a response. Then in case user is authenticated you can render regular application routes, if not – render routes for sign in, sign up and so on.
Related
My Application: Laravel(API SERVER) + React(Frontend)
What I want to do is restrict the access to routes of App if the user is not authenticated.
function App() {
const [userInfo, setUserInfo] = setState({isSignedIn: false});
return (
<Routes>
<Route path="/admin/*" element={ userInfo.isSignedIn? <Admin />: <Navigate replace to="/home/sign-in"/> } />
<Route path="/home/*" element={<Home />} />
</Routes>
);
}
To do that, auth API should be requested before App rendered.
My first try was like...
function App() {
const requestUserInfo = async() => {
const result = await axios.get("/auth");
return result.data;
}
const [userInfo, setUserInfo] = setState(requestUserInfo);
return (
<Routes>
<Route path="/admin/*" element={ userInfo.isSignedIn? <Admin />: <Navigate replace to="/home/sign-in"/> } />
<Route path="/home/*" element={<Home />} />
</Routes>
);
}
However I realized initial state cannot be async.
Also I tried axios in useEffect(), but I think it forces too many rerendering.
So Here Is My Question: What is the most common way to get auth info by API?
I'm using Firebase v9 and react-router v6. I haven't used v6 so this was quite confusing. How can I make it where the guest user can only access the login page. Only users who were logged in can access the homepage and other pages.
Everytime I'll reload any page, it will show this in the console but it will still direct the user to the right page :
No routes matched location "/location of the page"
How can I use a private route for the profile page?
//custom hook
export function useAuth() {
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user));
return unsub;
}, []);
return currentUser;
}
App.js
import { auth, useAuth } from "./Firebase/utils";
import { onAuthStateChanged } from "firebase/auth";
function App() {
const currentUser = useAuth();
const user = auth.currentUser;
const navigate = useNavigate();
console.log(currentUser?.email);
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
console.log(uid);
navigate("/Home");
// ...
} else {
// User is signed out
// ...
navigate("/");
}
});
}, []);
return (
<div>
<div>
<Routes>
{currentUser ? (
<>
//If i do it this way and I'll go the profile page and reload it, it will always go to back to the Homepage.
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={
<LogInPage />
}
/>
</>
)}
</Routes>
</div>
</div>
);
}
export default App;
This is what the console.log(user) shows:
Package.json file:
Issues
The main issue is that the currentUser value is initially falsey
const [currentUser, setCurrentUser] = useState();
and you are making a navigation decision on unconfirmed authentication status in App
<Routes>
{currentUser ? (
<>
// If i do it this way and I'll go the profile page and reload it,
// it will always go to back to the Homepage.
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={<LogInPage />}
/>
</>
)}
</Routes>
When refreshing the page the currentUser state is reset, is undefined, i.e. falsey, and only the "/" path is rendered.
Solution
In react-router-dom is a common practice to abstract route protection into a specialized "protected route" component. You will also want to conditionally handle the indeterminant state until your Firebase auth check has had a chance to confirm an authentication status and update the currentUser state.
Example:
export function useAuth() {
const [currentUser, setCurrentUser] = useState(); // <-- initially undefined
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user)); // <-- null or user object
return unsub;
}, []);
return { currentUser };
}
AuthWrapper - Uses the useAuth hook to check the authentication status of user. If currentUser is undefined it conditionally returns early null or some other loading indicator. Once the currentUser state is populated/defined the component conditionally renders either an Outlet for nested/wrapped Route components you want to protect, or the Navigate component to redirect to your auth route.
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation();
const { currentUser } = useAuth();
if (currentUser === undefined) return null; // <-- or loading spinner, etc...
return currentUser
? <Outlet />
: <Navigate to="/" replace state={{ from: location }} />;
};
App - Unconditionally renders all routes, wrapping the Home and Profile routes in the AuthWrapper layout route.
function App() {
return (
<div>
<div>
<Routes>
<Route element={<AuthWrapper />}>
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</Route>
<Route path="/" element={<LogInPage />} />
</Routes>
</div>
</div>
);
}
I was trying to prevent user from accessing login and signup page once he is logged in. I was trying to find some solution here or Youtube but didn't manage to find what I was looking for. In meantime I was trying to achieve that on my own and I managed to do it with useEffect hook. I'm not sure if it's best practice so check my code below and If someone knows a better way it would mean a lot to me since I am a beginner and I still have a lot of unknowns. To be clear everything works good, just want to know if it's good practice to navigate with useEffect back to home page if user exists.
App.js
const App = () => {
return (
<Router>
<UserAuthContextProvider>
<Routes>
<Route
path="/home"
element={
<ProtectedHomeRoute>
<Home />
</ProtectedHomeRoute>
}
/>
<Route path="/" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</UserAuthContextProvider>
</Router>
);
};
This is part of UserAuthContextProvider where I'm setting user onAuthStateChange
const [user, setUser] = useState("");
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
return (
<userAuthContext.Provider
value={{
user,
signUp,
signIn,
facebookSignIn,
logout,
}}
>
{!loading && children}
</userAuthContext.Provider>
);
And inside Signin component I just used useEffect to check is there is user to redirect back to home
useEffect(() => {
if (user) {
navigate("/home");
}
});
The solution could be creating a component as ProtectedHomeRoute, doing the opposite thing, similar to your useEffect inside Signin; althought, no need to useEffect.
For example:
const PublicRoute = ({children}) => {
const {user} = useUserAuth()
return user?<Navigate to='/home'>:children
}
So then in your App.js:
const App = () => {
return (
<Router>
<UserAuthContextProvider>
<Routes>
<Route
path="/home"
element={
<ProtectedHomeRoute>
<Home />
</ProtectedHomeRoute>
}
/>
<Route path="/" element={<PublicRoute>
<Signin />
</PublicRoute>} />
<Route path="/signup" element={<PublicRoute>
<Signup />
</PublicRoute>} />
</Routes>
</UserAuthContextProvider>
</Router>
);
};
Even thought this can be a solution, refreshing page will lead to a data loose (since user is set to '' when refreshing). I recommend you using some libraries to change this behaviour, as Redux, Redux persist...
I am new to localStorage and React Router, and my goal is:
Redirect user to the "/dashboard" when he is logged in, and Redirect back to '/home' when he is logged out. Also, of course, not allowing him to go to the 'dashboard' if he is not logged in. For some reason my code in App.js not working:
function App() {
let userLogged;
useEffect(() => {
function checkUserData() {
userLogged = localStorage.getItem("userLogged");
}
window.addEventListener("storage", checkUserData);
return () => {
window.removeEventListener("storage", checkUserData);
};
}, []);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home />} />
{userLogged ? (
<Route path={"/dashboard"} element={<Dashboard />} />
) : (
<Route path={"/home"} element={<Home />} />
)}
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
I set it in the home and dashboard pages by localStorage.setItem('userLogged', false) and localStorage.setItem('userLogged', true)
You can only listen to changes in localStorage from other window/browser contexts, not from within the same browser/window context. Here it's expected the window knows its own state. In this case, you actually need some React state.
Convert the userLogged to a React state variable and use a useEffect hook to initialize and persist the userLogged state to/from localStorage. Instead of conditionally rendering Route components, create a wrapper component to read the userLogged value from localStorage and conditionally render an Outlet for nested/wrapped routes or a Navigate component to redirect to your auth route to login.
Example:
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation(); // current location
const userLogged = JSON.parse(localStorage.getItem("userLogged"));
return userLogged
? <Outlet />
: (
<Navigate
to="/"
replace
state={{ from: location }} // <-- pass location in route state
/>
);
};
...
function App() {
const [userLogged, setUserLogged] = useState(
JSON.parse(localStorage.getItem("userLogged"))
);
useEffect(() => {
localStorage.setItem("userLogged", JSON.stringify(userLogged));
}, [userLogged]);
const logIn = () => setUserLogged(true);
const logOut = () => setUserLogged(false);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home logIn={logIn} />} />
<Route path={"/home"} element={<Home logIn={logIn} />} />
<Route element={<AuthWrapper />}>
<Route path={"/dashboard"} element={<Dashboard />} />
</Route>
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
Home
import { useLocation, useNavigate } from 'react-router-dom';
const Home = ({ logIn }) => {
const { state } = useLocation();
const navigate = useNavigate();
const loginHandler = () => {
// authentication logic
if (/* success */) {
const { from } = state || {};
// callback to update state
logIn();
// redirect back to protected route being accessed
navigate(from.pathname, { replace: true });
}
};
...
};
You can render both route and use Navigate component to redirect. Like this -
// [...]
<Route path={"/dashboard"} element={<Dashboard />} />
<Route path={"/home"} element={<Home />} />
{
userLogged ?
<Navigate to="/dashboard" /> :
<Navigate to="/home" />
}
// other routes
Whenever you logout, you need to manually redirect to the desired page using useNavigate hook.
I have a React project that has a HeaderComponent that exists for all routes in project like this:
function App() {
return (
<Fragment>
<Router>
<HeaderComponent />
<Routes>
<Route path="/login" element={<Login />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/" element={<LandingPage />}></Route>
</Routes>
<FooterComponent />
</Router>
</Fragment>
);
}
And my problem is that the <HeaderComponent> is rendered when the website first loads but when the user logs in, the <HeaderComponent> is not aware of the changes because the component has already mounted.
So in my <HeaderComponent>, the componentDidMount function looks like this:
componentDidMount() {
AuthService.authorizeUser()
.then((r) => {
this.setState({ loggedIn: true });
})
.catch((error) => {
this.setState({ loggedIn: false });
});
}
This only works if I refresh the page.
Basically, if a user successfully logs in (from the <Login> component), what is the proper way of making my HeaderComponent aware of this?
You can use Context API to make AuthContext to share global state within your app:
// AuthContext.js
export const AuthContext = React.createContext({});
export const AuthProvider = ({
children,
}) => {
// your context logic
return (
<AuthContext.Provider value={yourAuthValue}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => React.useContext(AuthContext);
// Layout.js
import { Outlet } from 'react-router-dom'
// Using `Outlet` to render the view within layout
export const Layout = () => {
return (
<>
<HeaderComponent />
<Outlet />
<FooterComponent />
</>
)
}
// HeaderComponent.js
import { useAuth } from './AuthContext'
export const HeaderComponent = () => {
// get state from auth context
const { isLoggedIn } = useAuth()
return // rest of your code
}
// App.js
function App() {
return (
<Fragment>
<-- Wrap your app with AuthContext let other components within your app can access auth state !-->
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
</Fragment>
);
}
There are a couple of ways to do so.
When you're facing a situation where you need to share the same state between multiple components, lifting the state up should be the first thing to try Check this codesandbox.
And some great blogposts to read, KCD - Prop Drilling, KCD - State Management with React
Such approach may cause "prop drilling" when you need the same state in deeply nested components and that's where the context API comes in handy.
codesandbox