Why am I losing my ReactJS state? - reactjs

I'm new to ReactJS, but I have a simple use case: I have a login form that sets the user's state (full name, etc.) and then I use the React Router to browserHistory.push('/') back to the home page. I can confirm that if I stay on the login page that my states actually get saved, however when I go back to the homepage, and into my "parent" component (App.js) and run this before the render method:
console.log(this.state) // this returns null
It always returns true. My constructor is not even doing anything with the state. If I put a log in the constructor on my App.js parent component I can verify that the page is not actually being reloaded, that the component is only mounted once (at least the constructor on App.js is only called once during the whole homepage > login > homepage lifecycle). Yet again, the state seems to be removed after changing pages.
What simple thing am I missing?
Edit: some code, trying to simplify it:
// LoginForm.js
//Relevant method
handleSubmit() {
this.login(this.state.username, this.state.password, (success) => {
if (!success)
{
this.setState({ isLoggedIn: false })
this.setState({ loginError: true })
return
}
this.setState({ isLoggedIn: true })
browserHistory.push('/') // I can verify it gets here and if at this point I console.log(this.isLoggedIn) I return true
})
}
// App.js
class App extends React.Component {
constructor(props) {
super(props);
console.log('hello')
}
render() {
const { props } = this
console.log(this.state) // returns null
return (
<div>
<AppBar style={{backgroundColor: '#455a64'}}
title="ADA Aware"
showMenuIconButton={false}>
<LoginBar/>
</AppBar>
<div>
{props.children}
</div>
</div>
)}
//Part of my routes.js
export default (
<Route path="/" component={App}>
<IndexRoute component={HomePage}/>
<Route path="/login" component={LoginForm}/>
<Route path="*" component={NotFoundPage}/>
</Route>
);

Where you call handleSubmit(), what component calls it?
If it is <LoginForm> or <LoginBar> or something like this, your this. means that component, non <App>.
To set parent's state (<App> in your case) you should pass to child a property with a handler function (for example onLogin={this.handleAppLogin.bind(this)}) so your child must call this prop (this.props.onLogin(true)) and handler in the <App> will set App's state: handleAppLogin(isLoggedIn) { this.setState({isLoggedIn}); }
But for the "global" state values such as login state, access tokens, usernames etc, you better shoud use Redux or some other Flux library.

This was a closed issue router project and discussed in many SO articles:
https://github.com/ReactTraining/react-router/issues/1094
https://github.com/reactjs/react-router-redux/issues/358
Redux router - how to replay state after refresh?
However, it is persistent real world need. I guess the pattern of how to handle this is based on Redux and/or browser storage.

Related

componentDidMount always called before react render

I am new to react world, I tried to fetch user data from axios call, and tried to get the data before the react's render executed.
How I call this component
<Route path="/users" render={(props) => <User {...props}/>} />
Here is my component class
class User extends React.Component {
constructor(props) {
super(props);
this.state = { authenticated: false }
this.getCurrentUser = this.getCurrentUser.bind(this);
}
componentDidMount(){
console.log("componentDidMount");
this.getCurrentUser();
}
getCurrentUser(){
Api.getCurrentUser().then(response => {
if (response) {
console.log(response.data.username);
this.setState({authenticated: true});
}
}).catch(error =>{
this.setState({authenticated: false});
}
}
render() {
console.log(this.state.authenticated);
if (this.state.authenticated === false) {
return <Redirect to={{ pathname: '/login' }}/>
}
return (
<div><Page /> <div>
);
}
}
export default User;
The console.log sequence is
false
componentDidMount
user_one
Warning: Can't perform a React state update on an unmounted component. This is a no-op
The warning makes sense because react already redirect me to login so the user component is not mounted.
I don't understand why componentDidMount is not called before render, because it supposes to change the defaultState through this.setState()...
What am I missing here?
ComponentDidMount works the way you described it. It runs immediately after the component is rendered. What you can do is to wrap your Component with a parent component where you have the API call and pass on the isAuthenticated as props to .
Docs for reference
As #user2079976 rightly stated, your usage of componentDidMount is correct & it behaves the way it is intended to, but I think you might be getting the wrong impression due to your code execution workflow.
Problem Reason
This issue/warning is generally something to go with when you're updating a component that has unmounted, in your case it's likely the redirect that happens before your api return a result.
More Details:
Not having the full code sample, I had to guess a few of the variables in your setup & I'm unable to get the exact issue on my JsFiddle as you've explained (I think JsFiddle/react.prod swallows the warning messages), but... I'll try to update this fiddle to explain it as much as I can with comments.
// at render this is still false as `state.authenticated`
// only becomes true after the redirect.
// the code then redirects....
// then updates the not yet mounted component & its state
// which is causing the warning
if (this.state.authenticated === false) {
return (<Redirect to={{ pathname: '/about' }}/>)
}
return (<div>On Home</div>);
Possible Solution
Rather do your auth/logged-in (state) to a higher level/parent component, and have the router decide where to send the user.
We have used this exact example in one of our apps (which is an implementation of the above suggestion). It works well for an auth type workflow & is straight from the docs of the Router lib you're using :P https://reactrouter.com/web/example/auth-workflow

My Login Form is showing even when I am already authenticated using React Routing?

I am using redux saga in my app, I have a login form and when I get authenticated I use the Redirect Component to move to the app, I do this of course after changing my connectedUser state, that is like that :
const initialState = {
loading: false,
user: {},
status: ""
};
When authenticating my state change to status:"AUTH" and user: { // user data }, And this is how I redirect to the Application component and this is how my Authentication component rendering method looks like :
render() {
if (this.props.connectedUser.status === "AUTH") {
return <Redirect to="/" />;
}
return MyLoginForm
}
My routes are defined in the whole app container :
function App(props) {
return (
<ThemeProvider theme={theme}>
<I18nProvider locale={props.language.language}>
<BrowserRouter>
<Switch>
<Route exact path="/" component={Application}></Route>
<Route path="/authenticate" component={Authentification}></Route>
</Switch>
</BrowserRouter>
</I18nProvider>
</ThemeProvider>
);
}
const msp = state => ({ language: state.language });
export default connect(msp, null)(App);
This is working fine, the disconnect button is doing fine too, but the problem occurs when I am redirected to my app ( meaning this route "/" ).
I want that when I refresh the page, I don't get redirected to the authentication page, Why I am going there ? it is obvious that my state is the initial state again, the status is "" again.
I am not asking for code but an idea, how to implement this.
I was trying to put that status in my localStorage but I don't feel like it is a good practise, also I found a problem whith this because If I change the localStorage then I will have to re render the component so the condition can be verified.
I want that when I refresh the page, I don't get redirected to the authentification page, Why I am going there ? it is obvious that my state is the initial state again, the satus is "" again.
Sure! Refresh page will initial state again.
First of all, you need store current authentification after signed in. I suggest you use the redux-sessionstorage
Next, in Authentication component you should dispatch an action to get current authentification and update status in your state.

React. How to redirect early in a component lifecycle

This seems so simple, but I am new to react and trying different approaches and nothing is working for me. (BTW I am a regular contributor but working on my clients machine and can't remember my so password.)
The version of react router is 4.0, and state is stored using redux.
The scenario is that we are changing the order of routing in our application and would like to redirect any users that have Urls with the old structure to the new Url structure. I have tried the following (also note that I have "scrubbed" the names of the page, function calls and variables):
There is a trigger component for the section I need to direct from, with routing set up like this:
<Route path='page/:pageGuidGuid' component={PageTrigger}> </Route>
In the trigger component, ComponentWillMount makes a request that returns a 404 if the link is from the previous component, although it does redirect to the correct route. I am checking for and triggering the redirect in getInitialState, but the component keeps going through the lifecycle and ComponentWillMount is called, resulting in a 404 and then redirects to the correct page.
const PageTrigger = connectToStores(React.createClass({
getInitialState() {
this.checkForRedirect();
return {};
},
componentWillMount() {
if (this.props.params.guid) {
this.setCaseByGuid(this.props.params.guid);
}
},
checkFrorRedirect() {
/* logic to determine if it should redirect */
browserHistory.push(redirectUrl);
}
}
I have also tried creating a custom route...
<CaseRedirectRoute path='(cases/:caseGuid)' component={CaseTrigger}>
</CaseRedirectRoute>
And in a separate module (based off a login sample)
const CaseRedirectRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
checkForCaseRedirect(...props) ? (
<Redirect to={{
pathname: getCaseUrlRedirectUrl(...props),
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
I have WithRouter imported from react router. When I try to run this I am getting an error inside the react framework:
Uncaught Type Error: Cannot read property 'createRouteFromReactElement' of undefined
at RouteUtils.js:68
at forEachSingleChild (ReactChildren.js:52)
at traverseAllChildrenImpl (traverseAllChildren.js:98)
at traverseAllChildrenImpl (traverseAllChildren.js:114)
at traverseAllChildren (traverseAllChildren.js:186)
at Object.forEachChildren [as forEach] (ReactChildren.js:70)
at createRoutesFromReactChildren (RouteUtils.js:65)
at Function.createRouteFromReactElement (RouteUtils.js:35)
at RouteUtils.js:69
at forEachSingleChild (ReactChildren.js:52)
I've tried redirecting from app.js and page.js, but the PageTrigger is the first component having state set. I either need to figure out how to stop execution after the redirect, or figure out why my custom route keeps blowing up. Any help would be appreciated.
I'm not sure about your setup, but I would implement this redirect as this:
<Route path='oldurl/:p1/:p2' render={
routeProps => (
<Redirect to={'/newUrlHere/'+routeProps.match.params.p1} />
)
} />
So, if the route matches (you may specify exact match if necessary), when the Redirect "renders" it stops rendering and should start the rendering from the new URL. You may build your URL in the Redirect.to property as you wish

Protected Routes React Router 4 not working with auth state stored in Redux

I am trying to make an authenticated route in React Router v4 as per this example. Showing the code for posterity:
function PrivateRoute ({component: Component, authed, ...rest}) {
return (
<Route
{...rest}
render={(props) => (!!authed)
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}} />}
/>
)
}
My authentication state (authed), which is initialized as an empty object at the reducer, is derived from a Redux store. This is how my App.js looks like:
class App extends Component {
componentDidMount() {
const token = localStorage.getItem("token");
if (token) {
this.props.fetchUser();
}
}
render() {
return (
<Router>
<div>
<PrivateRoute authed={this.props.authed} path='/dashboard' component={Dashboard} />
/>
</div>
</Router>
);
}
}
The problem is that the authed state starts as undefined and then, once the Router component is mounted, it updates the state to true. This is however a bit late, because the user would be already redirected back to the login page. I also tried to replace the componentDidMount() lifecycle method, with the componentWillMount() but that did not fix the problem either.
What strategies would you suggest?
UPDATE 1: The only way I get around this is by testing for the authed state before returning the <Route /> component such as this:
render() {
if (!!this.props.authed) {
return (
<Router>
<div>
...
UPDATE 2: I am using Redux Thunk middleware to dispatch the action. The state is being passed as props correctly - I am using console.log() methods inside the PrivateRoute component to verify that the state mutates correctly. The problem is of course that it is mutating late, and the Route is already redirecting the user.
Pasting code of reducer and action...
Action:
export const fetchUser = () => async dispatch => {
dispatch({ type: FETCHING_USER });
try {
const res = await axios.get(`${API_URL}/api/current_user`, {
headers: { authorization: localStorage.getItem("token") }
});
dispatch({ type: FETCH_USER, payload: res.data });
} catch (err) {
// dispatch error action types
}
};
Reducer:
const initialState = {
authed: {},
isFetching: false
};
...
case FETCH_USER: // user authenticated
return { ...state, isFetching: false, authed: action.payload };
I had the same problem and from my understanding your update #1 should be the answer. However upon further investigation I believe this is an architectural problem. The current implementation of your Private route is dependent on the information being synchronous.
If we think about it pragmatically the ProtectedRoute essentially returns either a redirect or the component based on the state of our application. Instead of wrapping each Route with a component we can instead wrap all the routes in a component and extract our information from the store.
Yes it is more code to write per protected route, and you'll need to test if this is a viable solution.
Edit: Forgot to mention another big reason this is an architectural problem is if the user refreshes a page which is protected they will be redirected.
UPDATE
Better solution:
On refresh if they are authenticated it will redirect to their target uri
https://tylermcginnis.com/react-router-protected-routes-authentication/
Solution 1
//You can make this a method instead, that way we don't need to pass authed as an argument
function Protected(authed, Component, props) {
return !!authed
? <Component {...props} />
: <Redirect to='/login' />
}
class AppRouter extends React.PureComponent {
componentDidMount() {
const token = localStorage.getItem("token");
if (token) {
this.props.fetchUser();
}
}
render() {
let authed = this.props.authed
return (
<Router>
<Route path='/protected' render={(props) => Protected(authed, Component, props)} />
</Router>
)
}
}
class App extends Component {
render() {
return (
<Provider store={store}>
<AppRouter />
</Provider>
)
}
}
Solution 2
Or we can just check for each component (yes it's a pain)
class Component extends React.Component {
render() {
return (
!!this.props.authed
? <div>...</div>
: <Redirect to='/' />
)
}
}
The same problem was happening with me, I am using a temporary hack to solve it by storing an encrypted value inside localStorage and then decrypting it in my PrivateRoute component and checking if the value matches.
action.js
localStorage.setItem('isAuthenticated', encryptedText);
PrivateRoute.js
if (localStorage.getItem('isAuthenticated')) {
const isAuth = decryptedText === my_value;
return (
<Route
{...rest}
render={(props) =>
isAuth ? <Component {...props} /> : <Redirect to="/login" />
}
/>
);
} else {
return <Redirect to="/login" />;
}
Since localStorage is faster, so it is not unnecessarily redirecting. If someone deletes localStorage they will simply be redirected to /login
Note: It is a temporary solution.
Theoretically, you need to get a promise from the NODE API call which you are not getting right now. You need to make architectural changes. I suggest you use redux-promise-middleware this is a redux middleware. I have a sample project in my github account. Where you will get notified if your call to this.props.fetchUser() is completed or not, based on that using Promise you can handle this async problem you have. Go to that repo if need help ask me.
if your App component is connected to the redux store, you probably mean this.props.authed instead of this.state.authed
My authentication state (authed), which is initialized as an empty
object at the reducer, is derived from a Redux store
So you are comparing empty object with true here: (props) => authed === true? Why don't you initialize it with a false?
And are you sure the action this.props.fetchUser is switching the state to true?
Maybe you better also post your action and reducer file

Infinite componentDidUpdate() calls with react-router

I am new to react-router and right now I have following routes in my app:
<Router history={browserHistory}>
<Route path="/" component={MainLayout}>
<Route path="/v/:username/:reponame/:page/:perPage" component={Results} />
</Route>
</Router>
As you can see, there's a MainLayout component that includes an <input type="text"> which is used to connect to Github API and retrieve list of issues for a certain repo.
Then the Results component steps in. Here's the code for it's componentDidMount():
componentDidMount() {
const {
username,
reponame,
page,
perPage
} = this.props.params;
this.sendRequest(username, reponame, page, perPage);
}
sendRequests essentially contains the ajax query for fetching the output data, after which it's being set into the component's state:
this.state = {
data: [], // here
lastPage: -1,
errorMessage: ''
}
Now this works pretty well till the very moment when one wants to change the value of an input.
As I see, the Result component doesn't unmount. Instead, it invokes componentWillReceiveProps() and updates the existing component. AFAIK it is safe to perform side calls in componentDidUpdate() so I just copied the code above from componentDidMount() and pasted it in there. This way, though (and it is absolutely reasonable) componentDidMount() is being invoked over and over again.
The only workaround I came up with at the moment is comparing old and new data in the sendRequest() itself and invoke setState() inside of it only if it differs via deep-equal package, so it looks like this:
if (!equal(res, this.state.data)) {
this.setState({
...this.state,
data: res,
lastPage
});
}
Is this considered to be an ok pattern or there is a better way to solve this issue?
You should not use setState inside the cDM lifecycle. as it might trigger re-render, which will cause your infinite loop.
Updating the state after a component mount will trigger a second render() call and can lead to property/layout thrashing.
https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-mount-set-state.md

Resources