Keeping State After Component Unmounts - reactjs

I have an Auth component which is responsible for both login & sign up. I simply receive a prop (isSignup) and display the appropriate form fields. I also use react-router:
<BrowserRouter>
<Route
path="/signup" exact
render={() => <Auth isSignup />} />
<Route
path="/login" exact
render={() => <Auth />} />
</BrowserRouter>
In the Auth component I have a state which holds the values of the form fields.
I'd like to keep the state of Auth after it unmounts, i.e when react-router no longer renders it, so I can keep the values when the user switches between /signup and /login.
Is there a way to do this without global state management (e.g Redux)?

If you don't want to use Redux for this purpose, but you still want to share this specific state across your app, then there are a few ways to go about it. If you want to stay "Reacty", then you really have 2 options:
Pass state as props
Use a Provider
If you just want something that works but isn't necessarily the React way, you have more options (like setting window.isLoggedIn and toggling it at will... which I don't recommend).
For this answer, we'll focus on the "Reacty" options.
Pass state as props
This is the default approach with React. Track your isLoggedIn variable somewhere at the root of your component tree, then pass it down to your other components via props.
class App extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
render() {
return (
<BrowserRouter>
<Route
path="/signup"
exact
render={() => (
<Auth
isSignup
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
<Route
path="/login"
exact
render={() => (
<Auth
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
</BrowserRouter>
)
}
}
This works but gets tedious as you need to pass isLoggedIn, logIn, and logOut to almost every component.
Use a Provider
A Provider is a React component that passes data to all its descendants via context.
class AuthProvider extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
getChildContext() {
return {
isLoggedIn: this.state.isLoggedIn,
logIn: this.logIn,
logOut: this.logOut
}
}
static childContextTypes = {
isLoggedIn: PropTypes.bool,
logIn: PropTypes.func,
logOut: PropTypes.func
}
render() {
return (
<>
{this.props.children}
</>
)
}
}
Then, you initialize your app by wrapping the whole thing in AuthProvider:
<AuthProvider>
<BrowserRouter>
// routes...
</BrowserRouter>
</AuthProvider>
And in any component of your app, you will be able to access isLoggedIn, logIn, and logOut via this.context.
You can read more about the Provider pattern and how it works in this excellent article from Robin Wieruch.
notes:
It's common for components to receive context as props using a higher order component, but I left that out of the answer for brevity. Robin's article goes into this further.
You may recognize the Provider pattern as one that react-redux and other state management libraries utilize. It's fairly ubiquitous at this point.

You could create a wrapper which stores the state above the components:
class AuthWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return ([
<Route
key="signup"
path="/signup" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
isSignup
/>
</Route>,
<Route
key="login"
path="/login" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
/>
</Route>
]);
}
}
Then this component can be used, which stores the state over those routes.
<BrowserRouter>
<AuthWrapper />
</BrowserRouter>
You would just need to use the state and setState in the params instead in the Auth component.

Related

Redux how to get updated store state in root component

My root App component looks like this:
class App extends Component {
render() {
const state = store.getState();
const { isAuthenticated } = state.auth;
return (
<Provider store={store}>
<BrowserRouter basename="/">
<Switch>
{isAuthenticated ? (
<PrivateRoute
exact
path="/library/:user_id"
component={RessourcesByUser}
/>
) : (
<PublicRoute
exact
path="/library/:user_id"
component={PublicRessourcesByUserPage}
/>
)}
</Switch>
</BrowserRouter>
</Provider>
);
}
}
The problem with isAuthenticated is that when the app is first loaded before the user logs in its value is false.
And it makes sense since the user has not logged in yet.
The problem is that when he logs in, the App component does not mount again (obviously), nor does it get the updated store state in which isAuthenticated is true.
So isAuthenticated here in App component will remain false even though the user is authenticated and its value in the store is true.
It will change to true here once he refreshes because App component will get the updated state then in which isAuthenticated is true.
However, meanwhile this will cause logical bugs such as when he goes to a user's Library right after loading the app and logging-in the first time by clicking on a button that would direct to this path /library/:user_id, he will see the public component PublicRessourcesByUserPage that is meant to be displayed for non-authenticated users instead of RessourcesByUser which is meant for authenticated users.
It's been a while since I've used react-router, but if you want to get the freshest (reactive) state with Redux, then your component will need to be connected to Redux. So something along these lines:
import { connect } from 'react-redux';
class AuthRoutes extends Component {
render() {
return (
<Switch>
{this.props.isAuthenticated ? (
<PrivateRoute
exact
path="/library/:user_id"
component={RessourcesByUser}
/>
) : (
<PublicRoute
exact
path="/library/:user_id"
component={PublicRessourcesByUserPage}
/>
)}
</Switch>
)
}
}
connect((state) => ({ isAuthenticated: state.isAuthenticated }),null)(AuthRoutes);
---
class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter basename="/">
<AuthRoutes />
</BrowserRouter>
</Provider>
);
}
}
Note: I'm making an assumption that Switch will work OK like this.

react.js redirect to view

i want redirect to "/user". i write but this not work.
how to correctly redirect to the right page
onClick = (e) => {
this.setState({ errorLoad: false});
getPlayerInfo(this.state.id).then(data => {
if(data.success == false) {
this.setState({ errorLoad: true});
return;
}
this.setState({ user: data.player});
console.log(data);
<Redirect to="/user"/>
});
}
My router list. Among them there is a router with the path "/ user"
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
UPADATE
App.js
The button I click on is in the component <SearchForm/>
render() {
let style = {marginLeft: '20px'};
return (
<div>
<Header source='https://www.shareicon.net/data/2017/02/15/878753_media_512x512.png'/>
<SearchForm onClick={this.onClick} style={style} onChange={this.onHandle} placeholder="search"/>
<Centered style={ {marginTop: '50px'} }>
<BrowserRouter>
<Switch>
<Route exact path='/' component={Startup} />
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
</Switch>
</BrowserRouter>
</Centered>
</div>
);
}
There are two ways to programmatically navigate with React Router - <Redirect /> and history.push. Which you use is mostly up to you and your specific use case.
<Redirect /> should be used in user event -> state change -> re-render order.
The downsides to this approach is that you need to create a new property on the component’s state in order to know when to render the Redirect. That’s valid, but again, that’s pretty much the whole point of React - state changes update the UI.
The real work horse of React Router is the History library. Under the hood it’s what’s keeping track of session history for React Router. When a component is rendered by React Router, that component is passed three different props: location, match, and history. This history prop comes from the History library and has a ton of fancy properties on it related to routing. In this case, the one we’re interested is history.push. What it does is it pushes a new entry onto the history stack - aka redirecting the user to another route.
You need to use this.props.history to manually redirect:
onClick = (e) => {
this.setState({ errorLoad: false});
getPlayerInfo(this.state.id).then(data => {
if(data.success == false) {
this.setState({ errorLoad: true});
return;
}
this.setState({ user: data.player});
console.log(data);
this.props.history.push('/user');
});
}
You should be getting history as a prop from your <Router> component.
EDIT:
Okay thank you for the code update. The SearchForm component is not nested under your BrowserRouter, so it is not getting the history prop. Either move that component inside the BrowserRouter or use the withRouter HOC in SearchForm reacttraining.com/react-router/web/api/withRouter
Option 1: Move SearchForm inside the BrowserRouter
render() {
let style = {marginLeft: '20px'};
return (
<div>
<Header source='https://www.shareicon.net/data/2017/02/15/878753_media_512x512.png'/>
<Centered style={ {marginTop: '50px'} }>
<BrowserRouter>
<SearchForm onClick={this.onClick} style={style} onChange={this.onHandle} placeholder="search"/>
<Switch>
<Route exact path='/' component={Startup} />
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
</Switch>
</BrowserRouter>
</Centered>
</div>
);
}
Option 2: use the withRouter HOC to inject the history prop into SearchForm manually:
import { withRouter } from 'react-router-dom';
class SearchForm extends React.Component { ... }
export default withRouter(SearchForm)

React router private routes / redirect not working

I have slightly adjusted the React Router example for the private routes to play nice with Redux, but no components are rendered when Linking or Redirecting to other 'pages'. The example can be found here:
https://reacttraining.com/react-router/web/example/auth-workflow
Their PrivateRoute component looks like this:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)
But, because I have incorporated it in a Redux application, I had to adjust the PrivateRoute a little so I can access the redux store, as well as the route Props:
const PrivateRouteComponent = (props) => (
<Route {...props.routeProps} render={() => (
props.logged_in ? (
<div>{props.children}</div>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}} /> )
)} />
);
const mapStateToProps = (state, ownProps) => {
return {
logged_in: state.auth.logged_in,
location: ownProps.path,
routeProps: {
exact: ownProps.exact,
path: ownProps.path
}
};
};
const PrivateRoute = connect(mapStateToProps, null)(PrivateRouteComponent);
export default PrivateRoute
Whenever I'm not logged in and hit a PrivateRoute, I'm correctly redirected to /login. However, after for instance logging in and using a <Redirect .../>, or clicking any <Link ...> to a PrivateRoute, the URI updates, but the view doesn't. It stays on the same component.
What am I doing wrong?
Just to complete the picture, in the app's index.js there is some irrelevant stuff, and the routes are set up like this:
ReactDOM.render(
<Provider store={store}>
<App>
<Router>
<div>
<PrivateRoute exact path="/"><Home /></PrivateRoute>
// ... other private routes
<Route path="/login" component={Login} />
</div>
</Router>
</App>
</Provider>,
document.getElementById('root')
);
You need to wrap your Route with <Switch> tag
ReactDOM.render(
<Provider store={store}>
<App>
<Router>
<div>
<Switch>
<PrivateRoute exact path="/"><Home /></PrivateRoute>
// ... other private routes
<Route path="/login" component={Login} />
</Switch>
</div>
</Router>
</App>
</Provider>,
document.getElementById('root'));
Set the private route to not be pure:
export default connect(mapStateToProps, null, null, {
pure: false,
})(PrivateRoute);
This will let the Component re-render.
Please see: react-router-4-x-privateroute-not-working-after-connecting-to-redux.
Just had same problem, I solved it by making my App redux container and passing isAuthenticated as a prop to PrivateRoute
Here it is, I hope it helps
const App = (props) => {
return (
<Provider store={store}>
<Router>
<div>
<PrivateRoute path="/secured" component={Secured} isAuthenticated={props.isAuthenticated} />
</div>
</Router>
</Provider>
);
};
const mapStateToProps = state => ({
isAuthenticated: state.isAuthenticated
});
export default connect(mapStateToProps)(App);
Then in my PrivateRoute
const PrivateRoute = ({ component: Component, isAuthenticated, ...rest}) => (
<Route
{...rest}
render={props => (
isAuthenticated
? (
<Component {...props} />
)
: (<Redirect to={{ pathname: '/login', state: { from: props.location} }} />)
)}
/>
);
export default PrivateRoute;
I managed to get this working using the rest parameter to access the data from mapStateToProps:
const PrivateRoute = ({component: Component, ...rest}) => {
const {isAuthenticated} = rest;
return (
<Route {...rest} render={props => (
isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: {from: props.location}
}}/>
)
)}
/>
);
};
PrivateRoute.propTypes = {
isAuthenticated: PropTypes.bool.isRequired,
};
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
};
}
export default connect(mapStateToProps)(PrivateRoute);
Well, I think the answer to this question should really be more detailed, so here I am, after 4 hours of digging.
When you wrap your component with connect(), React Redux implements shouldComponentUpdate (sCU if you search answers on github issues) on it and do shallow comparison on props (it goes throug each key in the props object and check if values are identical with '==='). What it means in practice is that your component is considered Pure. It will now change only when its props change and only then! This is the key message here.
Second key message, React router works with context to pass the location, match and history objects from Router to Route component. It doesn't use props.
Now let's see in practice what happen, because even knowing that, I find it still pretty tricky:
Case 1:
There is 3 key to your props after connecting: path, component, and auth (given by connect). So, in fact, your wrapped component will NOT re-render at all on route changes because it doesn't care. When route changes, your props don't change and it will not update.
Case 3:
Now there is 4 keys to your props after connecting: path, component, auth and anyprop. The trick here is that anyprop is an object that is created each time the component is called. So whenever your component is called this comparison is made: {a:1} === {a:1}, which (you can try) gives you false, so your component now updates every single time. Note however that your component still doesn't care about the route, its children do.
Case 2:
Now that's the mystery here, because i guess you call this line in your App file, and there should be no reference to "auth" in there, and you should have an error (at least that's what i am guetting). My guess is that "auth" in your App file references an object defined there.
Now what should we do ?
I see two options here:
Tell React Redux that your component is not pure, this will remove the sCU injection and your component will now correctly update.
connect(mapStateToProps, null, null, {
pure: false
})(PrivateRoute)
Use WithRouter(), which will results in injecting the location, match and history object to your component as props. Now, I don't know the internals, but i suspect React router doesn't mutate those objects so each time the route change, its props change (sCU returns true) as well and your component correctly updates. The side effect of this is that your React tree is now polluted with a lot of WithRouter and Route stuff...
Reference to the github issue: Dealing with Update Blocking
You can see here that withRouter is intended as a quickfix but not a recommended solution. using pure:false is not mentionned so I don't know how good this fix could be.
I found a 3rd solution, though it's unclear to me if it is really a better solution than withRouter, using Higher Order Component. You connect your Higher-order Component to the Redux store, and now your route doesn't give a damn about what it renders, the HOC deals with it.
import Notlogged from "./Notlogged";
function Isloggedhoc({ wrap: Component, islogged, ...props }) {
return islogged ? <Component {...props} /> : <Notlogged {...props} />;
}
const mapState = (state, ownprops) => ({
islogged: state.logged,
...ownprops
});
export default connect(mapState)(Isloggedhoc);
in your App.js
<Route path="/PrivateRoute" render={props => <Isloadedhoc wrap={Mycomponent} />} />
You could even make a curried function to shorten it a bit:
function isLogged(component) {
return function(props) {
return <Isloadedhoc wrap={component} {...props} />;
};
}
Using it like that:
<Route path="/PrivateRoute" render={isLogged(Mycomponent)} />
I have struggled with this issue as well, and here is my solution.
Instead of passing isAuthenticated to every < PrivateRoute> component, you just need to get isAuthenticated from state in < PrivateRoute> itself.
import React from 'react';
import {Route, Redirect, withRouter} from 'react-router-dom';
import {connect} from 'react-redux';
// isAuthenticated is passed as prop here
const PrivateRoute = ({component: Component, isAuthenticated , ...rest}) => {
return <Route
{...rest}
render={
props => {
return isAuthenticated ?
(
<Component {...props} />
)
:
(
<Redirect
to={{
pathname: "/login",
state: {from: props.location}
}}
/>
)
}
}
/>
};
const mapStateToProps = state => (
{
// isAuthenticated value is get from here
isAuthenticated : state.auth.isAuthenticated
}
);
export default withRouter(connect(
mapStateToProps, null, null, {pure: false}
)(PrivateRoute));
Typescript
If you are looking for a solution for Typescript, then I made it work this way,
const PrivateRoute = ({ component: Component, ...rest }: any) => (
<Route
{...rest}
render={props =>
localStorage.getItem("authToken") ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
<Router>
<Switch>
<PrivateRoute exact path="/home" component={Home} />
<Route path="/login" component={Login} />
</Switch>
</Router>
Just in case you want to go by creating a class then something like this,
class PrivateRoute extends Route {
render() {
if (localStorage.getItem("authToken")) {
return <Route {...this.props} />
} else {
return <Redirect
to={{
pathname: "/login",
state: { from: this.props.location }
}}
/>
}
}
}
According to react-router documentation you may just wrap your connect function with withRouter:
// before
export default connect(mapStateToProps)(Something)
// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))
This worked for me and my views started to be updated along with routes in this case.
I have the similar issue like #Rein. In my case, PrivateRoute looks almost same to the original version but only connected to Redux and used it instead of fakeAuth in the original example.
const PrivateRoute = ({ component: Component, auth, ...rest }) => (
<Route
{...rest}
render={props =>
auth.isAuthenticated
? <Component {...props} />
: <Redirect to={{ pathname: "/login" }} />}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired,
component: PropTypes.func.isRequired
}
const mapStateToProps = (state, ownProps) => {
return {
auth: state.auth
}
};
export default connect(mapStateToProps)(PrivateRoute);
Usage and result:-
NOT working but expecting to work
<PrivateRoute path="/member" component={MemberPage} />
working but NOT desired to used like this
<PrivateRoute path="/member" component={MemberPage} auth={auth} />
working. JUST to work but NOT desired to used at all. An understanding from this point is that, if you connect original PrivateRoute to Redux, you need to pass some additional props (any prop)to make PrivateRoute working otherwise it does not work. Anyone, please give some hint on this behavior. This is my main concern. As a New Question at
<PrivateRoute path="/member" component={MemberPage} anyprop={{a:1}} />

React-Router: Update route's component on new props

App.js
import React, { Component } from 'react';
import { Router, Route, browserHistory } from 'react-router';
import Login from './Login';
import Courses from './Courses';
class App extends Component {
constructor() {
super();
this.state = {
username: '',
password: ''
};
}
setCredentials = ({ username, password }) => {
this.setState({
username,
password
});
}
render() {
return (
<Router history={browserHistory}>
<Route
path='/'
component={Login}
setCredentials={this.setCredentials}
/>
<Route
path='/courses'
component={Courses}
credentials={this.state}
/>
</Router>
);
}
}
The Login component takes in credentials from user, verifies them via an API call, updates App's state with them, and then redirects to the Courses component using this.props.router.push('/courses').
The Courses component should take in the updated credentials (i.e. App's updated state) as props, and afterwards perform an API call to fetch that user's courses.
How can I detect the updates in App's state inside the Courses component? Am I doing it wrong altogether?
Pass props to route's component correctly:
<Route
path='/'
component={(props) => <Login setCredentials={this.setCredentials} {...props} />}
/>
and NOT:
<Route
path='/'
component={Login}
setCredentials={this.setCredentials}
/>
The same for the Courses component, you pass extra-props the same way:
<Route
path='/courses'
component={(props) => <Courses credentials={this.state} {...props} />}
/>
And NOT:
<Route
path='/courses'
component={Courses}
credentials={this.state}
/>
Which answers the question:
How can I detect the updates in App's state inside the Courses component?
since crendentials becomes a prop of Courses and its value state of App.
As you said with the courses component reading from props you could have a lifecycle hook like so in the courses component:
componentWillReceiveProps(nextProps) {
if(nextProps.isAuthenticated && this.props.credentials !== nextProps.credentials) {
// perform fetch for courses here
}
}

react-router : share state between Routes without Redux

I would like to have a shared state (list of clients fetched remotely) between 2 sibling Routes : Timesheets and Clients.
I want to try how far i can go with 'pure' React (No Flux architecture).
This example works, but I have an error : browser.js:49 Warning: [react-router] You cannot change <Router routes>; it will be ignored
So, it doesn't seem to like async props.
constructor(props) {
super(props);
this.state = {
clients : []
}
}
componentDidMount() {
fetch("clients.json")
.then(response => response.json())
.then(clients => this.setState({ clients }));
}
render() {
return (
<Router history={browserHistory}>
<Route path="/" component={Header} >
<Route path="timesheet" component={() => (<Timesheets {...this.state} />) }/>
<Route path="clients" component={() => (<Clients {...this.state} />) }/>
</Route>
</Router>
);
}
Is it possible to send async props down to each route?
Or is it possible to set the whole state in the parent route (Header component) and then access this state from each child route (Timesheets and Clients components)?
You can use an high-order component to fetch and inject data to your top level component. Then you can pass props to sub routes via React.cloneElement.
HOC
const FetchHOC = url => Component => class extends React.Component() {
state = { data: null };
componentDidMount() {
fetch(url).then(data => this.setState({ data }));
}
render() {
return <Component {...this.props} {...this.state.data} />;
}
}
Route configuration
<Route path="/" component={FetchHOC('http://some-url')(App)}>
<Route path="subroute1" component={SubRoute1} />
<Route path="subroute2" component={SubRoute2} />
</Route>
Parent route render()
<div className="App">
{this.props.children && React.cloneElement(this.props.children, {
data: this.props.data,
// ...
})}
</div>
You can take a look at reacts Context. Redux also makes use of Context. It allows you to pass some data down to all children. But you should write code to use this data, for instance you have determine contextTypes etc.
You can see details on docs about how to use it.

Resources