Infinite useEffect loop when using Redux with React Hooks - reactjs

I am refactoring some code and turning my class components into function components as a way of learning how to use Hooks and Effects. My code uses Redux for state management and axios for database requests with Thunk as middleware for handling asynchronicity. I'm having an issue in one component that does a get request to retrieve a list of customers on what used to be componentDidMount. No matter what I try, the useEffect function gets into an infinite loop and continues requesting the customer list.
The component in question, CustomersTable, gets a list of customers from the database and displays it in a table. The component is wrapped by a container component that uses Redux's connect to pass in the retrieved list of customers to the CustomersTable as a prop.
useEffect(() => {
loadCustomers(currentPage, itemsPerPage, sortProp, (ascending ? 'asc' : 'desc'), {});
}, []);
loadCustomers is a Redux action that uses axios to fetch the customer list. currentPage, itemsPerPage, sortProp and ascending are state variables that are initialized to specific values on 'component mount'
I would expect that because I use the empty array that this would run only once. Instead it runs continuously. I can't figure out why this is happening. My best guess is that when redux gets the list, it returns a new object for state and therefore the props change every time, which then triggers a re-render, which then fetches a new list. Am I using this wrong in that Redux isn't meant to be used with hooks like this?
I ended up getting this working by adding the following:
useEffect(() => {
if (!list.length) {
loadCustomers(currentPage, itemsPerPage, sortProp, (ascending ? 'asc' : 'desc'), {});
}
}, []);
I'm not sure this is the behavior I truly want though. If the list of customers was truly 0, then the code would continue to fetch the list. If the list were truly empty, then I would want it to fetch only once and then stop. Edit: Turns out this definitely doesn't work. It works for the initial load, but breaks the code for any delete or edit.
OK, providing more context here. The container component that wraps the CustomersTable is:
import { connect } from 'react-redux';
import loadCustomers from './actions/customersActions';
import { deleteCustomer } from './actions/customerActions';
import CustomersTable from './CustomersTableHooks';
function mapStateToProps(state) {
return {
customers: state.customers,
customer: state.customer
};
}
export default connect(mapStateToProps, { loadCustomers, deleteCustomer })(CustomersTable);
The action, loadCustomers is:
export default function loadCustomers(page = 1, itemsPerPage = 50, sortProp = 'id', sortOrder = 'asc', search = {}) {
return (dispatch) => {
dispatch(loadCustomersBegin());
return loadCustomersApi(page, itemsPerPage, sortProp, sortOrder, search)
.then(data => dispatch(loadCustomersSuccess(data)))
.catch(() => dispatch(loadCustomersFailure()));
};
}
the reducer for customers is:
export default function customersReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_CUSTOMERS_BEGIN:
return Object.assign({}, state, { isLoading: true, list: [], totalItems: 0 });
case types.LOAD_CUSTOMERS_SUCCESS:
return Object.assign({}, state, { isLoading: false, list: action.customers || [], totalItems: action.totalItems });
case types.LOAD_CUSTOMERS_FAILURE:
return Object.assign({}, state, { isLoading: false, list: [], totalItems: 0 });
default:
return state;
}
}
I unfortunately can't post much of the CustomersTable itself because things are named in a way that would tell you what company I'm working for.

So, if i understand your code correctly, you are dispatching the loadCustomers action in child component within useEffect but you read actual data in parents mapStateToProps.
That would, of course, create infinite loop as:
parent reads customers from store (or anything from the store, for that matter)
renders children
child fetches customers in useEffect
properties on parent change and cause re-render
whole story goes on forever
Moral of the story: don't dispatch from presentational components. or, in other words, dispatch an action from the same component you read those same properties from store.

On every render you get new customer object because mapStateToProps if doing shallow equal. You could use memoized selectors to get customers, and it won't rerender when is not needed to.
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, deepEqual);
const customerSelector = createDeepEqualSelector(
[state => state.customerReducer.customers],
customers => customers,
);

I disagree with the most voted answer here.
properties on parent change and cause re-render
whole story goes on forever
A re-render will not call the function argument of useEffect when the 2nd argument of dependencies is and empty array. This function will only be called the 1st time, similar to the life cycle method componentDidMount. And it seems like this topic was created because that correctly expected behavior wasn't occurring.
It seems like what is happening is that the component is being unmounted and then mounted again. I had this issue and it brought me here. Unfortunately, without the component code we can only guess the actual cause. I thought it was related to react-redux connect but it turns out it wasn't. In my case the issue was that someone had a component definition within a component and the component was being re-defined / recreated on every render. This seems like a similar issue. I ended up wrapping that nested definition in useCallback.

Related

How to sto React useEffect hook from rendering duplicated data

I have a component where I use useEffect and inside call a function fetchLatest(). fetchLatest() is an action that makes a network request to get all movies that have been released during the current year. I am using Redux and in my store I have a state.movies.latest and each time I start the app for the first time the latest row of movies is accurate with no duplicated data. When I render a different component and go back to my home component where I do fetchLatest I then get duplicated data.
I figured out that my problem is because in my reducers I return something like...
return {
...state,
latest: [...state, ...payload]
}
where payload is an array of objects (movies).
So I changed my reducers to...
return {
...state,
latest: [...payload]
}
It works now and I don't get duplicated data but my concern is that I am not doing or understand this correctly. If I were to use useEffect and have my fetchLatest function in my useEffect hook and pass it as a dependency to my understanding my function fetchLatest will be recreated after every render cycle as a brand new object. Being that I already have in the store my latest array of movies I do not want to re-render a brand new object each time causing to add to my store in my state.movies.latest duplicated data. I at least believe this is what it is doing.
I decided that maybe I should be using useCallback to ensure that this brand new object of fetchLatest is not created unless its dependencies change.
So what I am not sure about is maybe I can have my updated reducer as I have it now and also useCallback to ensure no infinite loops are running for recreating my fetchLatest function each time as a new object when I re-build that component?
Also, I might not understand actually what useCallback does because I tried before using a useCallback and having my reducer as I originally had it and to my understanding if nothing changed then why is it that my fetchLatest runs when I rebuild the component. I only want my latest movies which I already have in the store when I first mount my component
my code for this component looks like this
const Latest = ({ fetchLatest, latest }) => {
useEffect(() => {
fetchLatest();
}, [fetchLatest]);
return (
<div className='genre-temp'>
<h3 className='text-light'>Latest</h3>
<div className='movies-container d-flex'>
{latest.map((movie) => (
<Movie
key={movie.id}
movieId={movie.id}
image={movie.image}
title={movie.title}
description={movie.description}
/>
))}
</div>
</div>
);
};
and what I had tried to do initially was add this useCallback and call handleFetching in my useEffect and pass it in as a dependency.
const handleFetching = useCallback(() => {
fetchLatest()
}, []);
However, useCallback in conjunction with useEffect while still having my reducer as such...
return {
...state,
latest: [...state, ...payload]
}
still would run each time the component rebuilt.
Try to add a request state to your state object. Then, you'll be able to check if the list is fetched. Usually, I use four states for requests: init, request, success, failure. Here is a rough example of a request:
if (requestState !== 'request' && requestState !== 'success') {
try {
setRequestState('request');
const response = await fetchList();
setList(response);
setRequestState('success');
catch (e) {
setRequestState('failure');
log(e);
}
}
If you have a request state, you'll always know when you need to request the data, show a spinner, show an error message, etc.
Hopefully, you got the idea.
P.S. In your code, it's supposed to be:
{
...state,
latest: [...payload]
}
latest: [...state.latest, ...payload] won't work as you expect.
latest: [...state, ...payload] looks wrong.

useState vs useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
(quote from https://reactjs.org/docs/hooks-reference.html#usereducer)
I'm interested in the bold part, which states that useReducer should be used instead of useState when being used in contexts.
I tried both variants, but they don't appear to differ.
The way I compared both approaches was as follows:
const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);
I passed each of them once to a context object, which was being consumed in a deeper child (I just ran separate tests, replacing the value by the function that I wanted to test).
<ContextObject.Provider value={updateState // dispatch}>
The child contained these functions
const updateFunction = useContext(ContextObject);
useEffect(
() => {
console.log('effect triggered');
console.log(updateFunction);
},
[updateFunction]
);
In both cases, when the parent rerendered (because of another local state change), the effect never ran, indicating that the update function isn't changed between renders.
Am I reading the bold sentence in the quote wrong? Or is there something I'm overlooking?
useReducer also lets you optimize performance for components that
trigger deep updates because you can pass dispatch down instead of
callbacks.
The above statement is not trying to indicate that the setter returned by useState is being created newly on each update or render. What it means is that when you have a complex logic to update state you simply won't use the setter directly to update state, instead you will write a complex function which in turn would call the setter with updated state something like
const handleStateChange = () => {
// lots of logic to derive updated state
updateState(newState);
}
ContextObject.Provider value={{state, handleStateChange}}>
Now in the above case everytime the parent is re-rendered a new instance of handleStateChange is created causing the Context Consumer to also re-render.
A solution to the above case is to use useCallback and memoize the state updater method and use it. However for this you would need to take care of closure issues associated with using the values within the method.
Hence it is recommended to use useReducer which returns a dispatch method that doesn't change between re-renders and you can have the manipulation logic in the reducers.
Practical observation on useReducer and useState -
UseState:
In my React Native project I've 1 screen containing 25+ different states created using useState.
I'm calling an api in useEffect (componentDidMount) and on getting the response based on some conditions, I'm setting up these 25 states, calling 25 state setter function for each function.
I've put a re-rendering counter and checked my screen is re-rendered 14 times.
re-rendering count likewise :
let count = 0;
export default function Home(props) {
count++;
console.log({count});
//...
// Rest of the code
}
UseReducer :
Then I've moved these 25 states in useReducer states, And used only single action to update these states on API response.
I've observed there is only 2 re-render.
//API calling method:
fetchData()
{
const response = await AuthAxios.getHomeData();
dispatch({type: 'SET_HOME_DATA', data: response.data});
}
//useReducer Code:
const initialStaes = {
state1: null,
state2: null,
//.....More States
state27: null,
state28: null
}
const HomeReducer = (state, action) => {
switch (action.type) {
case 'SET_HOME_DATA': {
return {
...state,
state1: (Data based on conditions),
state2: !(some Conditions ),
//....More states
state27: false
}
}
}
}
Advantage of useReducer in this case :
Using useReducer I've reduced number of re-renders on the screen, hence better performance and smoothness of the App.
Number of lines is reduced in my screen itself. It improved code readablity.
When you need to care about it
If you create a callback on render and pass it to a child component, the props of that child will change. However, when the parent renders, a regular component will rerender (to the virtual dom), even props remain the same. The exception is a classComponent that implements shouldComponentUpdate, and compares props (such as a PureComponent).
This is an optimization, and you should only care about it if rerendering the child component requires significant computation (If you render it to the same screen multiple times, or if it will require a deep or significant rerender).
If this is the case, you should make sure:
Your child is a class component that extends PureComponent
Avoid passing a newly created function as a prop. Instead, pass
dispatch, the setter returned from React.useState or a memoized
customized setter.
Using a memoized customized setter
While I would not recommend building a unique memoized setter for a specific component (there are a few things you need to look out for), you could use a general hook that takes care of implementation for you.
Here is an example of a useObjState hook, which provides an easy API, and which will not cause additional rerenders.
const useObjState = initialObj => {
const [obj, setObj] = React.useState(initialObj);
const memoizedSetObj = React.useMemo(() => {
const helper = {};
Object.keys(initialObj).forEach(key => {
helper[key] = newVal =>
setObj(prevObj => ({ ...prevObj, [key]: newVal }));
});
return helper;
}, []);
return [obj, memoizedSetObj];
};
function App() {
const [user, memoizedSetUser] = useObjState({
id: 1,
name: "ed",
age: null,
});
return (
<NameComp
setter={memoizedSetUser.name}
name={user.name}
/>
);
}
const NameComp = ({name, setter}) => (
<div>
<h1>{name}</h1>
<input
value={name}
onChange={e => setter(e.target.value)}
/>
</div>
)
Demo

componentWillReceiveProps not called after redux dispatch

I'm building a react native app and using redux to handle the state. I am running into a situation where one of my containers is not updating immediately when the redux state is changed.
Container:
...
class ContainerClass extends Component<Props, State> {
...
componentWillReceiveProps(nextProps: Object) {
console.log('WILL RECEIVE PROPS:', nextProps);
}
...
render() {
const { data } = this.props;
return <SubComponent data={data} />
}
}
const mapStateToProps = (state) => ({
data: state.data
};
export default connect(mapStateToProps)(ContainerClass);
Reducer:
...
export default function reducer(state = initalState, action) => {
switch(action.type) {
case getType(actions.actionOne):
console.log('SETTING THE STATE');
return { ...state, data: action.payload };
...
...
...
In a different random component, I am dispatching a call with the actionOne action, which I confirm prints out the relevant console.log. However, the console.log in the componentWillReceiveProps in the container is not printed.
The component that dispatches the call is a modal that has appeared over the Container, and closes automatically after the call is dispatched and the state is updated. What is weird is that although the Container isn't updated immediately, if I navigate to a different page and then back to the Container page, the state is in fact updated.
EDIT: Initial state is:
const initialState: Store = {
data: []
}
And the way I dispatch is in a different component which gets called as a new modal (using react-native-navigation) from Container:
fnc() {
...
setData(data.concat(newDatum));
...
}
Where setData and data are the redux dispatch action and the part of the store respectively that is passed in on props from the Container (which has setData and data through mapStateToProps shown above and a mapDispatchToProps which I didn't show).
I solved my problem by updating from react-native v0.56 to v0.57. Apparently there was a problem with react-redux v6 working properly in the react-native v0.56 environment.
Assuming you're using a recent version of React, componentWillReceiveProps is actually deprecated:
Using this lifecycle method often leads to bugs and inconsistencies
You can't really rely on that lifecycle hook in a number of situations. You may want to look at a slightly different approach with componentDidUpdate instead.
I think more important is to get the value after changing in state of redux rather than in which lifecycle you are getting the value . so for getting the value you can use subscribe method of redux in componentDidMount
store.subscribe( ()=> {
var updatedStoreState = store.getState();
})
I believe that getDerivedStateForProps would solve your problem.
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.data !== prevState.data) {
//Do something
} else {
//Do something else
}
}
You would check the state from the redux against the state from your component and then act accordingly.
Also, some info from the documentation that you might consider before using this method:
1. getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.
2. This method exists for rare use cases where the state depends on changes in props over time.
3. If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.
You can read more at: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops

Component calls Reselect only at the first time it renders

I'm building Multiple Select component for user to select the seasons on the post. So use can choose Spring and Fall. Here I'm using reselect to track selected objects.
My problem is that my reselect doesn't trigger one it renders at the first time. This question looks pretty long but it has many console.log() lines for clarification. So please bear with me! :)
('main.js') Here is my modal Component. Data for this.state.items is seasons = [{id: '100', value: 'All',}, { id: '101', value: 'Spring',}, { ... }]
return (
<SelectorModal
isSelectorVisible={this.state.isSelectorVisible}
multiple={this.state.multiple}
items={this.state.items}
hideSelector={this._hideSelector}
selectAction={this._selectAction}
seasonSelectAction={this._seasonSelectAction}
/>
('main.js') As you can see I pass _seasonSelectAction to handle selecting items. (It adds/removes an object to/from the array of this.state.selectedSeasonIds). selectedSeasonIds: [] is defined in the state. Let's look at the function.
_seasonSelectAction = (id) => {
let newSelectedSeasonIds = [...this.state.selectedSeasonIds, id];
this.setState({selectedSeasonIds : newSelectedSeasonIds});
console.log(this.state.selectedSeasonIds); <- FOR DEBUGGING
console.log(newSelectedSeasonIds); <- For DEBUGGING
}
I confirmed that it prints ids of selected Item. Probably providing code of SelectorModal.js is irrelevant to this question. So let's move on... :)
('main.js') Here is where I called createSelector
function mapStateToProps(state) {
return {
seasons: SelectedSeasonsSelector(state)
}
}
export default connect(mapStateToProps, null)(...);
(selected_seasons.js) Finally, here is the reselect file
import { createSelector } from 'reselect';
// creates select function to pick off the pieces of states
const seasonsSelector = state => state.seasons
const selectedSeasonsSelector = state => state.selectedSeasonIds
const getSeasons = (seasons, selectedSeasonIds) => {
const selectedSeasons = _.filter(
seasons,
season => _.contains(selectedSeasonIds, season.id)
);
console.log('----------------------');
console.log('getSeasons');
console.log(selectedSeasons); <<- You can see this output below
console.log('seaons');
console.log(seasons);
console.log('----------------------');
return selectedSeasons;
}
export default createSelector(
seasonsSelector,
selectedSeasonsSelector,
getSeasons
);
The output for system console is below
----------------------
getSeasons
Array []
seaons
undefined
----------------------
Thank you for reading this whole question and please let me know if you have any question on this problem.
UPDATE As Vlad recommended, I put SelectedSeasonsSelector inside of _renderSeasons but it prints empty result like above every time I select something. I think it can't get state.seasons, state.
_renderSeasons = () => {
console.log(this.state.seasons) // This prints as expected. But this becomes undefined
//when I pass this.state in to the SelectedSeasonsSelector
selectedSeasons = SelectedSeasonsSelector(this.state)
console.log('how work');
console.log(selectedSeasons);
let seasonList = selectedSeasons.map((season) => {
return ' '+season;
})
return seasonList
}
state.seasons and state.selectedSeasonsIds are getting undefined
Looks like you are assuming that this.setState will change redux store, but it won't.
In a _seasonSelectAction method you are calling this.setState that stores selected ids in container's local state.
However selectors are expect ids will be be stored in global redux store.
So you have to pass selected id's to redux store, instead of storing them in a local state. And this parts are looks missing:
dispatch action
use reducer to store this info into redux store.
add mapDispatchToProps handler to your connect
I'm guessing here, but it looks like confusing terms:component local state is not the same as redux store state. First one is local to your component and can be accessed through this.state attributive. Second is global data related to all of your application, stored in redux store and could be accessed by getState redux method.
I so you probably have to decide, whether to stay with redux stack or create pure react component. If pure react is your choice, than you dint have to use selectors, otherwise you have to dispatch action and more likely remove this.state.

React | Redux | Thunk - How data should be loaded from within a component?

I've got a component that uses componentWillMount to make an API call through redux to get data and update the state.
Before calling this method, I need to go to the DB and get a property upon which I'll decide if the data retrieval (from the 1st paragraph) should happen.
What I was thinking of doing (using promises) -
Fetch the property (from paragraph 2)
then, if data is needed, dispatch the normal flow (paragraph 1).
if data is not needed, carry on.
My question is WHERE should it go in your opinon.
On the one hand, it feels like a mega overkill to do it through the store. On the other hand, any chance I'll encounter side effect problems.
In addition, I could implement the logic in the component or in the action creator. What do you think is best?
Additional info:
1. I'm using redux-thunk. Changing to sagas is out of the question.
2. The property that I'm checking is in 1 reducer, while the data that needs to be fetched is in another reducer (dunno, might be problematic for some solutions.).
Option 1:
import {getData} from '....../dataActions';
import {getToken} from '......../userActions';
const MegaComponent extends React.Component {
componentWillMount() {
getToken(uid)
.then(shouldUpdate => {
if (shouldUpdate) {
getData(uid);
} else {
console.log('no need to get data');
}
})
}
}
funciton mapStateToProps(state, ownProps) {
return {
user: state.user
}
}
function mapDispatchToProps(dispatch) {
return {
getToken: (uid) => dispatch(getToken(uid)),
getData: (uid) => dispatch(getData(uid))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MegaComponent);
Option 2 (what I think should be done)
export function getToken(uid) {
return dispatch => {
return api.getToken(uid)
.then(token => {
if (token === 'what it should') {
return {type: 'NO_DATA_CHANGE_NEEDED', action: null};
} else {
// the action that handle getting data is in a different action creator.
// I either import it here and call it, or there's a better way.
}
})
}
}
UPDATE
This might come in handy for future visiotrs - getting state inside an action creator
Hey this is a very broad question, but I want to give a short advice on how I would solve it although I am not quite sure if i got it totally right.
So I would start by separating the concerns of point 1. (database call) and 2. (everything else)
You could start by writing a Component-Wrapper (HoC) that only does the database call to get the prop you need for further processing.
Once you fetched the data from db you can render the InnerComponent with the data as prop and do everything else you need there just by checking the prop you injected and trigger additional actions.
I wouldn't let my component deal with logic. keep your components all about View. Think of a component as a visual representation of a point-in-time state.
In componentWillMount you can check if the data exists, if it's not then you call your action which will fetch the data through an API and pass the data to the reducer. the reducer will 'update' the state.
the dispatch calls should be invoked from actions.js and not from your component.
So, my feeling was right - option 2 it is. Much like #guruPitka suggested.
In my action creator (that is being called by the component), I get the current app state, make an API call, compare the tokens and act accordingly.
export function getToken(uid) {
return (dispatch, getState) => {
const currentToken = getState().user.token;
return api.lastAppLaunch(uid)
.then(newToken => {
if (newToken === currentToken) {
// All is good, carry on.
} else {
dispatch(getData(uid)); //getData is from a different action creator.
}
});
}
}
And the call in the component -
this.props.getToken(uid);

Resources