How and where to dispatch action when react router changes props - reactjs

I may be missing something here, change in react-router params using Link will cause the props to change with the new param(s) and trigger an update cycle.
You can't dispatch an action in the update cycle but that's the only place where the parameter is available. I need to get new data when the day parameter changes in my stateless component.
The component has 2 links, previous and next day. The previous day looks like this:
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
[UPDATE]
Here is the solution so far:
The Overview component is just a function taking props and returning jsx, the following is the container that will check if date is set and if it's not it'll redirect. If date is set then it'll return Overview.
It also checks if the router day paramater changed, if it did then it'll set dispatchFetch to true.
This will then cause the render function to asynchronously dispatch the getData action.
Not sure if there would be another way to do this, I would prefer to listen to events from router and dispatch the events from there but there is no (working) way to listen to the router.
import { connect } from 'react-redux';
import React, { Component } from 'react';
import Overview from '../components/Overview';
import { getData } from '../actions';
import { selectOverview } from "../selectors";
import { Redirect } from "react-router-dom";
const defaultDate = "2018-01-02";
const mapStateToProps = (lastProps => (state, ownProps) => {
var dispatchFetch = false;
if (lastProps.match.params.day !== ownProps.match.params.day) {
lastProps.match.params.day = ownProps.match.params.day;
dispatchFetch = true;
}
return {
...selectOverview(state),
routeDay: ownProps.match.params.day,
dispatchFetch
}
})({ match: { params: {} } });
const mapDispatchToProps = {
getData
};
class RedirectWithDefault extends Component {
render() {
//I would try to listen to route change and then dispatch getData when
// routeDay changes but none of the methods worked
// https://github.com/ReactTraining/react-router/issues/3554
// onChange in FlexkidsApp.js never gets called and would probably
// result in the same problem of dispatching an event in update cycle
// browserHistory does not exist in react-router-dom
// this.props.history.listen didn't do anything when trying it in the constructor
//So how does one dispatch an event when props change in stateless components?
// can try to dispatch previous and next day instead of using Link component
// but that would not update the url in the location bar
if (this.props.dispatchFetch) {
Promise.resolve().then(() => this.props.getData(this.props.routeDay));
}
return (this.props.routeDay)//check if url has a date (mapStateToProps would set this)
? Overview(this.props)
: <Redirect//no date, redirect
to={{
pathname: "/list/" + defaultDate
}}
/>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RedirectWithDefault);

Intially I expect your Router is like,
<Route path="/sessions/:prefDay" component={MyComponent}/>
You will have to do this in getDerivedStateFromProps(),
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prefDay !== nextProps.match.params.prefDay) {
return {
prefDay: nextProps.match.params.prefDay,
};
}
// Return null to indicate no change to state.
return null;
}

Have you tried componentWillReceiveProps?
componentWillReceiveProps(props){
if(this.props.data != props.data && this.props.state.routeDay){
this.props.getData(props.state.routeDay);
}
}

To add parameter to route you need in router do this
<Route
path="/sessions/:prefDay"
....
/>
It means that now "/sessions/" rout have a parameter named "prefDay"
Example /sessions/:prefDay
and now in Link component need to add this parameter
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
You can get this value from url like this
this.props.match.params.prefDay

In RedirectWithDefaults which is a container of the stateless component I added componentDidMount (not an update cycle method):
const historyListener = (lastDay => (props,currentDay) => {
if(lastDay !== currentDay){
lastDay = currentDay;
if(!isNaN(new Date(currentDay))){
console.log("getting:",currentDay);
props.getData(currentDay);
}
}
})(undefined);
class RedirectWithDefault extends Component {
componentDidMount(history) {
this.props.getData(this.props.routeDay || defaultDate);
this.unListen = this.props.history.listen((location) => {
historyListener(
this.props,
location.pathname.split('/').slice(-1)[0]
);
});
}
componentWillUnmount(){
this.unListen();
}
Removed code from the render function. Now when it mounts it'll dispatch the action to load data and when the route changes it will dispatch it again but not in the update cycle.

Related

Initial State of Redux Store with JSON Data?

I am working on React app where the state is managed by redux. I am using actions.js file to fetch JSON data and store it directly in the store. The initial Store has just one key (data) in its obj with null as its value.
I use componentDidMount() Lifecycle to call the function which updates the store's data key with the JSON data I receive. However, whenever I load my app it gives an error because it finds the data value as null.
I get it. componentDidMount() executes after the app is loaded and the error doesn't let it execute. I tried using componentWillMount() but it also gives the same error. ( Which I use in JSX )
When I try to chanage the data's value from null to an empty obj it works for some level but after I use it's nested objects and arrays. I get error.
I wanna know what is the way around it. What should I set the vaue of inital State or should you use anyother lifecycle.
If your primary App component can't function properly unless the state has been loaded then I suggest moving the initialization logic up a level such that you only render your current component after the redux state has already been populated.
class version
class LoaderComponent extends React.Component {
componentDidMount() {
if ( ! this.props.isLoaded ) {
this.props.loadState();
}
}
render() {
if ( this.props.isLoaded ) {
return <YourCurrentComponent />;
} else {
return <Loading/>
}
}
}
export default connect(
state => ({
isLoaded: state.data === null,
}),
{loadState}
)(LoaderComponent);
Try something like this. The mapStateToProps subscribes to the store to see when the state is loaded and provides that info as an isLoaded prop. The loadState in mapDispatchToProps is whatever action creator your current componentDidMount is calling.
hooks version
export const LoaderComponent = () => {
const dispatch = useDispatch();
const isLoaded = useSelector(state => state.data === null);
useEffect(() => {
if (!isLoaded) {
dispatch(loadState());
}
}, [dispatch, isLoaded]);
if (isLoaded) {
return <YourCurrentComponent />;
} else {
return <Loading />
}
}
And of course you would remove the fetching actions from the componentDidMount of the current component.

How to sync React Router Params with state

Using React Router Web.
Assume we have a route: /:user?showDetails=true. We know how to get the data from the URL with the useParams hook and something like the useQuery custom hook.
Also, we know how to set this data with history.push(/baruchiro?showDetails=false).
But if we get and set this data, and in case we don't use this to redirect the user from one page to another, but to change the current component (to let the user save its current page view), it's mean that the route is state.
How can I use the route as a state without getting the component dirty with a lot of history.push and useParams?
Update
I published this custom hook as npm package: use-route-as-state
If you want to use the route as state, you need a way to get the route params, and also update them.
You can't avoid using history.push since this is the way you change your "state", your route. But you can hide this command for cleaner code.
Here is an example of how to hide the get and the update in custom hooks, that make them to looks like a regular useState hook:
To use Query Params as state:
import { useHistory, useLocation} from 'react-router-dom'
const useQueryAsState = () => {
const { pathname, search } = useLocation()
const history = useHistory()
// helper method to create an object from URLSearchParams
const params = getQueryParamsAsObject(search)
const updateQuery = (updatedParams) => {
Object.assign(params, updatedParams)
// helper method to convert {key1:value,k:v} to '?key1=value&k=v'
history.replace(pathname + objectToQueryParams(params))
}
return [params, updateQuery]
}
To use Route Params as state:
import { generatePath, useHistory, useRouteMatch } from 'react-router-dom'
const useParamsAsState = () => {
const { path, params } = useRouteMatch()
const history = useHistory()
const updateParams = (updatedParams) => {
Object.assign(params, updatedParams)
history.push(generatePath(path, params))
}
return [params, updateParams]
}
Note to the history.replace in the Query Params code and to the history.push in the Route Params code.
Usage: (Not a real component from my code, sorry if there are compilation issues)
const ExampleComponent = () => {
const [{ user }, updateParams] = useParamsAsState()
const [{ showDetails }, updateQuery] = useQueryAsState()
return <div>
{user}<br/ >{showDetails === 'true' && 'Some details'}
<DropDown ... onSelect={(selected) => updateParams({ user: selected }) />
<Checkbox ... onChange={(isChecked) => updateQuery({ showDetails: isChecked} })} />
</div>
}
I created the following NPM package to store state using URL search params with react-router-dom-v6: https://www.npmjs.com/package/react-use-search-params-state

Props change (redux) resets the local state when using useState - updated

I am rewriting my class components into functional components. One component uses both Redux and local-state and I noticed that every time the props (redux state) change, the useState hook is executed, resetting my local state. Is this expected behavior, and how to persist your local-state?
old (simplified)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
new
// Count gets reset to 0 every time reduxState changes
const MyComponent = ({reduxState}) => {
const [count, setCount] = useState(0)
UPDATE - the cause of the problem is not in the refactoring into functional components. I have a parent (Higher Order Component) that re-creates the wrapped component on changes to Redux state. For clarity, here is the somewhat simplified HOC
import React, {useEffect} from 'react'
import { connect } from 'react-redux'
import { fetchObject } from '../actions/objects'
import { fetchIfNeeded } from '../utils'
const withObject = ({element}) => WrappedComponent => ({id, ...rest}) => {
const Fetcher = ({status, object, fetchObject}) => {
useEffect(() => {
fetchIfNeeded(
status,
()=>fetchObject(element, id),
)
})
// THIS SECTION IS THE CAUSE OF MY PROBLEM
// Handle loading and error status
if (status && status.error) return <>error loading: {status.error.message}</>
if (status === undefined || object === undefined || status.isFetching) return <>loading</>
// END OF SECTION
// Pass through the id for immediate access
return <WrappedComponent {...{object, status, id}} {...rest} />
}
const mapStateToProps = state => ({
object: state.data[element][id],
status: state.objects[element][id]
})
const mapDispatchToProps = {
fetchObject
}
const WithConnect = connect(mapStateToProps, mapDispatchToProps)(Fetcher)
return <WithConnect/>
}
export default withObject
UPDATE2: The code sample above has been updated to highlight the problematic section. Every time the status changes (e.g. isFetching: true), the hoc returned a different component to reflect this. When it changes again to isFetching: false, a new instance of wrapped component is created and returned, losing its original state.
In short, it is a bad idea to return different components from a HOC depending on redux state. This should be handled in the regular component.

Check New Route on componentWillUnmount?

I have some code that will run on componentWillUnmount() but I only want it run if they go back to the previous page. If they go forward to the next page I don't want what is inside the componentWillUnmount to run.
I am using React Router 4 but when I check it in the componentWillUnmount it still has not updated to whatever the next url is.
componentWillUnmount() {
const props = this.props;
const location = props.location;
}
React Router provides a history object which you can use to set some variables before the transition to a new location.
Try something like this:
componentDidMount() {
this.props.history.block((location, action) => {
this.isGoingBack = action === 'POP';
})
}
componentWillUnmount() {
if (this.isGoingBack) {
...
}
}
You might need to check the location aswell.

Update route and redux state data at same time

I have an app that has user profiles. On the user profile there are a list of friends, and when clicking on a friend it should take you to that other user profile.
Currently, when I click to navigate to the other profile (through redux-router Link) it updates the URL but does not update the profile or render the new route.
Here is a simplified code snippet, I've taken out a lot of code for simplicity sake. There are some more layers underneath but the problem happens at the top layer in my Profile Container. If I can get the userId prop to update for ProfileSections then everything will propagate through.
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) { this.props.getUser(userId) }
}
render() {
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
As you can see, what happens is that I am running the getUser action on componentWillMount, which will happen only once and is the reason the route changes but the profile data does not update.
When I change it to another lifecycle hook like componentWillUpdate to run the getUser action, I get in an endless loop of requests because it will keep updating the state and then update component.
I've also tried using the onEnter hook supplied by react-router on Route component but it doesn't fire when navigating from one profile to another since it's the same route, so that won't work.
I believe I'm thinking about this in the wrong way and am looking for some guidance on how I could handle this situation of navigating from one profile to another while the data is stored in the redux store.
So I would suggest you approach this in the following way:
class Profile extends Component {
componentWillMount() {
const { userId } = this.props.params
if (userId) {
// This is the initial fetch for your first user.
this.fetchUserData(userId)
}
}
componentWillReceiveProps(nextProps) {
const { userId } = this.props.params
const { userId: nextUserId } = nextProps.params
if (nextUserId && nextUserId !== userId) {
// This will refetch if the user ID changes.
this.fetchUserData(nextUserId)
}
}
fetchUserData(userId) {
this.props.getUser(userId)
}
render() {
const { user } = this.props
return <ProfileSections userId={user.id} />
}
}
const mapStateToProps = ({ user }) => {
return { user }
}
export default connect(mapStateToProps, { getUser })(Profile);
Note that I have it set up so in the componentWillMount lifecycle method, you make the request for the initial userId. The code in the componentWillReceiveProps method checks to see if a new user ID has been received (which will happen when you navigate to a different profile) and re-fetches the data if so.
You may consider using componentDidMount and componentDidUpdate instead of componentWillMount and componentWillReceiveProps respectively for the fetchUserData calls, but it could depend on your use case.

Resources