i'm trying to use React Context to manage authentication, but i can't see the value that return the context in PrivateRoute.js
App.js
render() {
return (
<>
<BrowserRouter>
<Islogin>
<Header/>
<Banner/>
<Switch>
<PrivateRoute exact path="/index" component={Landing} />
<PrivateRoute path="/upload" component={Upload} exact />
<PublicRoute restricted={false} path="/unauth" component={Unauthorized} exact />
</Switch>
</Islogin>
</BrowserRouter>
</>
);
}
}
export default App;
the console log of isAuthenticated returns undefined
PrivateRoute.js
const PrivateRoute = ({component: Component, ...rest}) => {
const isAuthenticated = useContext(AuthContext)
console.log(isAuthenticated)
const [validCredentials, setValidCredentials] = React.useState(false)
React.useEffect(() => {
if (typeof isAuthenticated === 'boolean') {
setValidCredentials(isAuthenticated)
}
}, [isAuthenticated])
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /signin page
<Route {...rest} render={props => (
validCredentials ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
export default PrivateRoute;
IsLogin.js
The api call works and the console log shows true.
export default function Islogin({ children }) {
var [auth, setAuth] = React.useState(false)
React.useEffect(() =>{
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log("try")
console.log(res)
setAuth(res)
})
},[])
return (
<AuthContext.Provider value={auth}>
{children}
</AuthContext.Provider>
)
}
You may need to import it at the top of the file that you are using it in (PrivateRoute.js)
Try this:
import {useContext} from 'react'
Related
I'm trying to maintain state values between routes in context. But it gets reset when the route changes.
aackage.json:
"react-router-dom": "^6.8.0",
"react-dom": "^18.2.0",
"react": "^18.2.0",
App.js:
export default const App = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const getData = async () => {
setLoading(true);
const data = await axios.get("url", {
withCredentials: true,
});
setData(data);
setLoading(false);
};
useEffect(() => {
getData()
console.log("I run on route change");
}, []);
const GlobalContextValue= {
data: data,
loading: loading,
};
return (
<>
<GlobalContextProvider value={GlobalContextValue}>
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/:slug" element={<PostPage />} />
{/* <Route path="*" element={<NoPage />} /> */}
</Routes>
</BrowserRouter>
</<GlobalContextProvider />
</>
)
}
Whenever I try to access any route the getData function inside the useEffect calls which inturns resets the data. I have attached a CodeSandbox to replicate the same
I don't know if this problem is related to reactJs or react-router. Thanks in advance
As you don't seem to have any navigation link, I assume you are using the browser search bar, or a normal HTML <a> tag. Well, doing so refreshes the page, so the entire app gets re-created.
Using useNavigate or Link from React Router Dom, doesn't refresh the page, hence your context data remains untouched:
const HomePage = () => {
return (
<>
<h1>Hii Homepage </h1>
<Link to="/1">Go to PostPage</Link>
</>
);
};
const PostPage = () => {
const params = useParams();
return (
<>
<h1>Hii PostPage {params.slug} </h1>
<Link to="/">Go to HomePage</Link>
</>
);
};
export default function App() {
useEffect(() => {
console.log(
"I run on load and route change with browser search bar, not with useNavigate or Link"
);
}, []);
return (
<>
{/* This context wrapping BrowserRouter keeps its value if you navigate with Link or
useNavigate. */}
<GlobalContextProvider value={{ key: "some value" }}>
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/:slug" element={<PostPage />} />
{/* <Route path="*" element={<NoPage />} /> */}
</Routes>
</BrowserRouter>
</GlobalContextProvider>
</>
);
}
I get a username from "login.jsx" and i want to pass it to the "App.js" to access in everywhere. How can I do that?
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
}, []);
return (
<BrowserRouter>
<Offer/>
<Navigationbar/>
<Route path="login" element={user ? <Navigate to="/courses" /> : <Login />} />
</Routes>
<Footer />
</BrowserRouter>
);
}
you can pass callback function to Login component as props and call this prop inside Login component and pass user name as argument, then inside callback function you call setUser function to update the value of user
function App() {
const [user, setUser] = useState(null);
const updateUser = (value) => {
setUser(value);
}
useEffect(() => {
}, []);
return (
<BrowserRouter>
<Offer/>
<Navigationbar/>
<Route path="login" element={user ? <Navigate to="/courses" /> : <Login updateUser={updateUser} />} />
</Routes>
<Footer />
</BrowserRouter>
);
}
Here is How you can do that
App.jsx
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
}, []);
const updateUser = (newUser) => {
setUser(newUser);
}
return (
<BrowserRouter>
<Offer/>
<Navigationbar/>
<Route path="login" element={user ? <Navigate to="/courses" /> : <Login onNewUser={updateUser} />} />
</Routes>
<Footer />
</BrowserRouter>
);
}
In Login.jsx when you get new user call props.updateUser(user)
You can do that with React Context;
Basically, contexts are global states.
You can read more here : https://reactjs.org/docs/context.html
It allows you to wrap your <App /> into a context provider and pass to your wrapper a value that you will be able to user in your App.
If I set in context provider sample data, I see this data in all nested components.
But I need login to the account and in response, I get data about user for set in the global context and use in all components.
context/AuthProvider.tsx
const AuthContext = createContext<any>({});
export const AuthProvider = ({ children }: any) => {
const [auth, setAuth] = useState({});
return (
<>
<AuthContext.Provider value={{ auth, setAuth }}>{children}</AuthContext.Provider>
</>
);
};
hooks/useAuth.ts
const useAuth = () => {
return useContext(AuthContext);
};
export default useAuth;
index.tsx
import { AuthProvider } from './context/AuthProvider';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
I have App with BrowserRouter logic for not logged users redirect to login. If logged, so go to the Homepage.
components/App/App.tsx
const AppContainer: FC<any> = () => {
const { token } = useToken();
return (
<>
<div className={'wrapper'}>
<BrowserRouter>
{!token ? <LoggedOutRoutes /> : <LoggedInRoutes />}
</BrowserRouter>
</div>
</>
);
};
const LoggedOutRoutes: FC<any> = () => (
<Switch>
<Route path="/" exact={true}>
<Login />
</Route>
<Redirect from={'*'} to={'/'} />
</Switch>
);
const LoggedInRoutes: FC = () => (
<Switch>
<Route path="/" exact={true} component={Homepage} />
</Switch>
);
In login component sending request with data and I getting access_token and user data. Now I need set user data in useAuth hook.
const Login: FC<any> = () => {
const { setToken } = useToken();
const { setAuth } = useAuth()
const handleSubmit = async (event: any) => {
event.preventDefault();
const res = await API.login({
login,
password
});
const { access_token, user } = res;
setToken(access_token);
setAuth(user);
window.location.reload();
};
return (
<form onClick={handleSubmit}>
// ...There i have submit form, not interesting
</form>
);
};
After reloading the page, my page will be Homepage where I won't use my context data from the provider but I have an empty object, why?
The problem is window.location.reload. Any SPA will not retain data after a page refresh by default.
Now if you still want to persist that information even after page reload, i recommend to persist that info in localStorage. So something like this should work.
export const AuthProvider = ({ children }: any) => {
const [auth, setAuth] = useState(localStorage.get('some-key') || {});
const updateAuth = (auth) => {
localStorage.set('some-key', auth);
setAuth(auth);
}
return (
<>
<AuthContext.Provider value={{ auth, updateAuth }}>{children}</AuthContext.Provider>
</>
);
};
I want to show a navbar whenever user is logged in
when ever user presses login button i replace the page with home page
i've tried to define use state and on condition of if path name is not equal to /login or /step-two set it to true then in the app i said if usestate was equal by true show the navbar
but i don't see navbar until i refresh the page
here is what i have tried in useeffect
const [hasNavBar, setNavBar] = useState(false)
useEffect(() => {
console.log(history.location.pathname);
if (history.location.pathname !== '/login'|| history.location.pathname !=='/step-two'
) {
setNavBar(true)
}
}, [])
here is the whole code
const App = () => {
const [hasNavBar, setNavBar] = useState(false)
useEffect(() => {
console.log(history.location.pathname);
if (history.location.pathname !== '/login' ||
history.location.pathname !== '/step-two'
) {
setNavBar(true)
}
}, [])
return (
<div>
<SNackbar></SNackbar>
<Router history={history}>
<div>
<Route path="/login" exact component={FirstLogin} />
<Route path="/step-two" exact component={SecondLogin} />
<Route path="/home" exact component={Home} />
<Route path='/ranking-list' exact component={RankingListPage} />
</div>
</Router>
<div>
{hasNavBar ?
<div>
<BottomNavBar />
</div> : ''
}
</div>
</div>
)
};
export default App;
update
i tried to use history.listen bu still i see the same error
const App = () => {
const [hasNavBar,setnavbar]=useState(false)
useEffect(() => {
history.listen(()=>{
if (history.location.pathname === '/'||history.location.pathname === '/steptwo') {
setnavbar(false);
}else{setnavbar(true);}
})
if (localStorage.getItem("token") && history.location.pathname === '/') {
history.replace('/home');
}
}, [])
return (
<div>
<SNackbar></SNackbar>
<Router history={history}>
<div>
<Route path="/" exact component={FirstLogin} />
<Route path="/steptwo" exact component={SecondLogin} />
<Route path="/home" exact component={Home} />
<Route path='/ranking-list' exact component={RankingListPage} />
</div>
</Router>
{hasnavbar?
<div
style={{ position: 'fixed', bottom: '0px', width: "100%", height: '80px' }}>
<BottomNavBar />
</div>:''
}
</div>
)
};
export default App;
as #lissettdm said
you need to use history.listen to call an function whenever history changes
but no need to Wrap App inside Router
if you don't use history.listen usestate only would change whenever you refresh the page as the useeffect would be run on refresh
you could define its default to false and whenever it is not in the condition it would be true a else statement so that whenever you change the route it would be called and make hasnavbar to true
const [hasnavbar,setnavbar]=useState(false)
useEffect(() => {
history.listen(()=>{
if (history.location.pathname === '/login'||history.location.pathname === '/steptwo') {
setnavbar(false);
}else{setnavbar(true);}
})
}, [])
here is the whole code
const App = () => {
const [hasnavbar,setnavbar]=useState(false)
useEffect(() => {
history.listen(()=>{
if (history.location.pathname === '/login'||history.location.pathname === '/steptwo') {
setnavbar(false);
}else{setnavbar(true);}
})
}, [])
return (
<div>
<SNackbar></SNackbar>
<Router history={history}>
<div>
<Route path="/login" exact component={FirstLogin} />
<Route path="/steptwo" exact component={SecondLogin} />
<Route path="/home" exact component={Home} />
<Route path='/ranking-list' exact component={RankingListPage} />
</div>
</Router>
{hasnavbar?
<div>
<BottomNavBar />
</div>:''
}
</div>
)
};
export default App;
Wrap App inside Router component and listen for history changes:
index.js
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
App.js
const history = useHistory();
useEffect(() => {
return history.listen(location => {
if (
history.location.pathname !== "/login" &&
history.location.pathname !== "/step-two"
) {
setNavBar(true);
}
});
}, []);
See working example: https://stackblitz.com/edit/react-5xd5d1?file=src%2FApp.js
Change your useEffect's logic from || to &&, right now it doesn't make sense.
Try to use a useLocation hook instead:
import { useLocation } from 'react-router-dom';
...
let location = useLocation();
...
if (location.pathname !== '/login' && location.pathname !== '/step-two') { ... }
I just tried to build the react-router docs ex on browser but there is problem in AuthButton component it isn't showing signOut button when the isAuthenticated turns true
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
useHistory,
useLocation,
} from 'react-router-dom';
export default function AuthExample() {
return (
<Router>
<div>
<AuthButton />
<ul>
<li>
<Link to='/public'>Public Page</Link>
</li>
<li>
<Link to='/protected'>Protected Page</Link>
</li>
</ul>
<Switch>
<Route path='/public'>
<PublicPage />
</Route>
<Route path='/login'>
<LoginPage />
</Route>
<PrivateRoute path='/protected'>
<ProtectedPage />
</PrivateRoute>
</Switch>
</div>
</Router>
);
}
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
fakeAuth.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
fakeAuth.isAuthenticated = false;
setTimeout(cb, 100);
},
};
function AuthButton() {
let history = useHistory();
return fakeAuth.isAuthenticated ? (
<p>
Welcome!{' '}
<button
onClick={() => {
fakeAuth.signout(() => history.push('/'));
}}>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
);
}
function PrivateRoute({ children, ...rest }) {
return (
<Route
{...rest}
render={({ location }) =>
fakeAuth.isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: '/login',
state: { from: location },
}}
/>
)
}
/>
);
}
function PublicPage() {
return <h3>Public</h3>;
}
function ProtectedPage() {
return <h3>Protected</h3>;
}
function LoginPage() {
let history = useHistory();
let location = useLocation();
let { from } = location.state || { from: { pathname: '/' } };
let login = () => {
fakeAuth.authenticate(() => {
history.replace(from);
});
};
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={login}>Log in</button>
</div>
);
}
The reason it's not updating is because it doesn't know to update. You change the route but AuthButton doesn't know to re-render based on the route you need to pass it a prop so that it knows when to update. I refactored your code to incorporate using react hooks. By using hooks you can store isAuthenticated in local state in AuthExample via useState.
From AuthExample, pass down the state value for isAuthenticated as a prop to AuthButton. If the prop changes, AuthButton will detect it and this will trigger a re-render of AuthButton and reflect the correct component structure you are looking for. See below.
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
useHistory,
useLocation
} from "react-router-dom";
export default function AuthExample() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const fakeAuth = {
isAuthenticated: isAuthenticated,
authenticate(cb) {
fakeAuth.isAuthenticated = true;
setIsAuthenticated(true);
setTimeout(cb, 100); // fake async
},
signout(cb) {
setIsAuthenticated(false);
fakeAuth.isAuthenticated = false;
setTimeout(cb, 100);
}
};
return (
<Router>
<div>
<AuthButton fakeAuth={fakeAuth} isAuthenticated={isAuthenticated} />
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
<Switch>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage fakeAuth={fakeAuth} />
</Route>
<PrivateRoute path="/protected" fakeAuth={fakeAuth}>
<ProtectedPage />
</PrivateRoute>
</Switch>
</div>
</Router>
);
}
function AuthButton(props) {
const { fakeAuth, isAuthenticated } = props;
let history = useHistory();
return isAuthenticated ? (
<p>
Welcome!{" "}
<button
onClick={() => {
fakeAuth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
);
}
function PrivateRoute({ children, ...rest }) {
const { fakeAuth } = rest;
return (
<Route
{...rest}
render={({ location }) =>
fakeAuth.isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
function PublicPage() {
return <h3>Public</h3>;
}
function ProtectedPage() {
return <h3>Protected</h3>;
}
function LoginPage(props) {
const { fakeAuth } = props;
let history = useHistory();
let location = useLocation();
let { from } = location.state || { from: { pathname: "/" } };
let login = () => {
fakeAuth.authenticate(() => {
history.replace(from);
});
};
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={login}>Log in</button>
</div>
);
}
You can also see a working example in this code sandbox. There are a few ways to do this but hooks make it easy to manipulate state values to update functional components without having to make them class components. This way also keeps most of your code intact as is just adding a few checks for when isAuthenticated is updated.
I think the problem is in rendering process.
In my opinion, if you put the sub-functions in to the exported function, this problem may solve.
If the problem won't solve, try the class base component for handling this rendering process.
wish you success