Is React's useEffect dependency array an anti pattern - reactjs

I have a question regarding useEffect in React and the dependency array. As far as I understand useEffect is there to handle side effects of state changes.
Let's say I'm creating an application like Zoom. So for the receiver call I have code that handles the request for a call in a useEffect when a local state variable called "callState" is equal to answering:
const [localStream, setLocalStream] = useState()
const [remoteStream, setRemoteStream] = useState()
const [remoteRequest, setRemoteRequest] = useState()
const [currentUser, setCurrentUser] = useState()
const [callState, setCallState] = useState()
useEffect(() => {
const answerCall = async () => {
console.log("answering")
if (!remoteRequest || callState !== CallState.Answering) return
console.log('remoteStream', remoteStream)
console.log('localStream', localStream)
console.log('currentUser', currentUser)
}
answerCall()
}, [localStream, remoteStream, remoteRequest, currentUser, callState])
The issue here is that I only want to call the answerCall useEffect when callState changes but it does need to use many of the state variables. I have the conditional if (!remoteRequest || callState !== CallState.Answering) return so I do prevent the useEffect from running if callState isn't answered, however it seems weird that I continuously call a useEffect really only meant to run when callState changes and I need a conditional to bail early if one of the state variables such as localStream changes (if I'm changing the stream to the back facing camera for example). It seems like this design is prone to errors and bugs even if it is more declarative.
I added the console.log('answering') to show my point. If the user logs in, the callState is set to hanging up, the current user refreshes an attribute, the localStream changes.. in all these cases it will log 'answering' to the console.
I can add '// eslint-disable-next-line react-hooks/exhaustive-deps' and only add the callState but there are many articles that warn against this:
https://dev.to/aman_singh/why-effects-shouldn-t-lie-about-their-dependencies-1645
https://betterprogramming.pub/stop-lying-to-react-about-missing-dependencies-10612e9aeeda
What am I missing here?

You need to add only callState into array dependency and move the rest of the logic to a separate method and call the method inside useEffect only when the value of callState is changed.
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [remoteRequest, setRemoteRequest] = useState();
const [currentUser, setCurrentUser] = useState();
const [callState, setCallState] = useState();
const answerCall = useCallback(async() => {
console.log('remoteStream', remoteStream);
console.log('localStream', localStream);
console.log('currentUser', currentUser);
console.log('remoteStream', remoteRequest);
}, [localStream, remoteStream, remoteRequest, currentUser]);
useEffect(() => {
(async () => {
if (callState) {
answerCall()
}
)()
}, [callState]);

Related

How can I stop react from making multiple network request simultaneously

I am making a get request to get data from my rest API, and when I have the data react will keep on making the same request simultaneously multiple times.
this is the code:
export default function Transaction() {
const [transactions, setTransaction] = useState([]);
const [loading, setLoading] = useState(true)
const { id } = useParams();
// this is where I am getting the problem
useEffect(() => {
const fetchTransc = async () => {
const res = await axios.get(`http://localhost:4000/api/get-records/${id}`);
setTransaction(res.data)
setLoading(false)
console.log(res.data)
};
fetchTransc();
},[id,transactions]);
The second argument of the UseEffect hook is the dependency array. It tells react which variables to watch for changes. When a dependency changes or when the component is mounted for the first time, the code inside the hook is executed.
In your case the array indicates that each time “id” or “transations” change, the hook should be executed.
Since setTransation is called in the fetch function, that will trigger the hook again because “transations” is present in hook’s the dependency array.
Each time the transactions state variable is set with a brand a new object fetched from the url, react will trigger the useEffect hook.
If “transations” is removed from the hook’s dependency array, this should work fine. Maybe also adding an IF to check the “id” value could be useful to prevent errors.
useEffect(() => {
const fetchTransc = async () => {
if(id != null) {
const res = await axios.get(`http://localhost:4000/api/get-records/${id}`);
setTransaction(res.data)
setLoading(false)
console.log(res.data)
}
};
fetchTransc();
},[id]);

Why does a useSelector result freeze my app when paired with a useEffect state update?

EDIT: Question is invalid. There was a failure to properly use shallowEqual... no idea why array setter was caught and not string but I suppose it is immaterial.
This is a very peculiar bug:
const slice = useSelector(state => state.slice)
const [stringTest, setStringTest] = useState('')
const [arrayTest, setArrayTest] = useState([])
// APP FREEZES
useEffect(() => {
console.log('This never even fires')
setArrayTest(['failure'])
}, [slice])
// BUT THIS WORKS
useEffect(() => {
setStringTest('success')
}, [slice])
// AND THIS WORKS
useEffect(() => {
setArrayTest(['success'])
}, [])
Why would my useSelector result, as a dependency, exclusively prohibit updating state as an array or object? Is there a workaround?

Custom hooks not working properly with useEffect

I wasn't sure on a title for this issue, but I can better explain it here. I have a custom hook that is relying on some information. Part of the information must rely on an async call.
I have three situations happening.
Tried conditionally rendering the custom hook, but react does not like that due to rendering more hooks on a different render.
The custom hook is only mounting once and not passing in the updated information it needs.
I tried passing the dependency to the custom hook and it causes an infinite loop.
Here is a small example of what I'm doing.
Custom Hook:
export function useProducts(options){
const [products, setProducts] = useContext(MyContext)
useEffect(() => {
// only gets called once with `options.asyncValue` === null
// needs to be recalled once the new value is passed in
const loadProducts = async () => {
const data = await asyncProductReq(options)
setProducts(data)
}
loadProducts()
}, []) // if I pass options here it causes the infinite loop
return [products, setProducts]
}
Inside function calling:
export function(props){
const [asyncValue, setValue] = useState(null)
useEffect(() => {
const loadValue = async () => {
const data = await asyncFunc()
setValue(data)
}
loadValue()
}, []}
const options = {...staticValues, asyncValue}
const [products] = useProducts(options)
return (
<h2>Hello</h2>
)
}
I know that I need to pass the options to be a dependency, but I can't figure out why it's causing an infinite reload if the object isn't changing once the async call has been made inside the func.
You were correct in adding options in the dependencies list for your custom hook.
The reason it is infinitely looping is because options IS constantly changing.
The problem is you need to take it one step further in the implementation and make use of the useMemo hook so options only changes when the async value changes, instead of the whole component changing.
So do this:
const options = React.useMemo(() => ({...staticValues, asyncValue}), [asyncValue])

useEffect re-renders too many times

I have this component, that needs to fetch data, set it to state and then pass it to the children.
Some of the data also needs to be set in context.
My problem is that using useEffect, once called the API, it will re-render for each setvalue() function I need to execute.
I have tried passing to useEffect an empty [] array, still getting the same number of re-renders, due to the fact that the state is changing.
At the moment the array is containg the set...functions to prevent eslint to throw warnings.
Is there a better way to avoid this many re-renders ?
const Home = (props) => {
console.log("TCL: Home -> props", props);
const classes = useStyles();
const [value, setValue] = React.useState(0);
//CONTEXT
const { listSavedJobs, setListSavedJobs, setIsFullView} = useContext(HomeContext);
const {
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
} = useContext(UserContext);
// STATE
const [searchSettings, setSearchSettings] = useState([]);
const [oppData, setOppData] = useState([]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleChangeIndex = index => {
setValue(index);
};
//API CALLS
useEffect(() => {
const triggerAPI = async () => {
setIsFullView(false);
const oppResponse = await API.getOpportunity();
if(oppResponse){
setOppData(oppResponse.response);
}
const profileResponse = await API.getUserProfile();
if(profileResponse){
setUserName(profileResponse.response.first_name);
setUserLastName(profileResponse.response.last_name);
setUserEmail(profileResponse.response.emailId);
}
const profileExtData = await API.getUserProfileExt();
if(profileExtData){
setAvatarProfile(profileExtData.response.avatar);
setListSavedJobs(profileExtData.response.savedJobs);
setSearchSettings(profileExtData.response.preferredIndustry);
}
};
triggerAPI();
}, [
setOppData,
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
setListSavedJobs,
setIsFullView,
]);
...```
Pass just an empty array to second parameter of useEffect.
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
Source
Edit: Try this to avoid rerenders. Use with caution
Only Run on Mount and Unmount
You can pass the special value of empty array [] as a way of saying “only run on mount and unmount”. So if we changed our component above to call useEffect like this:
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
Then it will print “mounted” after the initial render, remain silent throughout its life, and print “unmounting…” on its way out.
Prevent useEffect From Running Every Render
If you want your effects to run less often, you can provide a second argument – an array of values. Think of them as the dependencies for that effect. If one of the dependencies has changed since the last time, the effect will run again. (It will also still run after the initial render)
const [value, setValue] = useState('initial');
useEffect(() => {
// This effect uses the `value` variable,
// so it "depends on" `value`.
console.log(value);
}, [value])
For more clarification useEffect
If you are using React 18, this won't be a problem anymore as the new auto batching feature: https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
If you are using an old version, can refer to this solution: https://statics.teams.cdn.office.net/evergreen-assets/safelinks/1/atp-safelinks.html

how to create a callbackRef with dependencies

This blog post and the official docs show how to use useCallback to create a callback ref.
But neither one has an example of a case where useCallback has dependencies.
How should I set that up?
For example, if I do the following, it won't work, because the callback will be triggered without any arguments whenever myDependency changes.
const [myDependency, setMyDependency] = useState();
const myRef = useCallback(node => {
doSomeMagic(node, myDependency);
}, [myDependency]);
I think the best way to do this is to split the logic out into a useCallback and a useEffect. useCallback is used to keep track of the node. useEffect is used to trigger whenever the node changes OR myDependency changes.
const [node, setNode] = useState();
const [myDependency, setMyDependency] = useState();
const myRef = useCallback(node => {
setNode(node);
}, []);
useEffect(() => {
doSomeMagic(node, myDependency);
}, [node, myDependency]);

Resources