React warning about setState in unmounted component - reactjs

I'm getting this error:
warning.js:33 Warning: Can't call setState (or forceUpdate) on an
unmounted component. This is a no-op, but it indicates a memory leak
in your application. To fix, cancel all subscriptions and asynchronous
tasks in the componentWillUnmount method.
But I'm not using a componentWillUnMount method.
I'm using a HOC to make sure the user is authenticated before accessing their /account route.
Here's the Route:
<StyleRoute props={this.props} path="/account" component=
{RequireAuth(Account)} />
where RequireAuth is the HOC. Here's the HOC:
import { withRouter } from 'react-router';
export default function RequireAuth(Component) {
return class AuthenticatedComponent extends React.Component {
componentWillMount() {
this.checkAuth();
}
checkAuth() {
if ( ! this.props.isAuthenticated) {
this.props.history.push(`/`);
}
}
render() {
return this.props.isAuthenticated
? <Component { ...this.props } />
: null;
}
}
return withRouter(AuthenticatedComponent);
}
The code works as intended, but I'm getting that error when /account is rendered. As you notice, nowhere in my direct code is there an componentWillUnMount method. I'm really at a loss for why this warning keeps popping up and any info would help.
Update 5/23/18:
To get rid of the error and still have props pass down, I did two thing:
1) I opted for a having two higher order functions in parent App component instead of using the HOC. One higher order function is for passing props and the other is to check authentication. I was having trouble passing any props other than the browser history, hence the renderProps function below.
renderProps = (Component, props) => {
return (
<Component {...props} />
);
}
checkAuth = (Component, props) => {
if (props.isAuthenticated) {
return <Component {...props} />
}
if (!props.isAuthenticated) {
return <Redirect to='/' />
}
}
2) To use these, I had to user render in my Route, as opposed to component.
//I could pass props doing this, sending them through the above functions
<Route exact path="/sitter-dashboard" render={ () => this.checkAuth(SitterDashboard, this.props) } />
<Route exact path={"/account/user"} render={() => this.renderProps(User, this.props)} />
//I couldn't pass props doing this
<Route {...this.props} exact path="/messages" component={Messages} />
Here's the documentation on router vs component as a Route render method: https://reacttraining.com/react-router/web/api/Route/route-render-methods
Also, here's a good explanation on Stack Overflow
Finally, I used this code from the React Router 4 documentation as a template for what I did above. I'm sure the below is cleaner, but I'm still learning and what I did makes a bit more sense to me.
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);

I had the same error time ago and it was generated by a component which was using ref tag, and there was some manual manipulation.
A good practice to see these kind of errors is drawing your app flow and see when your are calling setState.
Another thing I would change if I were you is componentDidMount instead of componentWillMount to check some data. Take into account fb deprecated this functionality.
This lifecycle was previously named componentWillMount. That name will continue to work until version 17. Use the rename-unsafe-lifecycles codemod to automatically update your components.
Reactjs component documentation

I had a similar problem, but I did figure out the reason behind the same, so here is the snippet of code where I was encountering this err.
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
Cause:
this.setState({ showLoader: true });
const { username } = this.state;
const URL = `https://api.github.com/users/${username}`;
try {
const { data } = await axios(URL);
this.props.apiData(data);
this.props.history.push("profile");
} catch (e) {
console.error(e);
}
this.setState({ showLoader: false });
As you can see in the code-snippet, I was doing
this.props.history.push("profile");
before setting the state.
this.setState({ showLoader: false });
And then err seems to be legit in this case as I was redirecting to a different component and then setting the state on the component I was earlier.
Solution:
By placing
this.setState({ showLoader: false });
above the this.props.history.push("profile"); solved the problem.
I hope this helps.

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

Passing props through react router not available in componentDidMount?

I've passed props via the react router like this to the mentioned component:
<BrowserRouter>
<Switch>
<Route path="/product-page" exact render={(props) => ( <ShopPages {...props} response={this.state.response}/>)}/>
</Switch>
</BrowserRouter>
I would like to access this props in componentDidMount() like something like this:
componentDidMount() {
console.log(this.props.response)
}
The prop this.props.response is available in the component I can console.log it in the render(). I'm not sure why in the above scenario the console shows the array empty. I've tried to see if the data is available then show the data as so console.log( 'show', this.props.response && this.props.response) or by adding async as so:
async componentDidMount() {
const data = await this.props.response
}
But this does nothing too. Any help?
I'm just going to assume that this.state.response is not a Promise, but is actually the data since you said it was an empty array, therefore async/await will do nothing.
ComponentDidMount() is only fired once when the component mounts and is NOT the place to perform actions based on prop updates that can happen after mount. So I would suggest passing the promise, and if that is not an option do something like
componentDidUpdate(prevProps, prevState) {
if (!prevProps.response.length && this.props.response.length) {
//Your code
}
}

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

On url change I want to re render my component how should I do that

I have two links that fetches the sample component but when I click on any one of them it gets loaded and when I click on another one it won't re-render only url gets changed. I want to re-render the component on both link clicks. Is there any way to do that??
I was facing similar issue sometime back when I was working on a react project.
You need to use componentWillReceiveProps function in your component.
componentWillReceiveProps(nextProps){
//call your api and update state with new props
}
UPDATE
For react version 16.3+ please use componentDidUpdate
componentDidUpdate (prevProps, prevState) {
// update state
}
To make it more clear when your component loads for the first time by calling url www.example.com/content/a componentDidMount() is run.
Now when you click another link say www.example.com/content/b same component is called but this time prop changes and you can access this new prop under componentWillReceiveProps(nextProps) which you can use to call api and get new data.
Now you can keep a common function say initializeComponent() and call it from componentDidMount() and componentWillReceiveProps()
Your router would look something like this:-
ReactDOM.render((
<Router history={browserHistory}>
<Route path="/content" component={app}>
<IndexRoute component={home}/>
<Route path="/content/:slug" component={component_name} />
</Route>
</Router>
), document.getElementById('app'));
So now when you call www.example.com/content/a, a would be taken as slug. Within that component if you call www.example.com/content/b , b would be taken as slug and would be available as nextProps parameter in componentWillReceiveProps.
Hope it helps!!
use location hook.
const location = useLocation();
const renderDOM = () => {
// return your DOM, e.g. <p>hello</p>
}
useEffect(() => {
// re render your component
renderDOM();
},[location]);
return (
<>
{renderDOM()}
</>
);
I just had the same problem myself using a pure function component like this:
import { useHistory, Link, BrowserRouter, useLocation } from 'react-router-dom'
function Comp() {
const history = useHistory()
// useLocation()
return (
<>
<p>current path is {history.location.pathname}</p>
<Link to="/abc">click me</Link>
<Link to="/xyz">click me</Link>
</>
)
}
export default function App() {
return (
<BrowserRouter>
<Comp />
</BrowserRouter>
)
}
When you click the links, the URL in the title bar updates but the component does not re-render (i.e. the displayed path doesn't update)
In this case I found the solution is to include a call to useLocation() (shown commented out). You don't even need to do anything with the return value; adding this call makes the component magically re-render when ever the location changes.
Note this is with react-router 5.3. react-router 6 is changed beyond recognition so I don't know if the same problem still exists
React 16.3+ discourages the use of componentWillReceiveProps.
React now recommends moving data updates into componentDidUpdate (source):
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
static getDerivedStateFromProps(props, state) {
// Store prevId in state so we can compare when props change.
// Clear out previously-loaded data (so we don't render stale stuff).
if (props.id !== state.prevId) {
return {
externalData: null,
prevId: props.id,
};
}
// No state update necessary
return null;
}
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = loadMyAsyncData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}

Why am I losing my ReactJS state?

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.

Resources