Solve the react-hooks/exhaustive-deps when updating redux in useEffect - reactjs

Can you help me solve useEffect riddles?
I want to satisfy the linter react-hooks/exhaustive-deps rule but also have a component which does not re-render all the time. My component should aggregate data if it is loading for the first time. This data is fetched from redux, then the summation is done and the data is stored back to redux.
As explained in this question, I could move the aggregation to the component where the data is added, but this would force me to update the arrays more often, once either stocks or transaction is added and this even I never view the aggregate.
This question also points to a post by Dan Abramov not to disable the linter rule. Which let me came to the conclusion just leaving something out in the useEffect array is not the way to go — as it is the case in my example.
Now the question is how to solve this?
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { ITransactionArray } from "../../../store/account/types";
import { IStockArray, STOCKS_PUT } from "../../../store/portfolio/types";
import { useTypedSelector } from "../../../store/rootReducer";
import { AppDispatch } from "../../../store/store";
const aggregateStockQuantities = (
stocks: IStockArray,
transactions: ITransactionArray
): IStockArray => {
console.log("run Value/aggregateStockQuantities");
const newStocks = [...stocks];
newStocks.forEach((s) => {
s.quantity = 0;
return s;
});
transactions.forEach((t) => {
newStocks.forEach((s) => {
if (s.quantity !== undefined && t.isin === s.isin) {
s.quantity += t.quantity;
}
return s;
});
});
return newStocks;
};
const Test: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
const { stocks } = useTypedSelector((state) => state.portfolio);
const { transactions } = useTypedSelector((state) => state.account);
const stockQuantities = aggregateStockQuantities(stocks, transactions);
useEffect(() => {
dispatch({
type: STOCKS_PUT,
payload: stockQuantities,
});
}, [dispatch]);
// only works if I leave out stockQuantities
// but then warning: React Hook useEffect has a missing dependency: 'stockQuantities'. Either include it or remove the dependency array react-hooks/exhaustive-deps
// Render
}
Update #1: Had to change the aggregateStockQuantities so quantity is null at the start.

I would advice making a selector that only selects aggregateStockQuantities from the redux state.
And move the const stockQuantities = aggregateStockQuantities(stocks, transactions); inside the useEffect hook.

Related

Adding recommended dependency by linter results in an infinite re-rendering loop

Here's my code:
SectionState.js:
import { React, useState, useEffect } from "react";
import QuestionContext from "./QuestionContext";
import questions from "../data/questions.json";
import { useNavigate } from "react-router-dom";
const SectionState = (props) => {
// set questions from json to an array of 4 elements
const newQuestions = Object.keys(questions.content).map(
(key) => questions.content[key].question
);
//useState for Question state
const [currentQuestion, setCurrentQuestion] = useState(0);
const newQuestionsArr = {
qID: 0,
questionTxt: newQuestions[currentQuestion],
}
const [questionCtx, setQuestionCtx] = useState(newQuestionsArr);
const navigate = useNavigate()
useEffect(() => {
setQuestionCtx(prevState => ({
...prevState,
qID: currentQuestion,
questionTxt: newQuestions[currentQuestion],
}));
}, [currentQuestion]);
const updateNextQuestion = () => {
if (!(currentQuestion >= newQuestions.length)) {
setCurrentQuestion((nextCurrentQuestion) => nextCurrentQuestion + 1);
}
else{
navigate('/result')
}
};
const updatePrevQuestion = () => {
if (currentQuestion <= 0) {
console.log(`No more questions`);
} else {
setCurrentQuestion((prevCurrentQuestion) => prevCurrentQuestion - 1);
}
};
return (
<QuestionContext.Provider
value={{ questionCtx, updateNextQuestion, updatePrevQuestion }}>
{props.children}
</QuestionContext.Provider>
);
};
export default SectionState;
Linter throws the following warning
React Hook useEffect has a missing dependency: 'newQuestions'. Either include it or remove the dependency array
If I add newQuestions in the dependency array, it results in re-rendering loop. I can't declare either newQuestions or questionCtx state inside useEffect as it is used elsewhere in the code.
I can see that I have to update the questionTxt. What should I do here to update the said value and remove the warning?
A new newQuestions object is created at every render. usEffect is triggered when one of the dependencies changes. Hence the infinite render loop.
If the newQuestions is a constant that depends on a json you import from a file, you could move it outside of the component as mentioned in #CertainPerformance answer. codesandbox
If for some reasons you want to declare the newQuestions variable inside of your component, you could use useMemo hook. codesandbox
You could disable the lint rule which is probably not a good idea.
I'm not really sure what you trying to achieve, but it seems like you probably don't need the useEffect and might have some redundant states.
Maybe you could use only one state, and get rid of the useEffect. You only need one state to keep track of the current question, and calculate other variables in each render.
const [currentQuestion, setCurrentQuestion] = React.useState(0);
const questionCtx = React.useMemo(
() => ({
qId: currentQuestion,
questionTxt: newQuestions[currentQuestion]
}),
[currentQuestion]
);
codesandbox
You could read more about managing state in the react beta documentation.
The problem is that the newQuestions array is computed anew each time the function runs, and so won't be === to the old array (and so will run every render). If newQuestions depended on other React values, the usual fix would be to memoize it with useMemo, but because it looks to depend only on a static imported value, you may as well just declare it outside the component (which means it doesn't need to be a dependency anymore either).
It also looks like you don't care about the keys, only the values - so, easier to use Object.values than Object.keys.
import { React, useState, useEffect } from "react";
import QuestionContext from "./QuestionContext";
import questions from "../data/questions.json";
import { useNavigate } from "react-router-dom";
const newQuestions = Object.values(questions.content).map(
val => val.question
);

Rendered more hooks than during the previous render using useEffect

I have a component with an array of objects, which among other things i am filtering based on a string.
Problem is when I try to set the return of this filter to the local state, it's throwing errors that i am not quite understanding the reason.
import React, { useState, useEffect } from 'react';
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag'
const ProductsGrid = () => {
const [productsList, setProductsList] = useState([]);
const { loading, data } = useQuery(GET_PRODUCTS);
if (loading) return <div><h4>bla bla bla</h4></div>
const { merchants } = data;
let filtered = [];
merchants.map(merchant => {
merchant.products.map(product => {
if (product.name.includes('Polo')) {
filtered.push(product);
}
})
})
console.log({ filtered });
}
This is printing the following:
So, because I want this array in my state, I decided to do this: setProductsList(filtered);
and what happened after inserting this line was this:
It started rendering multiple times. I assumed that, every time the state changes, it re-renders the component (correct me if im wrong). I don't know why it did it multiple times though.
So, I thought on using useEffect to achieve the expected behaviour here.
useEffect(() => {
console.log('useeffect', data);
if (data) {
const { merchants } = data;
console.log({merchants })
let filtered = [];
merchants.map(merchant => {
merchant.products.map(product => {
if (product.name.includes('Polo')) {
filtered.push(product);
// console.log({ filtered });
}
})
})
console.log({ filtered });
setProductsList(filtered);
}
}, [data])
and the output was it:
So, I am understanding what's going on here and what is this last error about.
I assume my approaching is towards the right direction, using useEffect to run the function only once.
Your problem is due to the useEffect call occurring after the if (loading) condition, which returns early.
Calling hooks after a conditional return statement is illegal as it violates the guarantee that hooks are always called in exactly the same order on every render.
const { loading, data } = useQuery(GET_PRODUCTS);
const [productsList, setProductsList] = useState([]);
if (loading)
return (
<div>
<h4>bla bla bla</h4>
</div>
); // <-- Cannot use hooks after this point
useEffect(/* ... */)
To fix, move the useEffect call to be before the conditional.

What is the best way to use redux action in useEffect?

I have a React Component like shown bellow (some parts are ommited) and I'm using redux for state management. The getRecentSheets action contains an AJAX request and dispatches the response to redux which updates state.sheets.recentSheets with the response's data.
All this works as expected, but on building it throws warning about useEffect has a missing dependency: 'getRecentSheets'. But if I add getRecentSheets to useEffect's dependency array it starts to rerun indefinitely and thus freezes the app.
I've read React documentation about the useEffect hook https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies but it doesn't provide a good example for such usecase. I suppose it is something with useCallback or react-redux useDispatch, but without examples I'm not sure how to implement it.
Can someone please tell me what the most concise and idiomatic way to use redux action in useEffect would be and how to avoid warnings about missing dependencies?
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import SheetList from '../components/sheets/SheetList';
import { getRecentSheets } from '../store/actions/sheetsActions';
const mapStateToProps = (state) => {
return {
recentSheets: state.sheets.recentSheets,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRecentSheets: () => dispatch(getRecentSheets()),
}
}
const Home = (props) => {
const {recentSheets, getRecentSheets} = props;
useEffect(() => {
getRecentSheets();
}, [])
return <SheetList sheets={ recentSheets } />
};
export default connect(mapStateToProps, mapDispatchToProps) (Home);
After all, it seems that correct way will be as follows:
// ...
import { useDispatch } from 'react-redux';
import { getRecentSheets } from '../store/actions/sheetsActions';
const Home = props => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getRecentSheets());
}, [dispatch])
// ...
};
This way it doesn't complain about getRecentSheets missing in dependencies array. As I understood from reading React doc on hooks that's because it's not defined inside the component. Though I'm new to frontend and I hope I didn't mess something up here.
Passing an empty array in your hook tells React your hook function will not have any dependent values from either props or state.
useEffect(() => {
getRecentSheets();
}, [])
The infinite loop arises when you declare the dispatcher as a dependency on the hook. When the component is initialized, props.recentSheets hasn't been set, and will rerender once you make your AJAX call.
useEffect(() => {
getRecentSheets();
}, [getRecentSheets])
You could try something like this:
const Home = ({recentSheets}) => {
const getRecentSheetsCallback = useCallback(() => {
getRecentSheets();
})
useEffect(() => {
getRecentSheetsCallback();
}, [recentSheets]) // We only run this effect again if recentSheets changes
return <SheetList sheets={ recentSheets } />
};
No matter how many times Homes re-renders, you retain the memoized function to your dispatch call.
Alternatively, you may have encountered find similar patterns utilizing local state and then make your effect "depend" on sheets.
const [sheets, setSheets] = useState(recentSheets)
Hope this helps
I would add a check to see if recentSheets exists or not, using that as my dependency.
useEffect(() => {
if (!recentSheets) getRecentSheets();
}, [recentSheets])

useEffect goes in infinite loop when combined useDispatch, useSelector of Redux

I try to code with react-hook and 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 componentwillreceiveprop
# Action
export const actGetProductRequest = id => dispatch =>
callApi(`products/${id}`, "GET").then(res =>
dispatch({
type: types.EDIT_PRODUCT,
payload: { product: res.data }
})
);
------
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import useForm from "./useForm";
import { actGetProductRequest } from "../../actions";
...
const { history, match } = props;
const getProduct = useSelector(state => state.itemEditing);
const dispatch = useDispatch();
const { values, setValues, handleChange, handleSubmit } = useForm(
getProduct,
history
);
useEffect(() => {
if (match) {
let id = match.params.id;
dispatch(actGetProductRequest(id));
}
setValues(()=>({...getProduct}));
},[match,dispatch, setValues, getProduct]);
And I tried to call API and return the product, but the site always loop render infinite. Continuous rendering cannot be edited the form. like the
Can anyone please help me out how to resolve this issue...
p.s: with code here, it run ok. But I want to use with redux, and I pass the code calling axios to the dispatch and redux to return the new changed state
useEffect(() => {
if (match) {
let id = match.params.id;
callApi(`products/${id}`, "GET", null).then(res => {
const data = res.data;
setValues(()=>({...data}));
});
}
const clearnUp = () => setValues(false);
return clearnUp;
},[match, setValues]);
p.s: full code
code
Just get the id from the history > match
const { history, match } = props;
const id = match ? match.params.id : null;
You don't need to add dispatch to the dependencies. Instead of match, use id. And you can skip the method references. Since setValues is basically a setState call, it can be skipped too (Read React docs). But if you do want to use any function references in dependecies, make sure you wrap them with useCallback.
useEffect(() => {
if (id) {
dispatch(actGetProductRequest(id));
}
setValues(()=>({...getProduct}));
},[id, getProduct]);
Your main issue might be with the match object. Since, history keeps changing, even if the id is the same.

Inside useEffect is never run in react-redux application

I have the following component. I did debugging. The function inside useEffect never get called. The code reaches to useEffect but then does not enter inside, and therefore does not fetch records from the database. Any ideas why this is happening?
import * as React from 'react';
import { useEffect } from 'react';
import { connect } from 'react-redux';
import { FetchAssignmentData } from './AssignmentDataOperations'
const AssignmentComprehensive = (props) => {
useEffect(() => {
if (props.loading != true)
props.fetchAssignment(props.match.params.id);
}, []);
if (props.loading) {
return <div>Loading...</div>;
}
if (props.error) {
return (<div>{props.error}...</div>)
}
//these are always null
const assignmentId = props.assignmentIds[0];
const assignment = props.assignments[assignmentId];
return (
//this throws error since the values are never fetched from db
<div>{props.assignments[props.assignmentIds[0]].title}</div>
);
}
const mapStateToProps = state => ({
assignmentIds: state.assignmentReducer.assignmentIds,
assignments: state.assignmentReducer.assignments,
submissions: state.assignmentReducer.submissions,
rubric: state.assignmentReducer.rubric,
loading: state.assignmentReducer.loading,
error: state.assignmentReducer.error
})
const mapDispatchToProps = dispatch => {
return { fetchAssignment: (id) => dispatch(FetchAssignmentData(id)) };
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AssignmentComprehensive);
Because of useEffect second argument:
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
So it runs only once (when the props.loading istrue) and never again.
You seem to have 3 dependencies:
useEffect(() => {
...
}, [props.loading, props.fetchAssignment, props.match.params.id])
See also: react-hooks/exhaustive-deps eslint rule.

Resources