I want the logout button to show up only when the user's logged in. It kind of works now but it's not working the way it should.
With the code below, the logout button shows up when the user's logged in only if the page's refreshed. That's because upon the <App/> component being loaded, the <Navbar/> component mounted along with it.
But how can I make it so that even if <Navbar/>'s loaded, it can still be possible to manipulate when the button can appear based on if the auth token is not null?
Here's App.js:
const App = () => {
let [logoutButtonFlag, setLogoutButtonFlag] = useState(false);
let authToken = localStorage.getItem('token');
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
return (
<>
<Navbar logoutButtonFlag={logoutButtonFlag}/>
</>
);
}
export default App;
Here's Navbar.js:
const Navbar = (props) => {
return (
{!props.logoutButtonFlag ? null : <button className="learn-more">Logout</button>}
);
};
export default Navbar;
you are providing a non-state variable to the list of states that useEffect hook 'listen' to, so it will not run again after you change its value.
const [authToken, setAuthToken] = useState(localStorage.getItem('token'));
and when you update the local storge "token" also update authToken to the same value.
and your useEffect will retrigger on authToken change because now its a state
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
The reason way only on refresh it was updating is because the value of the "token" in local storage was changed already.
Related
so I'm using Redux-Toolkit Query on my project, and I have an authSlice, where I keep the authenticated user info and an access_token.
I also keep this info in local storage so whenever I reload the page I can get the values from the local storage and save them in the state.
The catch is that I have a RequiredAuth component that checks if the user trying to access specific routes is authenticated, by checking if there is an access_token in the state, it works fine except that if I reload this page while I'm authenticated I will be redirected to my login page.
The RequiredAuth component code:
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectToken } from "./authSlice";
const RequireAuth = () => {
const token = useSelector(selectToken)
const location = useLocation()
return (
token ? <Outlet /> : <Navigate to="/auth/login" state={{ from:
location}} replace />
)
}
export default RequireAuth
Code that gets user info and token from local storage when the page is reloaded and adds it to state:
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setCredentials, selectToken } from '../features/auth/authSlice';
const Header = () => {
const dispatch = useDispatch()
const [user, setUser] = useState(JSON.parse(localStorage.getItem('profile')))
const stateToken = useSelector(selectToken)
useEffect(() => {
if(user?.access_token && !stateToken) {
dispatch(setCredentials({ user: user.user, access_token: user.access_token }))
}
}, [])
// Omited code, not relevant
return (
<header className='nav-bar'>
// Omited code, not relevant
</header>
)
}
export default Header
I believe whenever I reload a page where a user needs to be authenticated this happens: in the "RequiredAuth" I will get a null access_token from the state, so I get redirected and only then my useEffect will copy the local storage data to the state.
I fixed this problem by changing the RequiredAuth component to this:
import { useLocation, Navigate, Outlet } from "react-router-dom";
const RequireAuth = () => {
const profile = JSON.parse(localStorage.getItem('profile'))
const location = useLocation()
return (
profile?.access_token ? <Outlet /> : <Navigate to="/auth/login" state={{ from:
location}} replace />
)
}
export default RequireAuth
But I would like to know if there is a better way to keep data in state after reloading a page in order to solve this problem because getting it from local storage feels counterintuitive, since the data will be stored in the state after the useEffect logic completes.
I think this redux-persist package can help. It basically stores your state inside localStorage/sessionStorage and it will rehidrate your state every time you refresh the page.
You can't keep data with a refresh. Netherless, you can use redux-persist. This package keeps the data inside the localstorage and populate automatically the store on a refresh.
you have to use redux-persist with rtk for the store token. above method, if u r navigating to other routes it will work fine. suppose u r refresh the page u will redirect to the login page
Refer redux-persist
I am struggling with getting my NavBar to not display/show until a user has logged in (received a token). I know you can set it up using a ternary but I am not able to get one to function. If another option besides a ternary works I am okay with that.
import Auth from './Auth/Auth';
import Sitebar from './Home/Navbar';
import ReviewIndex from './Reviews/ReviewIndex';
import Navigation from './Home/Navigation'
import {
BrowserRouter as Router
} from 'react-router-dom';
function App() {
const [sessionToken, setSessionToken] = useState('');
useEffect(() => {
if (localStorage.getItem('token')){
setSessionToken(localStorage.getItem('token'));
}
}, [])
const updateToken = (newToken) => {
localStorage.setItem('token', newToken);
setSessionToken(newToken);
console.log(sessionToken)
}
const clearToken = () => {
localStorage.clear();
setSessionToken('');
window.location.href="/"
}
const protectedViews = () => {
return (sessionToken === localStorage.getItem('token') ? <ReviewIndex token={sessionToken}/>: <Auth updateToken={updateToken}/>)
}
return (
<div className="App">
<Router>
<Sitebar sessionToken={sessionToken} clickLogout={clearToken}/>
<Navigation sessionToken={sessionToken} />
{protectedViews()}
</Router>
</div>
);
}
export default App;
the way i would approach this is i would use the turnary operator to conditionally render it. so here you are setting the session token as soon as the component renders and you are only doing it once.
so heres what you could write to conditionally render this.
{sessionToken ? "Put Jsx here that you want to render if they are authenticated" : "Put Jsx here to render if they are not authenticated"}
// css file
.navigation {
visibility: 'hidden';
// or
display: 'none';
}
// component
<Navigation className={sessionToken ? 'navigation': ''} />
It really depends what you want to do. lizardcoder's solution won't mount the component at all until the user is logged in. My solution will hide it, but it will mount. If you don't want to initialize anything in Navigation until the user is logged in, lizardcoder's is the right way to go. If you just want to hide it, css is a good way to go.
I'm making a small SNS app using React. (Gatsby.js)
What I want to do is to persist state in previous pages even when you go back with the browser. Like twitter or instagram, when you go to follow or follower page and visit more pages, you do not lose data when you go back. I can't find a single clue.
Is it something to do with history api or routing?
There is also a User page and it contains some links to following/follower pages in my app. When a user reaches on a page, I am fetching API in useEffect hook using url params then store it to global state(recoil), show on the component.
Here is a problem.
When I visit user page and move forward to follower page, then visit another user page from there and move to his follower page, when I go back with the browser, they don't remember the global state (of course) and it'll get fetch data again which shows loading. Tried to clean data when unmounting because it shows previous data when you go to other page. Couldn't find the bast practice.
Probably it's nothing to do with global states (Recoil), somehow window is remembering what was in the previous pages (and scroll position too)?
I'd appreciate any advice. Thank you.
React/GatsbyJS
Router/Reach-router
Route
...
<PrivateRoute path="/user/:userId" component={UserPage} />
<PrivateRoute path="/:userId/follower" component={FollowerPage} />
...
UserPage
const UserPage = () => {
const { userId } = useParams()
const user = useRecoilValue(userProfileViewData)
const loading = useRecoilValue(loadingUserPage)
...
useEffect(() => {
... // fetch api to get user info
...// store it in global state -> userProfileViewData
}, [])
if(loading) return <div>Loading</div>
return (
<div>
<div>{user.name}</div>
...
<div onClick={() => navigate('/app/' + userId + '/follower')}>Follower</div>
...
</div>
)
}
export default UserPage
FollowerPage
const FollowerPage = () => {
const { userId } = useParams()
const [loading, setLoading] = useState(true)
const followerData = useRecoilValue(followers)
...
useEffect(() => {
...// api calls to get followers
...// store it in global state -> followers
}, [])
useEffect(() => {
return () => {
.. // I have a function to clean data here
}
}, [])
if(loading) {
return <div>loading....</div>
}
return (
<div>
<div>Follower</div>
<div>
{followerData.map(user => (
<div key={`user.id}>
<div onClick={() => navigate(`/app/user/` + user.id)}>
{user.name}
</div>
</div>
))}
</div>
</div>
)
}
export default FollowerPage
Maybe you can use Redux to globally remember state of values
I've been looking around to find a solution and couldn't find anything.
All results that came from searching this was on how to avoid other components re rendering .
Basically as the title says I am trying to make a component which is outside the routes to re render on each route change .
This component returning a notification if history object has a specific state .
Right now this component work as it should, the only problem is that it's only rendered once so it wont search to see if history object has been changed and the notification wont happend .
This is my App :
const App = () => {
return(
<Router history={history}>
<NotificationComponent>
{searchSystemNotification()}
</NotificationComponent>
<img id="background-shape" src={shape} alt="" />
<Header />
<Routes />
<Footer />
</Router>
)
}
This is the function that runs inside the notification component to see if history object has the specific state it's looking for :
const searchSystemNotification = () => {
if(typeof history.location.state !== 'undefined'){
if(history.location.state.hasOwnProperty('message')){
let systemNotification = history.location.state.message;
// Delete message after showing it
let state = history.location.state;
delete state.message;
history.replace({ ...history.location, state });
return(
<Message type={systemNotification.type} text={systemNotification.text} />
)
}
}
}
Basically all i need is to re render "NotificationComponent" on each route change to ofcourse return the "Message" component if the state exists .
useHistory
You can use the useHistory hook anywhere inside your Router to subscribe to changes in its history.
I don't know what your NotificationComponent looks like. One possibility is to call useHistory in the NotificationComponent and define its child as a function of history.
Based on what I'm seeing here, the most straight-forward thing to do is to refactor searchSystemNotification() into a function component and call useHistory there. The component will either render a Message or null.
const SearchSystemNotification = () => {
const history = useHistory();
if(typeof history.location.state === 'object'){
if(history.location.state.hasOwnProperty('message')){
// let's use destructuring instead of `delete`
const {message, ...rest} = {history.location.state};
// pass all values other than message
history.replace({ ...history.location, state: rest });
return(
<Message type={message.type} text={message.text} />
)
}
}
// if we aren't rendering a message then return null
return null;
}
Removing Message
We have a big problem with the above code. Our component will re-render with the new value of history every time that history changes. But inside the hook we are changing history by removing the message from the state. So the message will show for a moment and then immediately disappear as our component re-renders in response to the changes that it made.
We want to either A) just keep the message in the history or B) remove the message only in response to user action, like clicking an "x" on the message.
For B), try this:
const SearchSystemNotification = () => {
const history = useHistory();
if(typeof history.location.state === 'object'){
if(history.location.state.hasOwnProperty('message')){
// let's use destructuring instead of `delete`
const {message, ...rest} = history.location.state;
const onClickClose = () => {
history.replace({ ...history.location, state: rest });
}
return(
<Message type={message.type} text={message.text} onClickClose={onClickClose} />
)
}
}
// if we aren't rendering a message then return null
return null;
}
Edit: useLocation
It turns out we need to use useLocation in order to reload on location changes. We can combine this with useHistory to handle closing the message like this:
const SearchSystemNotification = () => {
const history = useHistory();
const location = useLocation();
if (typeof location.state === "object") {
if (location.state.hasOwnProperty("message")) {
// let's use destructuring instead of `delete`
const { message, ...rest } = location.state;
const onClickClose = () => {
history.replace({ ...history.location, state: rest });
};
return (
<Message
type={message.type}
text={message.text}
onClickClose={onClickClose}
/>
);
}
}
// if we aren't rendering a message then return null
return null;
};
Alternatively, we can avoid making changes to history by keeping the message in the history and having the Message component render conditionally based on its own internal state. We can hide it based on a user click of an "X" or based on a timeout.
const SearchSystemNotification = () => {
const location = useLocation();
if (typeof location.state === "object") {
if (location.state.hasOwnProperty("message")) {
return (
<Message
{...location.state.message}
/>
);
}
}
// if we aren't rendering a message then return null
return null;
};
I am having issues detecting state change from my Redux reducer in a React App. When I change the state within one component, the other component in the app does not receive the update without the component being loaded again or refreshed. Consider the following components:
Component1.js
const Component1 = ({
getFromDB,
updateDB,
db_stuff: { db_stuff }
}) => {
const id = some-id;
useEffect(() => {
getFromDB(id);
updateDB(id, { data: someUpdate });
}, [id]);
// this updates the database and dispatches new state change
const handleUpdateDB = () => {
updateDB(id, { data: someUpdate });
};
return (
<Fragment>
<Button
onClick={handleUpdateDB}
>
Update DB
</Button>
</Fragment>
)
};
Component1.propTypes = {
getFromDB: PropTypes.func.isRequired,
updateDB: PropTypes.func.isRequired,
db_stuff: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
db_stuff: state.db_stuff
});
export default connect(mapStateToProps, {
updateDB,
getFromDB
})(Component1);
In the above component, when the button is click, the action is called to write to the database and change the app state. Within another component I have the state but it is not detecting the state change. Below is the contents of Component2:
Component2
const Component2 = ({
getFromDB,
db_stuff: { db_stuff }
}) => {
const id = some-id;
const [openAlert, setOpenAlert] = useState(false);
const [redirect, setRedirect] = useState(false);
useEffect(() => {
getFromDB(id);
}, [id]);
const handleNextPageClick = () => {
// This is were the issue is I believe but I have included the other code in case it is needed
// I have tried removing getFromDB(id) but it doesn't help
// I have tried making the function async and using await getFromDB(id) but this doesn't work either
getFromDB(id);
if (db_stuff && db_stuff.data <= someOtherData) {
setOpenAlert(true);
} else if (db_stuff && db_stuff.data > someOtherData) {
setRedirect(true);
}
};
const handleAlertClose = () => {
setOpenAlert(false);
};
return (
<Fragment>
//... other components
<Button
onClick={handleNextPageClick}
>
Next
</Button>
<Snackbar
open={openAlert}
autoHideDuration={3000}
onClose={handleAlertClose}
>
<Alert onClose={handleAlertClose} severity="info">
Please wait
</Alert>
</Snackbar>
{redirect ? <Redirect to={`/AnotherComponent`} /> : null}
</Fragment>
);
};
Component2.propTypes = {
getFromDB: PropTypes.func.isRequired,
db_stuff: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
db_stuff: state.db_stuff
});
export default connect(mapStateToProps, {
getFromDB
})(Component2);
So when the user in Component2 clicks next they will get an alert notifying them to please wait for the user in Component1 to continue. When the user in Component1 clicks next, the database and state is updated. But when the user clicks the button they are notified one more time. If they click the button again, then they are allowed to continue. So the state is only updating after an event occurs in the component (user clicking next page updates the state). I want the state to update in Component2 once there is a state change made from Component1. As of now the button has to be clicked twice.
I have seen similar issues but none of the solutions have worked. Any help or guidance is greatly appreciated. Thank you in advance.
So the app runs on two separate machines? Then you expect the update that one user sends to reach the second user. But you use a GET request to get the current state once the component loads. There is no mechanism that subscribes to any state changes from the remote api that happen later. So only a refresh triggers the app to get the updated state.
To get instant updates a Websocket implementation could provide this instant update. But it requires a complete different setup. For now you may reside to polling the remote api periodically. Say every few seconds.