Passing props through react router not available in componentDidMount? - reactjs

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
}
}

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

React warning about setState in unmounted component

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.

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

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});
}
);
}
}

How to hydrate server-side parameters with React + Redux

I have a universal React app that is using Redux and React Router. Some of my routes include parameters that, on the client, will trigger an AJAX request to hydrate the data for display. On the server, these requests could be fulfilled synchronously, and rendered on the first request.
The problem I'm running into is this: By the time any lifecycle method (e.g. componentWillMount) is called on a routed component, it's too late to dispatch a Redux action that will be reflected in the first render.
Here is a simplified view of my server-side rendering code:
routes.js
export default getRoutes (store) {
return (
<Route path='/' component={App}>
<Route path='foo' component={FooLayout}>
<Route path='view/:id' component={FooViewContainer} />
</Route>
</Route>
)
}
server.js
let store = configureStore()
let routes = getRoutes()
let history = createMemoryHistory(req.path)
let location = req.originalUrl
match({ history, routes, location }, (err, redirectLocation, renderProps) => {
if (redirectLocation) {
// redirect
} else if (err) {
// 500
} else if (!renderProps) {
// 404
} else {
let bodyMarkup = ReactDOMServer.renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>)
res.status(200).send('<!DOCTYPE html>' +
ReactDOMServer.renderToStaticMarkup(<Html body={bodyMarkup} />))
}
})
When the FooViewContainer component is constructed on the server, its props for the first render will already be fixed. Any action I dispatch to the store will not be reflected in the first call to render(), which means that they won't be reflected in what's delivered on the page request.
The id parameter that React Router passes along isn't, by itself, useful for that first render. I need to synchronously hydrate that value into a proper object. Where should I put this hydration?
One solution would be to put it, inline, inside the render() method, for instances where it's invoked on the server. This seems obviously incorrect to me because 1) it semantically makes no sense, and 2) whatever data it collects wouldn't be properly dispatched to the store.
Another solution which I have seen is to add a static fetchData method to each of the container components in the Router chain. e.g. something like this:
FooViewContainer.js
class FooViewContainer extends React.Component {
static fetchData (query, params, store, history) {
store.dispatch(hydrateFoo(loadFooByIdSync(params.id)))
}
...
}
server.js
let { query, params } = renderProps
renderProps.components.forEach(comp =>
if (comp.WrappedComponent && comp.WrappedComponent.fetchData) {
comp.WrappedComponent.fetchData(query, params, store, history)
}
})
I feel there must be better approach than this. Not only does it seem to be fairly inelegant (is .WrappedComponent a dependable interface?), but it also doesn't work with higher-order components. If any of the routed component classes is wrapped by anything other than connect() this will stop working.
What am I missing here?
I recently wrote an article around this requirement, but it does require the use of redux-sagas. It does pickup from the point of view of redux-thunks and using this static fetchData/need pattern.
https://medium.com/#navgarcha7891/react-server-side-rendering-with-simple-redux-store-hydration-9f77ab66900a
I think this saga approach is far more cleaner and simpler to reason about but that might just be my opinion :)
There doesn't appear to be a more idiomatic way to do this than the fetchData approach I included in my original question. Although it still seems inelegant to me, it has fewer problems than I initially realized:
.WrappedComponent is a stable interface, but the reference isn't needed anyway. The Redux connect function automatically hoists any static methods from the original class into its wrapper.
Any other higher-order component that wraps a Redux-bound container also needs to hoist (or pass through) any static methods.
There may be other considerations I am not seeing, but I've settled on a helper method like this in my server.js file:
function prefetchComponentData (renderProps, store) {
let { params, components, location } = renderProps
components.forEach(componentClass => {
if (componentClass && typeof componentClass.prefetchData === 'function') {
componentClass.prefetchData({ store, params, location })
}
})
}

Resources