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
Related
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.
When an error is thrown in our React 16 codebase, it is caught by our top-level error boundary. The ErrorBoundary component happily renders an error page when this happens.
Where the ErrorBoundary sits
return (
<Provider store={configureStore()}>
<ErrorBoundary>
<Router history={browserHistory}>{routes}</Router>
</ErrorBoundary>
</Provider>
)
However, when navigating back using the browser back button (one click), the URL changes in the address but the page does not update.
I have tried shifting the error boundary down the component tree but this issue persists.
Any clues on where this issue lies?
The op has probably found a resolution by now, but for the benefit of anyone else having this issue I'll explain why I think its happening and what can be done to resolve it.
This is probably occurring due to the conditional rendering in the ErrorBoundary rendering the error message even though the history has changed.
Although not shown above, the render method in the ErrorBoundary is probably similar to this:
render() {
if (this.state.hasError) {
return <h1>An error has occurred.</h1>
}
return this.props.children;
}
Where hasError is being set in the componentDidCatch lifecycle method.
Once the state in the ErrorBoundary has been set it will always render the error message until the state changes (hasError to false in the example above). The child components (the Router component in this case) will not be rendered, even when the history changes.
To resolve this, make use of the react-router withRouter higher order component, by wrapping the export of the ErrorBoundary to give it access to the history via the props:
export default withRouter(ErrorBoundary);
In the ErrorBoundary constructor retrieve the history from the props and setup a handler to listen for changes to the current location using history.listen. When the location changes (back button clicked etc.) if the component is in an error state, it is cleared enabling the children to be rendered again.
const { history } = this.props;
history.listen((location, action) => {
if (this.state.hasError) {
this.setState({
hasError: false,
});
}
});
To add to jdavies' answer above, make sure you register the history listener in a componentDidMount or useEffect (using [] to denote it has no dependencies), and unregister it in a componentWillUnmount or useEffect return statement, otherwise you may run into issues with setState getting called in an unmounted component.
Example:
componentDidMount() {
this.unlisten = this.props.history.listen((location, action) => {
if (this.state.hasError) {
this.setState({ hasError: false });
}
});
}
componentWillUnmount() {
this.unlisten();
}
The analog to jdavies answer for React Router 6 is:
const { pathname } = useLocation()
const originalPathname = useRef(pathname)
useEffect(() => {
if (pathname !== originalPathname.current) {
resetErrorBoundary()
}
}, [pathname, resetErrorBoundary])
tl;dr wrap components where you expect errors with error boundaries but not the entire tree
Tried first #jdavies answer using withRouter but then found a better solution for my use case: Dan from the React-Team advised against using HOCs with Error Boundaries and rather use them at stragetic places.
In that Twitter thread is a debate around the pros and cons though and Dan left it open which way you should go but I found his thoughts convincing.
So what I did was to just wrap those strategic places where I expect an error and not the entire tree. I prefer this for my use case because I can throw more expressive, specific error pages than before (something went wrong vs there was an auth error).
jdavies comment is the way to go,
but, if you are confused by this, basically you make it look like this:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
const { history } = props;
history.listen((location, action) => {
if (this.state["hasError"]) {
this.setState({
hasError: false,
});
}
});
this.state = { hasError: false };
}
...
then at the end of the file you add:
export default withRouter(ErrorBoundary);
(don't forget to import { withRouter } from "react-router-dom"; at top)
also, if you were using e.g. export class ErrorBoundry ... like i was, don't forget to change the import { ErrorBoundary } from "./ErrorBoundry"; to import ErrorBoundary from "./ErrorBoundry"; wherever you use it e.g. App.tsx
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
I have a React component that represents a page. Users should never reach the page unless they have some state. I wrote the following to have react-router redirect the user if state is missing:
class BoxScreen extends Component {
constructor(props) {
super(props);
if (Object.keys(this.props.boxState.current).length === 0) {
browserHistory.push('/secured/find')
}
I then get this message:
warning.js:36 Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
Ok, so I'll move my redirection code to componentWillMount:
class BoxScreen extends Component {
...
componentWillMount() {
if (Object.keys(this.props.boxState.current).length === 0) {
browserHistory.push('/secured/find')
}
}
render() {
// Don't want this executed or else I'll need to pollute it with null guards...
}
}
However, the issue is that the above doesn't stop render from being called, which is definitely not desired. Where is the most appropriate spot to put this so I can short-circuit the component rendering and perform the redirect?
You could use react-router's onEnter prop to accomplish that:
function checkAuthenticated(nextState, replace) {
if (!SOME_CONDITION) {
replace('/secured/find')
}
}
<Route ... onEnter={checkAuthenticated} />
But I believe you can't access the components props in this way. You'd have to keep track of the state in some other way. In case you're using Redux, I believe you could use redux-router to get the store.
You can check the Enter/leave hooks documentation
Just use react-router's <Redirect/> component inside render to achieve it, I am using v4 so I will provide you solution compatible to that.
import {Redirect} from 'react-router-dom';
class BoxScreen extends Component {
...
render() {
return this.props.condition ?
<ActualComponent /> : <Redirect to="/someGuardLink">;
}
}
Replace <ActualComponent /> with your components JSX, and yah, this should work. Your component will still mount and render but after that it is going to redirect you to some other link without you getting to notice any effect of it. This is the official way of doing it.
Expanding on what smishr4 had to say:
Have a default state for your object in the reducer. For example, my initial state for an ID is:
ID: -1
Be sure that you've mapped your object from state to props.
function mapStateToProps(state, ownProps) {
return {
ID: state.ID,
}
}
On anything that extends from React.Component, write an if condition within the componentWillMount() function - so that this happens before page load. This particular function will route me back to the homepage, if the .ID field is undefined (which is always initially true), and after it is set to its initial state in the reducer (-1).
componentWillMount() {
{
if (this.props.ID != -1 && this.props.ID != undefined) {
this.props.loadYourDataFromAPIGivenID(this.props.ID);
}
else {
this.props.history.push('/');
}
}
}
Is there a better way? Probably. You might be able to do the blocking within the router directly, but this is a good work around.
You can also use this.state directly, but this.props is a bit safer to use.
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.