React router + redux navigating back doesn't call componentWillMount - javascript

Currently I pre-load data from api in container component's lifecycle method componentWillMount:
componentWillMount() {
const { dept, course } = this.props.routeParams;
this.props.fetchTimetable(dept, course);
}
It is called when user navigates to route /:dept/:course, and it works fine, until you navigate from let's say: /mif/31 to /mif/33 and then press back button. The component is not actually reinitialized, so the lifecycle method is not called, and the data isn't reloaded.
Is there some sort of way to reload data in this case? Should I maybe use another method of preloading data? I see react router emits LOCATION_CHANGE event on any location change, including navigating back, so maybe I can somehow use that?
If it matters, here's is how I implement data loading:
import { getTimetable } from '../api/timetable';
export const REQUEST_TIMETABLE = 'REQUEST_TIMETABLE';
export const RECEIVE_TIMETABLE = 'RECEIVE_TIMETABLE';
const requestTimetable = () => ({ type: REQUEST_TIMETABLE, loading: true });
const receiveTimetable = (timetable) => ({ type: RECEIVE_TIMETABLE, loading: false, timetable });
export function fetchTimetable(departmentId, courseId) {
return dispatch => {
dispatch(requestTimetable());
getTimetable(departmentId, courseId)
.then(timetable => dispatch(receiveTimetable(timetable)))
.catch(console.log);
};
}

You need to use componentWillReceiveProps to check if new props (nextProps) are same as existing props (this.props). Here's relevant code in Redux example: https://github.com/reactjs/redux/blob/e5e608eb87f84d4c6ec22b3b4e59338d234904d5/examples/async/src/containers/App.js#L13-L18
componentWillReceiveProps(nextProps) {
if (nextProps.dept !== this.props.dept || nextProps.course !== this.props.course) {
dispatch(fetchTimetable(nextProps.dept, nextProps.course))
}
}

I might be wrong here, but I believe the function you are looking for is not componentWillMount but componentWillReceiveProps,
assuming you are passing down variables (like :courseId) from redux router to your component, using setState in componentWillReceiveProps should repaint your component.
Otherwise, you can subscribe to changes in your store: http://redux.js.org/docs/api/Store.html
Disclaimer: I probably know less about redux then you.

Related

Infinite re-render using Zustand

I'm coming from redux + redux-saga and class component, everything went well when using componentDidMount in class component. Dispatching action to fetch api works well without duplicate request.
I've been learning functional component for a while, and decided to use Zustand to replace my redux-saga to handle my state management process. I've able to set state A from state B in reducer just by calling the action creators and the state get updated.
First of all, here's my react functional component code so far:
HomeContainer
import { useEffect } from "react";
import { appStore } from "../App/store";
export default function HomeContainer(props: any): any {
const getCarouselData = appStore((state: any) => state.getCarousels);
const carousels = appStore((state: any) => state.carousels);
useEffect(() => {
if (carousels.length === 0) {
getCarouselData();
}
}, [carousels, getCarouselData]);
console.log("carousels", carousels);
return <p>Home Container</p>;
}
Loading Slice
const loadingSlice = (set: any, get: any) => ({
loading: false,
setLoading: (isLoading: boolean) => {
set((state: any) => ({ ...state, loading: isLoading }));
},
});
export default loadingSlice;
App Store
import create from "zustand";
import homeSlice from "../Home/store";
import loadingSlice from "../Layout/state";
export const appStore = create((set: any, get: any) => ({
...loadingSlice(set, get),
...homeSlice(set, get),
}));
Coming to Zustand, it seems like the behaviour is different than Redux. I'm trying to update the boolean value of loading indicator with this code below:
import create, { useStore } from "zustand";
import axios from "axios";
import { appStore } from "../App/store";
const homeSlice = (set: any, get: any) => ({
carousels: [],
getCarousels: () => {
appStore.getState().setLoading(true);
axios
.get("api-endpoint")
.then((res) => {
set((state: any) => ({
...state,
carousels: res.data,
}));
})
.catch((err) => {
console.log(err);
});
appStore.getState().setLoading(true);
},
});
export default homeSlice;
The state is changing, the dialog is showing, but the component keeps re-render until maximum update depth exceeded. I have no idea why is this happening. How can I update state from method inside a state without re-rendering the component?
Any help will be much appreciated.
Thank you.
Update
The new instance of getCarousels is not created because of the dispatch since the create callback is called only once to set the initial state, then the updates are made on this state.
Original answer
Your global reducer is calling homeSlice(set, get) on each dispatch (through set). This call creates a new instance of getCarousels which is passed as a dependency in your useEffect array which causes an infinite re-rendering.
Your HomeContainer will call the initial getCarousels, which calls setLoading which will trigger the state update (through set) with a new getCarousels. The state being updated will cause the appStore hook to re-render the HomeContainer component with a new instance of getCarousels triggering the effect again, in an infinite loop.
This can be solved by removing the getCarouselsData from the useEffect dependency array or using a ref to store it (like in the example from the Zustand readme) this way :
const carousels = appStore((state: any) => state.carousels);
const getCarouselsRef = useRef(appStore.getState().getCarousels)
useEffect(() => appStore.subscribe(
state => (getCarouselsRef.current = state.getCarousels)
), [])
useEffect(() => {
if (carousels.length === 0) {
getCarouselsRef.current();
}
}, [carousels]); // adding getCarouselsRef here has no effect

React can't perform state update on unmounted component

I'm using the following method to control my header from other components. However I'm getting the old "can't perform a react state update on unmounted component" error when changing page
export const store = {
state: {},
setState(value) {
this.state = value;
this.setters.forEach(setter => setter(this.state));
},
setters: []
};
store.setState = store.setState.bind(store);
export function useStore() {
const [ state, set ] = useState(store.state);
if (!store.setters.includes(set)) {
store.setters.push(set);
}
return [ state, store.setState ];
}
My header then uses it to set a class and control if it needs to be black on white or white on black
const Header = () => {
const [type] = useStore();
render( ... do stuff )
};
And my components on page import useStore and then call setType based on a number of factors, certain layouts are one type, some others, some vary depending on API calls so there are a lot of different Components that need to call the function to set the headers state.
const Flexible = (props) => {
const [type, setType] = useStore();
if( type !== 'dark ){ setType('dark') }
... do stuff
};
The header its self is always on page, is before and outside the router and never unmounts.
This all works perfectly fine and sets the headers sate. However when I change page with React Router I get the can't set state error. I can't see why I would get this error. I first thought that the Component might be trying to run again with react router so I moved the code to set the headers state into a useEffect that only runs on initialisation but that didn't help.
You only ever add to the setters, never remove. So when a component unmounts, it will remain in the setters, and the next time some other part of the app tries to set the state, all the setters get called, including the setter for the unmounted component. This then results in the error you're seeing.
You'll need to modify your custom hook to make use of useEffect, so that you can have teardown logic when unmounting. Something like this:
export function useStore() {
const [ state, set ] = useState(store.state);
useEffect(() => {
store.setters.push(set);
return () => {
const i = store.setters.indexOf(set);
if (i > -1) {
store.setters.splice(i, 1);
}
}
}, []);
return [ state, store.setState ];
}
This error is pretty straightforward it means that you are mutating the state (calling setState) in a component that is not mounted.
This mostly happens with promises, you call a promise, then when its resolved you update the state, but if you switch the page before it resolves, when the promise is resolved it still tries to update the state of a component that now its not mounted.
The easy and "ugly" solution, is to use some parameter that you control in componentWillUnmout to check if you still need to update the state or not like this:
var mounted = false;
componentWillMount(){
mounted = true
}
componentWillUnmount(){
mounted = false
}
// then in the promise
// blabla
promise().then(response => {
if(mounted) this.setState();
})

What is the best way to trigger an redux action if the props changed after the react 16.3?

I would like to trigger some redux action to store some state into the redux(cache some state) after the props have changed. With the releasing of react 16.3, a lot of life cycle functions in the React have changed. Previously, I put this into the componentWillReceiveNextProps(). However, it seems that there is no good way to do that. Currently, I do something like this:
const MyComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.name !== prevState.name) {
nextProps.cacheState(prevState.someState) // this function will dispatch a redux action
return {
someState: nextProps.someState // the someState will changed after this function
}
}
return {
...
}
}
}
It seems that this is not a good way to dispatch an action in the getDerivedStateFromProps() since it cause some side effect. So what is the best practice to solve this problem?
You can do it in getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this.state.name !== prevState.name) {
this.props.cacheState(prevState.someState)
}
}
Alternatively, you can do it directly before or after the setState({ name: newName }) call in your function.

How to update state using Redux?

I am using this starter kit https://github.com/davezuko/react-redux-starter-kit and am following some tutorials at the same time, but the style of this codebase is slightly more advanced/different than the tutorials I am watching. I am just a little lost with one thing.
HomeView.js - This is just a view that is used in the router, there are higher level components like Root elsewhere I don't think I need to share that, if I do let me know, but it's all in the github link provided above.
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { searchListing } from '../../redux/modules/search'
export class HomeView extends React.Component {
componentDidMount () {
console.log(this.props)
}
render () {
return (
<main onClick={this.props.searchListing}>
<NavBar search={this.props.search} />
<Hero/>
<FilterBar/>
<Listings/>
<Footer/>
</main>
)
}
}
I am using connect() and passing in mapStateToProps to tell the HomeView component about the state. I am also telling it about my searchListing function that is an action which returns a type and payload.
export const searchListing = (value) => {
console.log(value)
return {
type: SEARCH_LISTINGS,
payload: value
}
}
Obviously when I call the method inside the connect() I am passing in an empty object searchListing: () => searchListing({})
const mapStateToProps = (state) => {
return {
search: { city: state.search }
}
}
export default connect((mapStateToProps), { searchListing: () => searchListing({}) })(HomeView)
This is where I am stuck, I am trying to take the pattern from the repo, which they just pass 1, I think anytime that action is created the logic is just add 1 there is no new information passed from the component.
What I am trying to accomplish is input search into a form and from the component pass the users query into the action payload, then the reducer, then update the new state with the query. I hope that is the right idea.
So if in the example the value of 1 is hardcoded and passed into the connect() method, how can I make it so that I am updating value from the component dynamically? Is this even the right thinking?
You almost got it right. Just modify the connect function to pass the action you want to call directly:
const mapStateToProps = (state) => ({
search: { city: state.search }
});
export default connect((mapStateToProps), {
searchListing
})(HomeView);
Then you may use this action with this.props.searchListing(stringToSearch) where stringToSearch is a variable containing the input value.
Notice : You don't seem to currently retrieve the user query. You may need to retrieve it first and then pass it to the searchListing action.
If you need to call a function method, use dispatch.
import { searchListing } from '../../redux/modules/search';
const mapDispatchToProps = (dispatch) => ({
searchListing: () => {
dispatch(searchListing());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeView);
Then, you have made the function a prop, use it with searchListing.

How to listen state changes in react.js?

What is the angular's $watch function equivalent in React.js?
I want to listen state changes and call a function like getSearchResults().
componentDidMount: function() {
this.getSearchResults();
}
The following lifecycle methods will be called when state changes. You can use the provided arguments and the current state to determine if something meaningful changed.
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
In 2020 you can listen to state changes with the useEffect hook like this
export function MyComponent(props) {
const [myState, setMystate] = useState('initialState')
useEffect(() => {
console.log(myState, '- Has changed')
},[myState]) // <-- here put the parameter to listen
}
I haven't used Angular, but reading the link above, it seems that you're trying to code for something that you don't need to handle. You make changes to state in your React component hierarchy (via this.setState()) and React will cause your component to be re-rendered (effectively 'listening' for changes).
If you want to 'listen' from another component in your hierarchy then you have two options:
Pass handlers down (via props) from a common parent and have them update the parent's state, causing the hierarchy below the parent to be re-rendered.
Alternatively, to avoid an explosion of handlers cascading down the hierarchy, you should look at the flux pattern, which moves your state into data stores and allows components to watch them for changes. The Fluxxor plugin is very useful for managing this.
Since React 16.8 in 2019 with useState and useEffect Hooks, following are now equivalent (in simple cases):
AngularJS:
$scope.name = 'misko'
$scope.$watch('name', getSearchResults)
<input ng-model="name" />
React:
const [name, setName] = useState('misko')
useEffect(getSearchResults, [name])
<input value={name} onChange={e => setName(e.target.value)} />
I think you should be using below Component Lifecycle as if you have an input property which on update needs to trigger your component update then this is the best place to do it as its will be called before render you even can do update component state to be reflected on the view.
componentWillReceiveProps: function(nextProps) {
this.setState({
likesIncreasing: nextProps.likeCount > this.props.likeCount
});
}
If you use hooks like const [ name , setName ] = useState (' '), you can try the following:
useEffect(() => {
console.log('Listening: ', name);
}, [name]);
Using useState with useEffect as described above is absolutely correct way. But if getSearchResults function returns subscription then useEffect should return a function which will be responsible for unsubscribing the subscription . Returned function from useEffect will run before each change to dependency(name in above case) and on component destroy
It's been a while but for future reference: the method shouldComponentUpdate() can be used.
An update can be caused by changes to props or state. These methods
are called in the following order when a component is being
re-rendered:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
ref: https://reactjs.org/docs/react-component.html
I use this code to see which one in the dependencies changes. This is better than the pure useEffect in many cases.
// useWatch.js
import { useEffect, useMemo, useRef } from 'react';
export function useWatchStateChange(callback, dependencies) {
const initialRefVal = useMemo(() => dependencies.map(() => null), []);
const refs = useRef(initialRefVal);
useEffect(() => {
for(let [index, dep] of dependencies.entries()) {
dep = typeof(dep) === 'object' ? JSON.stringify(dep) : dep;
const ref = refs.current[index];
if(ref !== dep) {
callback(index, ref, dep);
refs.current[index] = dep;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
}
And in my React component
// App.js
import { useWatchStateChange } from 'useWatch';
...
useWatchStateChange((depIndex, prevVal, currentVal) => {
if(depIndex !== 1) { return } // only focus on dep2 changes
doSomething("dep2 now changes", dep1+dep2+dep3);
}, [ dep1, dep2, dep3 ]);

Categories

Resources