Updating mounted or mounting component warning - javascript

I have the following warning in my tests:
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the ProtectedRoute component.
So I checked my ProtectedRoute component. This component is built upon the Route component from react-router and checks if user is logged in before rendering route. Here is the component code:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router-dom';
import { Authentication } from 'local-authentication-service';
const renderMergedProps = (component, ...otherProps) => {
const finalProps = Object.assign({}, ...otherProps);
return (
React.createElement(component, finalProps)
);
};
class ProtectedRoute extends Component {
constructor() {
super();
this.state = { loading: true };
}
async componentDidMount() {
try {
const user = await Authentication.getUser();
this.setState({ user, loading: false });
} catch (e) {
this.setState({ loading: false });
}
}
render() {
const { loading, user } = this.state;
const { component, ...otherProps } = this.props;
if (loading) return <div>Loading...</div>;
return user ? <Route render={routeProps => renderMergedProps(component, routeProps, { user }, otherProps)} /> : <Redirect to="/auth/login" />;
}
}
ProtectedRoute.propTypes = {
component: PropTypes.oneOfType([
PropTypes.element,
PropTypes.func,
]).isRequired,
};
export default ProtectedRoute;
As far as I see, the only state change is done in componentDidMount() so it should not be throwing this error.
Where should I check to solve this warning ?
Thank you !

Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.?
Before componentDidMount life cycle,render is called.
In render,
return user ?
<Route render={routeProps => renderMergedProps(component, routeProps, { user }, otherProps)} />
: <Redirect to="/auth/login" />;
initially user is evaluated as a false resulted in Redirect route is getting called. So, current ProtectedRoute component is getting unmounted.
But at the same time,you are settting setState inside componentDidMount which not going to execute as your component getting unmounted.
Because of this, you are getting above warning.

By the time your async auth call completes, you've already redirected your user to /auth/login and unmounted ProtectedRoute. When the auth call finally completes, it tries to update ProtectedRoute's state. That's when React gives you this nice message saying you are trying to update a component which is no longer there. Read for details :)
It's a bit tricky but here's what's happening:
On the initial mounting of the component, componentDidMount is invoked and fires off your Authentication.getUser call. Since it's async, it does run immediately and instead gets added to the the javascript event loop. At this point, the code continues on. Since the auth call hasn't actually completed, you have no user of of yet. In your render method you specify to redirect the viewer to /auth/login if there is no user. Since this is the case, you are redirected and your component ends up unmounting.
Do you see where I am going?
The event loop finally decides that no other synchronous code needs to be ran and gives that sitting authentication call a chance to fire. It does its thing and tries to update the state based on where there is/is not a user. But component is no longer there. :(
Then you get this nice message from the great react gods. But you're a coder, and you learn from this and this starts to happen less and less.
How can you avoid this?
You need your component state to keep track of whether or not your auth call has completed. Only after the call is completed should you try to check if there is/isn't a user. Only then, if a user is not present should you redirect.
Maybe an isFetchingUser state property would suffice? Then, once isFetchingUser is false and user does not exist you redirect, otherwise if isFetchingUser is true, you can show either a loading message, a spinner or just a white screen since your async call should hopefully be very fast.

Related

React - correct way to wait for page load?

In React JS, what is the correct way to wait for a page to load before firing any code?
Scenario:
A login service authenticates a user then redirects them (with a cookie), to a React App.
The React App then straight away searches for a cookie and validates it against an endpoint.
But the problem I am getting is when user is authenticated at login service, then forwarded to React App, the App is loading so quick before cookie is even loaded.
What is the right way to fire the code? I tried wrapping in a componentDidMount(), didnt work!
I would suggest you to use a state in the Main component of your application (usually App.jsx) which will control loading. When you start the app the state will be true and only after checking all you need to check it will beacome false. If state is loading you will show a spinner or whatever you want and when it is not loading, the website:
class App extends Component {
constructor(props) {
super(props);
this.state = { loading: true }
}
componentDidMount () {
checkToken()
.then(() => this.setState({ loading: false });
}
if (loading) {
return <Spinner /> // or whatever you want to show if the app is loading
}
return (
...rest of you app
)
}
If any doubt just let me know.
you can use Suspense and lazy :)
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

How can componentDidMount wait for a value from redux store before executing (without using setTimeout)?

I'm trying to get the authentication info into React component and fetch data related to the authenticated user, but componentDidMount is not getting the auth value from the redux store unless setTimeOut is used.
How Do I go about it?
I tried componentWillReceiveProps() , but that does not work either .
class Dashboard extends Component {
componentDidMount = () => {
console.log(this.props.auth);
setTimeout(
() => console.log(this.props.auth),
100
)
}
Only console.log within setTimeout returns the value
I had this same issue a while back and what helped me was to think about my component as a function.
What I would do is display a spinner, until auth is not null, then render a different view once it is ready.
class Dashboard extends Component {
render = () => {
if(this.props.auth === null){
return <SpinnerComponent />
}
return ....
}
}
What will happen is that:
Before props.auth is ready, the component will render a spinner
After props.auth is ready, the component will render your normal app
Rerendering happens when props are change (ie redux is changed)

How to dispatch redux actions using react-router v4?

I'm trying to combine react-router v4, react, and redux. Because react-router tracks the URL, I've opted to keep that piece of state out of the redux-model.
But i still need a way to to dispatch a redux action when a route change happens by react-router. Where is the best place to do that?
My first attempt was to put it in the onClick attribute of react-router's Link:
render() {
// link config
const links = this.props.photo.album( album => {
<Link key={album.name}
to=`/album/${album.name}`
onClick={() => this.props.dispatchAction(album.name)} />
})
// route config
return (
<div>
{links}
<Route path={`/album/:albumName`} component={Album}/>
</div>
)
}
The idea is that, when a user clicks a link, the dispatchAction() will update the redux state and then the Album component gets loaded.
The problem is that if a user navigates to the URL directly (eg /album/a1), the action is never dispatched, since the link is technically never clicked.
Because of this I removed the onClick portion of the Link, and moved the dispatchAction to the lifecycle methods of the Album component:
class Album extends Component {
// invoked when user navigates to /album/:albumName directly
componentDidMount() {
this.props.dispatchAction(this.props.match.params.albumName)
}
// invoked whenever the route changes after component mounted
componentWillReceiveProps(nextProps) {
if (this.props.match.params.albumName != nextProps.match.params.albumName) {
this.props.dispatchAction(nextProps.match.params.albumName)
}
....
}
Now whenever the Album component is mounted or its properties changed, it will dispatch the redux-action. Is this the correct approach for combining these libraries?
react-router-redux does this for you by applying a middleware on your store that dispatches actions on route changes, also on the initial route change. It's definitely the cleanest approach.
The only downside is it's still alpha but we have been using it without any issues for a while. It is also part of the react-router additional packages repo for a while.
You could create a custom Route component that dispatches your action in componentDidMount:
class ActionRoute extends React.Component {
componentDidMount() {
const { pathname } = new URL(window.location.href);
const [albumName] = pathname.split('/').slice(-1);
this.props.dispatchAction(albumName);
}
render() {
return <Route {...this.props} />
}
}
This will now dispatch your action whenever the route is loaded. Composability FTW!.
Side note: if you use ActionRoute for parameterized routes, (eg /album/1 and /album/2), you'll need to also dispatch the action in componentDidUpdate as the component isn't unmounted / remounted if you navigate from /album/1 to /album/2.

Clearing specific redux state before rendering component

I have the following "Buy" button for a shopping cart.
I also have a component called Tooltip, which will display itself for error/success messages. It uses the button's width to determine it's centre point. Hence, I use a `ref since I need to access it's physical size within the DOM. I've read that it's bad news to use a ref attribute, but I'm not sure how else to go about doing the positioning of a child component that is based off the physical DOM. But that's another question... ;)
I am persisting the app's state in localStorage. As seen here:
https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage
The issue I'm running into is that I have to clear the state's success property before rendering. Otherwise, if I have a success message in the state, on the initial render() the Tooltip will attempt to render as well. This won't be possible since the button it relies on is not yet in the DOM.
I thought that clearing the success state via Redux action in componentWillMount would clear up the success state and therefore clear up the issue, but it appears that the render() method doesn't recognize that the state has been changed and will still show the old value in console.log().
My work-around is to check if the button exists as well as the success message: showSuccessTooltip && this.addBtn
Why does render() not recognize the componentWillMount() state change?
Here is the ProductBuyBtn.js class:
import React, { Component } from 'react';
import { connect } from 'react-redux'
// Components
import Tooltip from './../utils/Tooltip'
// CSS
import './../../css/button.css'
// State
import { addToCart, clearSuccess } from './../../store/actions/cart'
class ProductBuyBtn extends Component {
componentWillMount(){
this.props.clearSuccess()
}
addToCart(){
this.props.addToCart(process.env.REACT_APP_SITE_KEY, this.props.product.id, this.props.quantity)
}
render() {
let showErrorTooltip = this.props.error !== undefined
let showSuccessTooltip = this.props.success !== undefined
console.log(this.props.success)
return (
<div className="btn_container">
<button className="btn buy_btn" ref={(addBtn) => this.addBtn = addBtn } onClick={() => this.addToCart()}>Add</button>
{showErrorTooltip && this.addBtn &&
<Tooltip parent={this.addBtn} type={'dialog--error'} messageObjects={this.props.error} />
}
{showSuccessTooltip && this.addBtn &&
<Tooltip parent={this.addBtn} type={'dialog--success'} messageObjects={{ success: this.props.success }} />
}
</div>
);
}
}
function mapStateToProps(state){
return {
inProcess: state.cart.inProcess,
error: state.cart.error,
success: state.cart.success
}
}
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (siteKey, product_id, quantity) => dispatch(addToCart(siteKey, product_id, quantity)),
clearSuccess: () => dispatch(clearSuccess())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductBuyBtn)
Well, it seems to be a known problem that's easy to get into (harder to get out of, especially in a nice / non-hacky way. See this super-long thread).
The problem is that dispatching an action in componentWillMount that (eventually) changes the props going in to a component does not guarantee that the action has taken place before the first render.
So basically the render() doesn't wait for your dispatched action to take effect, it renders once (with the old props), then the action takes effect and changes the props and then the component re-renders with the new props.
So you either have to do what you already do, or use the components internal state to keep track of whether it's the first render or not, something like this comment. There are more suggestions outlined, but I can't list them all.

How to handle invalid ID in react-router route in a redux app?

I have the route /messages/:id that renders a message. However if id is pointing to a non-existing message, where and how should that be handled? My component is bound to the message using redux:
function mapStateToProps(state, ownProps) {
return {
message: state.messages[ownProps.params.id]
}
}
Then message will be undefined in case no such message exist and the component must handle that, and render something different.
However, this seems to bloat the component and I think maybe this should be handled in the router? If there is no such message, it should not allow the route to be called.
Any thoughts?
I too am interested in this too and I have a solution, though not the most elegant one. Hopefully this will help though.
import NotFound from './NotFound'
import Message from './Message'
import {asyncGetMessage} from './api'
const onEnter = ({params, location}, replaceState, callback) => {
asyncGetMessage(params.messageId)
.then((isFound) => {
location.isFound = isFound
callback()
})
}
const getComponent = (Component) => {
return (location, callback) => {
callback(null, (state) => {
if (location.isFound) {
state.route.status = 200
return <Component {...state} />
} else {
state.route.status = 404
return <NotFound {...state} />
}
})
}
}
const routes = (
<Route path='messages'>
<Route path=':messageId' getComponent={getComponent(Message)} onEnter={onEnter} />
</Route>
)
What is happening here is the onEnter method is called initially and waits for the callback. The onEnter calls the asyncGetMessage promise and sets the isFound property on the location to true or false.
Then getComponent is called, I used a factory to provide the Message component as Component. It needs to return a callback, and within the callback, error and a function with state as the first argument. From there it checks the isFound property on location and returns either the Component setup in the factory or the NotFound component.
I am also setting the the route status to 404 so the server can provide the correct http code when it is rendering the first page load.
Unfortunately the signature of getComponent doesn't receive state, otherwise it would be possible to do it all there instead of using onEnter too.
Hope this helps.
I had the same problem, but my final thoughts on this are, that it is not the concern of the router or a getComponent Wrapper, but only the concern of the Component itself to handle it's own (faulty) state.
When your Component is able to be mounted in an erroneous state, like message = undefined, then your Component should react according to the erroneous state.
Preventing your Component from the erroneous state would be an alternative too, but this bloats your code anyways and the Component would still not be able to handle it's error state.

Categories

Resources