How to dispatch redux actions using react-router v4? - javascript

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.

Related

Updating mounted or mounting component warning

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.

React, Apollo 2, GraphQL, Authentication. How to re-render component after login

I have this code: https://codesandbox.io/s/507w9qxrrl
I don't understand:
1) How to re-render() Menu component after:
this.props.client.query({
query: CURRENT_USER_QUERY,
fetchPolicy: "network-only"
});
If I login() I expect my Menu component to re-render() itself. But nothing.
Only if I click on the Home link it re-render() itself. I suspect because I'm using this to render it:
<Route component={Menu} />
for embrace it in react-router props. Is it wrong?
Plus, if inspect this.props of Menu after login() I see loading: true forever. Why?
2) How to prevent Menu component to query if not authenticated (eg: there isn't a token in localStorage); I'm using in Menu component this code:
export default graphql(CURRENT_USER_QUERY)(Menu);
3) Is this the right way to go?
First, let me answer your second question: You can skip an operation using the skip query option.
export default graphql(CURRENT_USER_QUERY, {
skip: () => !localStorage.get("auth_token"),
})(Menu);
The problem now is how to re-render this component when the local storage changes. Usually react does not listen on the local storage to trigger a re-render, instead a re-render is done using one of this three methods:
The component's state changes
The parent of the component re-renders (usually with new props for the child)
forceUpdate is called on the component
(Note that also a change of a subscribed context will trigger a re-render but we don't want to mess around with context ;-))
You might want to go with a combination of 2. and 3. or 1. and 2.
In your case it can be enough to change the route (as you do in the login function). If this does not work you can call this.forceUpdate() on the App component after Login using a callback property on <Login onLogin={() => this.forceUpdate() } />.
EDITED
1) I just created new link https://codesandbox.io/s/0y3jl8znw
You want to fetch the user data in the componentWillReceiveProps method:
componentWillReceiveProps() {
this.props.client.query({query: CURRENT_USER_QUERY})
.then((response) => {
this.setState({ currentUser: response.data.currentUser})
})
.catch((e) => {
console.log('there was an error ', e)
})
}
This will make the component re-render.
2) Now when we moved the query call in the component's lifecycle method we have full control over it. If you want to call the query only if you have something in localstorage you just need to wrap the query in a simple condition:
componentWillReceiveProps() {
if(localstora.getItem('auth_token')) {
this.props.client.query({query: CURRENT_USER_QUERY})
.then((response) => {
this.setState({ currentUser: response.data.currentUser})
})
.catch((e) => {
console.log('there was an error ', e)
})
}
}
3) You want to store the global application state in redux store. Otherwise you will have to fetch the user info every time you need to work with it. I would recommend to define a state in you App component and store all the global values there.

Storing React Routes in Redux

I am using react-router v4 to generate dynamic routes which are loaded from an asynchronous API. The data retrieved from the API is passed through a function which will create the React routes, something similar to:
import Home from "containers/home"
import FeaturePage from "containers/featurePage"
response.map((item) => {
if (item.value.toLowerCase() === "home") {
return {
path: item.path,
component: Home,
exact: item.isExact
}
} else if (item.value.toLowerCase() === "featurePage") {
return {
path: item.path,
component: FeaturePage,
exact: item.isExact
}
} else {
// ...etc
}
});
Once the routes are loaded, they are added to the Redux store using an action-reducer as usual. This works well as the redux store supports functions.
However storing functions in the Redux store doesn't seem to be the best way forward. What would be a better implementation? I need to store the component which needs to be rendered, which is not serialisable.
Just store the component name as strings? (Not sure why you wanna add them to the store though)
You can probably use them later like this:
import Home from "containers/home"
import FeaturePage from "containers/featurePage"
// Assume the routes have been added to the redux store
const Routes = {
Home,
FeaturePage
};
response.map(({ component, path, exact }) => {
return (
<Route
key={ path }
exact={ exact }
path={ path }
component={ Routes[component] }
/>
);
});
A React component is made up of the state of the component (made up of state and props) and some behavior (like functions and html markup) which is modified based on the state. Once you have created the component, the behavior of the component will not change; only the state of the component will change.
I would think saving the state of the component would be enough to recreate the component at a later time and you would not need to save the component in Redux; instead save the state of the component in Redux.
Regarding saving your Routes in Redux, the same applies; you should save the state of the Route rather than the route itself in your Redux store. You can have a function which could take this state object (containing the path, isExact, component name etc.) and convert it to a real component for you.
Hope this helps!

React Router changing params doesn't fire componentWillRecieveProps

The Problem: Changing the parameters of a <Route /> component does not update the component it is rendering. The route change is shown in the URL bar, but directly rendering {this.props.match.params.id} shows the old :id and not the new one reflected in the URL bar.
Update: I fixed this by moving the <BrowserRouter /> out from the index.js file and into the App.js file. It is no longer the direct child of Provider and is instead the child of the App component. No clue why this makes everything suddenly work.
What I am doing: I have a <Link to="/user/11" /> that goes from user/7 (or any current ID) to a /user/11
The componentWillReceiveProps(newProps) of the component it is rendering is not fired.(This component is connected using react-redux if that helps any. I tried applying withRouter around the connection and that did not help)
If I manually refresh the page in chrome (using CTRL-R or the refresh button) the page shows the new data, rendering the "new" param.
TLDR: Switching from /user/7 to /user/11 does not fire that componentWillRecieveProps function and therefore leaving the component displaying the old state
Question: What am I doing incorrectly here that causes componentWillReceiveProps to not fire.
I am using react-router v4 and the latest create-react-app
This is my CWRP function:
componentWillReceiveProps(newProps) {
console.log("getProps")
this.props.getUser(newProps.match.params.id)
if (newProps.match.params.id == newProps.currentUser.id) {
this.setState({ user: "currentUser" })
} else {
this.setState({ user: "selectedUser" })
}
}
This is the full code of my component: https://gist.github.com/Connorelsea/c5c14e7c54994292bef2852475fc6b43
I was following the solution here and it did not seem to work for me. Component does not remount when route parameters change
You'll need to use React Router Redux

React router not reloading Component when changing url params

I know that it's not a default behaviour / feature of react-router to help us reload easily the current component but I really need this in my application.
My application deals with products. I have a product list that I can load, and when I click on an item, it displays the concerned product details.
On that page, I have related product links that load the same component, but with another product details, located at
<Route path="/products/:id/details" component={ProductDetail} />
I m fetching data in my componentWillMount, and it seems that if I only change the URL, a new component is NOT mounted, and so, I m always having my old data displayed, without fetching anything.
As a beginner using React, I'm looking for some help, or some tricks to reload the component concerned by that page. I mean being able to reload the ProductDetail with the good product.
I tried to look around with componentWillUpdate (a method in which I can see that the router URI changes :D) but I can't setState inside of it to make my component reload (it doesn't seem to be a good practice at all)
Any idea how can I make this work ?
EDIT : According to the first answer, I have to use onEnter. I m now stuck with the way of passing state/props to the concerned component :
const onEnterMethod = () => {
return fetch(URL)
.then(res => res.json())
.then(cmp => {
if (cmp.length === 1) {
// How to pass state / props to the next component ?
}
});
};
The way to handle it depends if you are using flux, redux or however you want to manage your actions. On top of it I would try to make use of onChange property of Route component (check React router docs):
<Route path="/products/:id/details" component={ProductDetail} onChange={someMethod} />
And then in the someMethod create the action if you are using redux or however is done in flux.
The redux would be:
<Route path="/products/:id/details" component={ProductDetail} onEnter={onEnterHandler(store)} />
And the onEnterHandler with the redux store:
function onEnterHandler(store) {
return (nextState, replace) => {
store.dispatch({
type: "CHANGEPRODUCT",
payload: nextState.params.id
})
};
}
And then in your ProductDetail component you would print the new information (It would require a bit more of learning in redux and redux-sagas libraries to complete that part).
Keep in mind that React is just the view part, trying to solve all those problems using only react is not only not recommended but also would mess up your code.

Categories

Resources