The issue seems to be simple: I have a main App which has a log in and log out methods, among other things:
onLoggedIn(authData) { // need to use both the user and the token
console.log(authData); // authData logged in console
this.setState({
user: authData.user.username //authData.user.username saved in userState
});
// saves token and user from handleSubmit in local storage
localStorage.setItem('token', authData.token);
localStorage.setItem('user', authData.user.username);
this.getMovies(authData.token); // retrieves films list
}
onLoggedOut() {
this.setState({
user: null
});
localStorage.clear();
}
onLoggedOut is used by a navbar button (which is also a child component):
function MyNavbar() {
return (
<Navbar bg="#781820" expand="lg">
<Navbar.Brand className="logo">
<h1>App Name</h1>
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link className="text-white ml-5">Home</Nav.Link>
</Nav>
<Form inline>
<Button id="btn-logout" type="submit" onClick={() => this.onLoggedOut()}>Logout</Button>
</Form>
</Navbar.Collapse>
</Navbar>
);
}
However, despite the fact that I have tried to import the method and/or the component into the child component:
import { onLoggedOut } from '../App/App;
import { App } from '../App/App';
the onLoggedOut method doesn't work. The method works (if I place the button in the main App, it clears the items in localStorage. Any of colleagues here could possible shed some light, please? Thanks in advance.
You would need to use Context Api. I have made a gist that have a simple approach for that.
There is a simple mistake you are making
You can import a method from a class
import { onLoggedOut } from '../App/App';
import { App } from '../App/App';
I'm not sure how you are doing this import, but
If onLoggedOut is inside App, you can't do that type of import.
If onLoggedOut is outside App, it would never work, because it uses this.setState.
You need to some something that is called Lifting State Up, but for authentication, it's better to use Context Api. I have made a gist (it's kind of the same, but better for your case).
Related
I am not very experienced with React yet and have been trying to approach the problem of securing specific routes from authentication.
For this purpose, I need to implement a HOC component, specifically a "PrivateRoute" component.
Some main generalities:
I'm developing with React 17.0.2 and then react-router-dom 6.2.1
I need to verify the presence of the token on a Redis server (in the cloud) and validate it.
The project is divided into two "modules," one in React for the frontend and one in node js/express, which exposes the server-side logic.
In the frontend module, the routes are implemented as follows:
return (<Router>
<div className="App">
<nav className="navbar navbar-expand-lg navbar-light fixed-top">
<div className="container">
<Link className="navbar-brand" to={"/"}>My Site</Link>
<div className="collapse navbar-collapse" id="navbarTogglerDemo02">
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<Link className="nav-link" to={"/sign-in"}>Login</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to={"/sign-up"}>Sign up</Link>
</li>
</ul>
</div>
</div>
</nav>
<Routes>
<Route exact path='/' element={<PrivateRoute><Home /></PrivateRoute>} />
<Route path="/sign-in" element={<Login />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="*" Component={<Login/>} status={403}/>
</Routes>
</div></Router>
As you can see, for example, I want the Home component to be protected, and therefore I implement the PrivateRoute component with which I wrap the Home component.
The implementation of the PrivateRoute component must validate any jwt token present in localStorage, passing it to an HTTP validation service exposed by the node/express module. The token is sent to the http service by passing the Authorization Bearer header.
The logic of retrieving from the localStorage and sending to the validation service is implemented with a useEffect (() => {}, []). Inside it is defined async function, so you can make the axios call with await. The function is always called from the body of useEffect. However, I cannot understand why updating the state variable created with useState seems not to update correctly (synchronization problem?). This does not seem to trigger the rendering update if the Home component is validated correctly (in the ternary operator, home is represented by props.children), rather than redirecting to the login page.
Below I report the implementation of the PrivateRoute:
export const PrivateRoute = (props) => {
console.log('props',props);
const [tokenValid, setTokenValid] = useState('');
useEffect(() => {
const checkRedisToken = async () => {
console.log('Access localStorage');
const ni_token = localStorage.getItem('ni_token');
console.log('Returned localStorage');
const config = { headers: { 'Authorization': `Bearer ${ni_token}` } };
console.log('Call axios');
const response = await axios.get('http://localhost:5000/identity/verifytoken', config);
const data = response.data;
console.log('After axios ', data);
setTokenValid(response.data);
}
checkRedisToken();
console.log('tokenValid', tokenValid);
}, []);
return (
tokenValid !== '' ?
props.children :
<Navigate to="/sign-in" />
)
}
Thank you in advance for your help in understanding my mistakes.
Thank you so much
I try to describe the solution I have adopted. The solution is the result of the reading of similar posts previously published and of readings scattered on the web.
As noted by #tromgy the problem of my first implementation is caused by the asynchronous call to the endpoint / identity / verifytoken.
The flow of execution, after the call, continues (waiting for a result) and the tokenValid state variable, even if set, seems not to be immediately detected by React and a new rendering is not invoked.
The correct solution seems to be to add an additional "loadingComplete" state.
The "loadingComplete" state seems to handle the stages where the component is not or is mounted waiting for the authenticated state to be ready and set as well.
The declaration of the routes has remained unchanged and for the sake of simplicity I show the code fragment below:
function App() {
return (<Router>
<div className="App">
<nav className="navbar navbar-expand-lg navbar-light fixed-top">
<div className="container">
<Link className="navbar-brand" to={"/"}>My Site</Link>
<div className="collapse navbar-collapse" id="navbarTogglerDemo02">
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<Link className="nav-link" to={"/sign-in"}>Login</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to={"/sign-up"}>Sign up</Link>
</li>
</ul>
</div>
</div>
</nav>
<Routes>
<Route exact path='/' element={<PrivateRoute><Home /></PrivateRoute>} />
<Route path="/sign-in" element={<Login />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="*" Component={<Login/>} status={403}/>
</Routes>
</div></Router>
);
}
export default App;
As mentioned, the changes made to the PrivateRoute component are quite substantial. Before giving a brief description below, I report the new implementation below:
export const PrivateRoute = (props) => {
console.log('props', props);
const [authenticated, setAuthenticated] = useState(null);
const [loadingComplete, setLoadingComplete] = useState(false);
useEffect(() => {
const ni_token = localStorage.getItem('ni_token');
const isLogin = async (ni_token) => {
console.log('isLogin');
try {
const config = { headers: { 'Authorization': `Bearer ${ni_token}` } };
const result = await axios.get('http://localhost:5000/identity/verifytoken', config);
setAuthenticated(result.data);
} catch (e) {
console.log(e);
}
setLoadingComplete(true);
}
isLogin(ni_token);
}, []);
if(loadingComplete){
console.log('Completed', authenticated);
return (authenticated === 'Verify Token Respone' ?
props.children :
<Navigate to="/sign-in" />)
}else {
console.log('Loading...');
return (<div> Loading...</div>)
}
}
As you can see the useEffect with empty dependency array, so that the passed function is only executed once.
On the first call to the web application root we observe the execution of console.log ('props', props).
Immediately thereafter, the console.log trace shows ('Loading ...').
Immediately after, the "isLogin" log appears, in fact useEffect calls isLogin (ni_token). At this moment the asynchronous http call is made to the endpoint "/ identity / verifytoken" in charge of validating the token. At the first access to the frontend (http: // localhost: 3000) there is no token and the endpoint returns an http 403 (Forbidden) status. The authenticated state variable is not changed and its value remains "null", but lodingComplete is set to "true" causing <Navigate to = "sign-in /> to be returned, ie the redirect to the login page.
As you can see, the change in state determines the rendering of the component, the "props" log and the "Completed null" log reappear.
Below you can find what is logged at first access:
props Object
Loading ...
Failed to load resource: the server responded with a status of 403 (Forbidden)
props Object
Completed null
Some thougths for further deepening about not so clear aspects: if I do not misinterpret, at the new rendering the stream passes again through console.log ('props', props) and obviously react does not re-execute the useStates? Otherwise loadingComplete would be false again and would render Loading while as expected it goes through redirection to the login component. Is my interpretation correct?
If I finally log in and a positive authentication happens, the flow highlighted with the logs executed with console.log is the following:
props {children: {…}}
Loading ...
isLogin
props {children: {…}}
Loading ...
props {children: {…}}
Completed Verify Token Respone
If I'm not wrong, the re-rendering is done twice in addition to the first one. But there is something I can't quite understand. The useEffect is performed (log isLogin) the asynchronous http call is performed and setAuthenticated will be performed later the flow of execution certainly goes through setLoadingComplete (true) at this point I expect a re-rendering but contrary to what I expect I still see the "Loading" log ... But on the other hand if this is not the case it should still redirect to the Login component. Eventually, the "Completed Verify Token Respone" log appears at the setAuthenticated setting.
I'm still a bit puzzled who helps me understand what I see and what is happening? I hope this post can be useful to learn more about the mechanisms and functioning of React and of this specific need.
I am very new to React Bootstrap and exploring how much it is customizable. While it's easy to hook into onSelect, I can't find any way to cancel the action itself. For example, if user has unsaved changes and should not go to the other route, then how onSelect can be prevented?
This is from React Bootstrap docs with a little change:
import React from 'react';
import {Navbar, Nav, NavItem, NavDropdown, MenuItem} from 'react-bootstrap';
// import './Header.scss';
function myOnSelect(eventKey, event) {
console.log(eventKey);
console.log(event);
alert(`Going ${eventKey}`);
return false // How to prevent from going there???
}
export const Header = props => {
const {brand} = props.topNav;
return (
<Navbar collapseOnSelect expand="sm" bg="light" variant="light" sticky="top"
onSelect={myOnSelect}>
<Navbar.Brand href="#home">{brand}</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav"/>
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="#features">Features</Nav.Link>
<Nav.Link href="#pricing">Pricing</Nav.Link>
<NavDropdown title="Dropdown" id="collasible-nav-dropdown">
<NavDropdown.Item href="#action/3.1">Action</NavDropdown.Item>
<NavDropdown.Item href="#action/3.2">Another action</NavDropdown.Item>
<NavDropdown.Item href="#action/3.3">Something</NavDropdown.Item>
<NavDropdown.Divider/>
<NavDropdown.Item href="#action/3.4">Separated link</NavDropdown.Item>
</NavDropdown>
</Nav>
<Nav>
<Nav.Link href="#deets">More deets</Nav.Link>
<Nav.Link eventKey={2} href="#memes">
Dank memes
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
);
};
Note: I do not mean going away to completely another page via a link, just another route inside the same page. It can be for "unsaved changes" or some other temporary reason, so disabling navbar menus is an overkill from UX point of view.
Extra twist to this is the project is going to use react-observable (with Redux and RxJS) for event/action logic, so it would be nice to understand how easy it is to hook React Boostrap into it. That is, some actions will need to be intercepted and handled in the epics and other middleware instead of following default React Bootstrap automation. So:
The example here can have some very specific solution, but could be nice to also hear how easy it is to do action-interceptions in other places of React Bootstrap or whether the javascript logic should be heavily customized or even abandoned.
I guess, for Navs the answer is somewhere in these arrangements: https://github.com/react-bootstrap/react-bootstrap/blob/master/src/AbstractNavItem.js
Maybe explicitly set activeKey property together with event.preventDefault(); can be used to build the desired control over nav.
We cannot stop the select from performing its onchange property. It will execute as soon as we select an option. Consider adding conditions in the myOnSelect function
function myOnSelect(eventKey, event) {
console.log(eventKey);
console.log(event);
if(condition) {
//do something
alert(`Going ${eventKey}`);
}
else {
//do something else
return false // How to prevent from going there???
}
}
I have searched for a few hours now found no working solution. I followed this guide
https://dev.to/crishanks/deploy-host-your-react-app-with-cpanel-in-under-5-minutes-4mf6
and everything works fine,
except for when I go into a react router path.
It doesn't just fail or return 404, it shows TypeError (basically API returning null or undefined)
The same route with the same API call works perfectly on localhost:3000.
I ran npm run build and copied all the contents onto cPanel file manager
Went to my domain and everything was working until I clicked into a react router route that has api fetching which works in localhost
<div>
<Header />
<Segment placeholder style={{marginTop: "2rem"}}>
<Grid celled>
<Grid.Row>
<Grid.Column width={4}>
<Image src={`${this.props.config.images.secure_base_url}original${this.props.movieDetails.poster_path}`} wrapped ui />
</Grid.Column>
<Grid.Column width={12}>
<div className="carousel-container">
<div className="swiper-container">
<h2 className="swiper-container__title">Cast</h2>
<div className="swiper-wrapper">
{this.props.movieCredits.cast.map(cast => {
return (
<div key={cast.cast_id} className="swiper-slide">
<Link>
{cast.profile_path ? <img className="swiper-slide__image" src={`${this.props.config.images.secure_base_url}w154${cast.profile_path}`} alt={cast.profile_path}/> : <img className="swiper-slide__image" src={placeholder} />}
<p className="swiper-slide__title">{cast.name}</p>
<p className="swiper-slide__details">as</p>
<p className="swiper-slide__details">{cast.character}</p>
</Link>
</div>
);
})}
</div>
it fails at the part where i use this.props.movieCredits.cast.map to iterate over fetched api call data. Note that the information inside this.props.movieCredits was called in the very same component as shown above,
const mapStateToProps = (state) => {
return {
config: state.PostMDBConfig,
apiKey: state.PostMDBConfig.apiKey,
moviesPopular: state.postMoviePopular,
tvPopular: state.postTVPopular,
itemType: state.setItemType.itemType,
movieGenres: state.getMovieGenre,
tvGenres: state.getTVGenre,
movieDetails: state.getMovieDetails,
movieCredits: state.getMovieCredits,
movieReviews: state.getMovieReviews,
movieVideos: state.getMovieVideos
}
}
const mapDispatchToProps = (dispatch) => {
return {
getMovieDetails: url => dispatch(getMovieDetails(url)),
getMovieCredits: url => dispatch(getMovieCredits(url)),
getMovieReviews: url => dispatch(getMovieReviews(url)),
getMovieVideos: url => dispatch(getMovieVideos(url))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ItemDetails);
Below is how I fetched the data
componentDidMount() {
this.fetchData(this.props.match.params.id);
}
fetchData(id, type = this.props.match.params.type) {
switch(type){
case 'movie':
this.props.getMovieDetails(`https://api.themoviedb.org/3/movie/${id}?api_key=${this.props.apiKey}&language=en-US`)
this.props.getMovieCredits(`https://api.themoviedb.org/3/movie/${id}/credits?api_key=${this.props.apiKey}`)
this.props.getMovieReviews(`https://api.themoviedb.org/3/movie/${id}/reviews?api_key=${this.props.apiKey}&language=en-US&page=1`)
this.props.getMovieVideos(`https://api.themoviedb.org/3/movie/${id}/videos?api_key=${this.props.apiKey}&language=en-US`)
break;
case 'tv':
break;
case 'people':
break;
default:
break;
}
}
Any help would be greatly appreciated, sorry about such a hassle.
EDIT!!! TLDR + IMPORTANT EDIT
After taking a break I think I've found the reason, being that this.match.params.id is not working, the question should be, how to get params from url when deployed
I believe this.match.params is not working, which causes the API fetch error, will be looking into a workaround for this.
EDIT
Solution ended up being using a different lifecycle method, not sure why it works in localhost but componentWillMount used instead fixed the problem of fetch not being fetched.
We can send data as props from parent component to child component easily but what to do in case of no relationship between two components ?
Please Read my context first so that I can make you understand my problem easily.
In my e commerce react App, when any new user visits my site I want to recommend the user to register. In registration process I keep two steps.
At first I want to take his mobile number from only one input field. when he fills the field and press 'continue' button -
I want to take him another page (component) where from I can take his name, password etc.
Here these two components are independent to each other. When user hits the 'continue' button I am calling a and redirect him to another component.
Now my question is How do I preserve this mobile number in 1st component and send to the 2nd component
[N.B: I don't want the mobile number be visible in the url when calling the 2nd Route]
First Component
export default class FirstComponent extends Component {
render() {
return (
<div>
<input type="text" placeholder="Type Mobile number" />
<Link to={{ pathname: '/signup' }}>
<button>Continue</button>
</Link>
</div>
)
}
}
Second Component
export default class SecondComponent extends Component {
render() {
return (
<div>
<input type="text" placeholder="Type Mobile number" />
</div>
)
}
}
Router
<Route exact path="/signup" render={(props) => {return <SecondComponent />} }/>
You can do it this way:
<Link to={{ pathname: '/signup', state: { number: 3199192910} }}>
<button>Continue</button>
</Link>
And you can access that in your signup component like:
import {withRouter} from 'react-router';
class SecondComponent extends Component {
render() {
// get the number
console.log(this.props.location.state.number)
return (
<div>
<input type="text" placeholder="Type Mobile number" />
</div>
)
}
}
export default withRouter(SecondComponent);
Check out this example, Quotes is what you should be looking at:
On click of button, you can call handleClick function and inside this function you could use push method to send props. This way you would have more control over what representation you want to send your data to other component.
<button onClick={this.handleClick}>continue</button>
handleClick = () => {
this.props.history.push({
pathname: '/signup',
state: { mobile: this.state.mobileNumber }
})
}
Hope that helps!!!
This is where Redux comes into the picture.
Redux is an open-source JavaScript library for managing application state.
So what happens in redux we having something called store which manages the whole state of the application. we dispatch some actions to the stores which calls a function called reducer which mutate the data in the store based on the action that has been dispatched. don't worry if you didn't understand what I said
just watch this 15 min video and you'll completely understand how to use Redux in your application and solve your problem https://www.youtube.com/watch?v=sX3KeP7v7Kg
in order to learn in depth I will recommend you go through the https://redux.js.org/basics/basic-tutorial
now coming back to your problem all you have to do is create a store which saves phone no and when the user clicks on continue button dispatch an action to the reduces to store the phone no and thus your phone no is persisted throughout the application and whenever you want to access the phone just write mapstatetoprops function which is shown in the video to access the data from the store and use it in that component
Hopefully, this will solve your problem
what happens when you don't use redux
of course, you send data as props but what will happen to the props when you refresh the page!!!! the props are lost but when we use redux we can save the data. And you're application works as expected even after refreshed of course they are many other ways to do it
I want to show a button when user is logged.If user is not logged then I m not showing button.When user logged i will set local storage values.when i set local storage in login Component,Header component must listen to that event and show the button.I m using addEventListener for listening.But its not listening.
I don't know where to listen in header Component.
// HeaderComponent(header.js):
class HeaderComponent extends Component {
componentDidMount(){
if(typeof window!='undefined'){
console.log(localStorage.getItem("token"));
window.addEventListener("storage",function(e){
this.setState({ auth: true});
})
}
}
render() {
return (
<div className="header">
<div className="container">
<div className="header-content">
<img src={logo} alt="logo"></img>
<div className="nav-links" >
<ul >
<li>Home</li>
<li>About</li>
<li>Services</li>
<li><NavLink activeClassName="active" to="/upload" >Upload</NavLink></li>
<li><NavLink activeClassName="active" to="/signup"> Sign Up</NavLink></li>
{ this.state.auth? <li onClick={this.onLogout}>Logout</li> :null}
</ul>
</div>
</div>
</div>
</div>
);
}
}
//loginComponent(login.js)
class LoginComponent extends Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit(event) {
const data = {
username: document.getElementById('name').value,
password: document.getElementById('password').value
}
axios.post(`http://localhost:4000/user/login`, data).then(res => {
this.props.history.push("/");
localStorage.setItem("token",res.data.token);
localStorage.setItem("auth",true);
}).catch(err => console.log(err));
}
render() {
return (
<section class="log-in">
<div class="card-col">
<form>
<h3>LOG IN</h3>
<div class="form-controls">
<input id="name" type="text" placeholder="username" class="input"></input>
</div>
<div class="form-controls">
<input id="password" type="password" placeholder="password" class="input"></input>
</div>
<button type="submit" onClick={this.onSubmit} class="button" >Log in</button>
</form>
</div>
</section>
)
}
}
The current answers are overlooking a really simple and secure option: window.dispatchEvent.
Where you set your localStorage item, if you dispatch an event at the same time then the eventListener in the same browser tab (no need to open another or mess with state) will also pick it up:
const handleLocalStorage = () => {
window.localStorage.setItem("isThisInLocalStorage", "true");
window.dispatchEvent(new Event("storage"));
};
window.addEventListener('storage', () => {
console.log("Change to local storage!");
// ...
})
EDIT:
Because this seems to be helpful, I'd also recommended checking out the useLocalStorage hook from the usehooks-ts team. You don't need to install it as a package; you can just copy the hook wholesale. This hook makes use of the solution I originally shared, but adds a whole lot more sophisticated logic to it.
Please take note of two things
storage event works only when the same application opened in two browser tabs (it is used to exchange info between different tabs of the same app). Storage event will not fire when both components shown on the same page.
When adding event listerner, you're passing function(), not array function. function() doe not capture this so you should explicitly bind(this) or change it to arrow function.
For example
window.addEventListener("storage",(function(e){
this.setState({ auth: true});
}).bind(this));
Or do with arrow function
window.addEventListener("storage",(e) => {
this.setState({ auth: true});
});
Here is simple example.
Be sure to open it in two tabs (the same link). Store value in one tab and see this value in another tab.
I found a really bad hack to accomplish this:
I have a Toolbar and a Login Component where the Toolbar component listens to changes in localStorage and displays the logged-in user name when the Login Component updates local storage if authentication is successful.
The Toolbar Component
(similar to the Header component in your case)
const [loggedInName, setLoggedInName] = useState(null);
useEffect(() => {
console.log("Toolbar hi from useEffect")
setLoggedInName(localStorage.getItem('name') || null)
window.addEventListener('storage', storageEventHandler, false);
}, []);
function storageEventHandler() {
console.log("hi from storageEventHandler")
setLoggedInName(localStorage.getItem('name') || null)
}
function testFunc() {
console.log("hi from test function")
storageEventHandler();
}
Add a hidden button to your Toolbar component. This hidden button will call the testFunc() function when clicked which will update the logged-in user's name as soon as local storage is updated.
<button style={{ display: 'none' }} onClick={testFunc} id="hiddenBtn">Hidden Button</button>
Now, in your Login component
.
.
.
//login was successful, update local storage
localStorage.setItem("name",someName)
//now click the hidden button using Javascript
document.getElementById("hiddenBtn").click();
.