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

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])

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
);

React useEffect inside const function with MobX

I have some React Redux code written in Typescript that loads some data from my server when a component mounts. That code looks like this:
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { MyAction } from 'my/types/MyAction';
export const useDispatchOnMount = (action: MyAction) => {
const dispatch = useDispatch();
return useEffect(() => {
dispatch(action);
}, [dispatch]);
};
This is simple enough - it uses the useEffect hook to do what I want. Now I need to convert the code so that it uses MobX instead of Redux for persistent state. If I have my own MobX store object called myStore, and myStore has a async method "loadXYZ" that loads a specific set of data XYZ, I know I can do this inside my component:
useEffect(() => {
async functon doLoadXYZ() {
await myStore.loadXYZ();
}
doLoadXYZ();
}, []);
This does indeed work, but I would like to put all this into a single fat arrow function that calls useEffect, much like what the useDispatchOnMount function does. I can't figure out the best way to do this. Anyone know how to do this?
EDIT: After further digging, it looks more and more like what I am trying to do with the Mobx version would break the rules of Hooks, ie always call useEffect from the top level of the functional component. So calling it explicitly like this:
export const MyContainer: React.FC = () => {
useEffect(() => {
async functon doLoadXYZ() {
await myStore.loadXYZ();
}
doLoadXYZ();
}, []);
...
};
is apparently the best way to go. Butthat raises the question: is the redux version that uses useDispatchOnMount a bad idea? Why?
You can do this if you don't use async/await in the useEffect. If you are fetching data, I would store it in myStore and use it directly out of the store instead of using async/await. It might look something like this:
export const SomeComp = observer(function SomeComp() {
const myStore = useStore() // get the store with a hook or how you are getting it
useEffect(myStore.loadXYZ, [myStore])
return <div>{myStore.theLoadedData}</div>
})
In loadXYZ you just store the data the way you want and use it. The component observing theLoadedData will re-render when it comes back so you don't need to have async/await in the component.

React get state from Redux store within useEffect

What is the correct way to get state from the Redux store within the useEffect hook?
useEffect(() => {
const user = useSelector(state => state.user);
});
I am attempting to get the current state within useEffect but I cannot use the useSelector call because this results in an error stating:
Invariant Violation: Hooks can only be called inside the body of a function component.
I think I understand why as it breaks one of the primary rules of hooks.
From reviewing the example on the Redux docs they seem to use a selectors.js file to gather the current state but this reference the mapStateToProps which I understood was no longer necessary.
Do I need to create some kind of "getter" function which should be called within the useEffect hook?
Don't forget to add user as a dependency to useEffect otherwise your effect won't get updated value.
const user = useSelector(state => state.user);
useEffect(() => {
// do stuff
}, [user]);
You can place useSelector at the top of your component along with the other hooks:
const MyComponent = () => {
...
const user = useSelector(state => state.user);
...
}
Then you can access user inside your useEffects.
I found using two useEffects to works for me, and have useState to update the user (or in this case, currUser).
const user = useSelector(state=>state.user);
const [currUser, setCurrUser] = useState(user);
useEffect(()=>{
dispatch(loadUser());
}, [dispatch]);
useEffect(()=>{
setCurrUser(user);
}, [user]);
You have to use currUser to display and manipulate that object.
You have two choices.
1 - If you only need the value from store once or 'n' time your useEffect is called and don't want to listen for any changes that may occur to user state from redux then use this approach
//import the main store file from where you've used createStore()
import {store} from './store' // this will give you access to redux store
export default function MyComponent(){
useEffect(() =>{
const user = store.getState().user;
//...
},[])
}
2 - If you want to listen to the changes that may occur to user state then the recommended answer is the way to go about
const MyComponent = () => {
//...
const user = useSelector(state => state.user);
useEffect(() => {
//...
},[])
//...
}
const tournamentinfofromstore=useSelector(state=>state.tournamentinfo)
useEffect(() => {
console.log(tournamentinfofromstore)
}, [tournamentinfofromstore])
So the problem is that if you change the state inside the useEffect that causes a rerender and then again the useEffect gets called "&&" if that component is passing data to another component will result in infinite loops.and because you are also storing that data in the child component's state will result in rerendering and the result will be infinite loop.!!
Although it is not recommended, you can use store directly in your component, even in the useEffect.
First, you have to export store from where it is created.
import invoiceReducer from './slices/invoiceSlice';
import authReducer from './slices/authSlice';
export const store = configureStore({
reducer: {
invoices: invoicesReducer,
auth: authReducer,
},
});
Then you can import it to a React Component, or even to a function, and use it.
import React, { useEffect } from 'react';
import { store } from './store';
const MyComponent = () => {
useEffect(()=> {
const invoiceList = store.getState().invoices
console.log(invoiceList)
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export default MyComponent
You can study the API for Store in here.
You can also see why this approach is not recommended in
here.
Or, if you are interested in using redux store outside a react component, take a look at this blog post.
To add on top of #Motomoto's reply. Sometimes you depend on store to be loaded before useEffect. In this case you can simply return in if the state is undefined. useEffect will rerender once the store is loaded
const user = useSelector(state => state.user);
useEffect(() => {
if(user === undefined){
return}else{
// do stuff
}}, [user]);
I'm having the same issue, The problem to the useSelector is that we cant call it into the hook, so I can't be able to update with the action properly. so I used the useSelector variable as a dependency to the useEffect and it solved my problem.
const finalImgData_to_be_assigned = useSelector((state) => state.userSelectedImg);
useEffect(()=>{
console.log('final data to be ready to assign tags : ', finalImgData_to_be_assigned.data);
}, [finalImgData_to_be_assigned ])

How to do fetch with React Hooks; ESLint enforcing `exhaustive-deps` rule, which causes infinite loop

I'm pretty new to React hooks in general, and very new to useSelector and useDispatch in react-redux, but I'm having trouble executing a simple get request when my component loads. I want the get to happen only once (when the component initially loads). I thought I knew how to do that, but I'm running into an ESLint issue that's preventing me from doing what I understand to be legal code.
I have this hook where I'm trying to abstract my state code:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
return {
data: data,
get: (props) => dispatch(actionCreators.get(props))
};
};
Behind the above function, there's a network request that happens via redux-saga and axios, and has been running in production code for some time. So far, so good. Now I want to use it in a functional component, so I wrote this:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const myState = useState();
React.useEffect(
() => {
myState.get();
return () => {};
},
[]
);
return <div>hello, world</div>;
};
What I expected to happen was that because my useEffect has an empty array as the second argument, it would only execute once, so the get would happen when the component loaded, and that's it.
However, I have ESLint running on save in Atom, and every time I save, it changes that second [] argument to be [myState], the result of which is:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const myState = useState();
React.useEffect(
() => {
myState.get();
return () => {};
},
[myState]
);
return <div>hello, world</div>;
};
If I load this component, then the get runs every single render, which of course is the exact opposite of what I want to have happen. I opened this file in a text editor that does not have ESLint running on save, so when I was able to save useEffect with a blank [], it worked.
So I'm befuddled. My guess is the pattern I'm using above is not correct, but I have no idea what the "right" pattern is.
Any help is appreciated.
Thanks!
UPDATE:
Based on Robert Cooper's answer, and the linked article from Dan Abramov, I did some more experimenting. I'm not all the way there yet, but I managed to get things working.
The big change was that I needed to add a useCallback around my dispatch functions, like so:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
const get = React.useCallback((props) => dispatch({type: 'MY_ACTION', payload:props}), [
dispatch
]);
return {
data: data,
get: get,
};
};
I must admit, I don't fully understand why I need useCallback there, but it works.
Anyway, then my component looks like this:
import * as React from 'react';
import { useState } from './my-state-file';
export default () => {
const {get, data} = useState();
React.useEffect(
() => {
get();
return () => {};
},
[get]
);
return <div>{do something with data...}</div>;
};
The real code is a bit more complex, and I'm hoping to abstract the useEffect call out of the component altogether and put it into either the useState custom hook, or another hook imported from the same my-state-file file.
I believe the problem you're encountering is that the value of myState in your dependency array isn't the same value or has a different JavaScript object reference on every render. The way to get around this would be to pass a memoized or cached version of myState as a dependency to your useEffect.
You could try using useMemo to return a memoized version of your state return by your custom useState. This might look something like this:
export const useState = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.data);
return useMemo(() => ({
data: data,
get: (props) => dispatch(actionCreators.get(props))
}), [props]);
};
Here's what Dan Abramov has to say regarding infinite loops in useEffect methods:
Question: Why do I sometimes get an infinite refetching loop?
This can happen if you’re doing data fetching in an effect without the second dependencies argument. Without it, effects run after every render — and setting the state will trigger the effects again. An infinite loop may also happen if you specify a value that always changes in the dependency array. You can tell which one by removing them one by one. However, removing a dependency you use (or blindly specifying []) is usually the wrong fix. Instead, fix the problem at its source. For example, functions can cause this problem, and putting them inside effects, hoisting them out, or wrapping them with useCallback helps. To avoid recreating objects, useMemo can serve a similar purpose.
Full article here: https://overreacted.io/a-complete-guide-to-useeffect/

Best practice to prevent state update warning for unmounted component from a handler

It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.

Resources