how to create a callbackRef with dependencies - reactjs

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

Related

Collision betwen useState interdependency and useEffect in React Hooks

I have a useEffect with dependency A and a function where something will trigger if B is true, looking like this:
const [docs, setDocs] = useState(null);
const [update, setUpdate] = useState(false);
useEffect(() => {
if (update == true) {
// do this
}
}, [docs]);
In another function, i'm setting update to true, and I update docs.
const deleteDocument = (a) => {
const newArray = a;
setUpdate(true);
setDocs(newArray);
};
The problem is that the useEffect will trigger before update is actually updated, therefore not executing the inner function.
Since I would like to do this the proper way (not adding a timeout), what would be the standard way of dealing with this?
The problem is
setUpdate(true);
setDocs(newArray);
Setting a state is an async function, where you cannot wait till update is set before next state change occurs.
useRef (one of many solutions)
const docs = useRef();
const [update, setUpdate] = useState(false);
useEffect(() => {
if (update == true) {
// do this
// use can use "docs" here as "docs.current"
}
}, [update]);
const deleteDocument = (a) => {
const newArray = a;
// set the new value of Docs. Changing 'ref' doesnot trigger a render
docs.current = newArray;
setUpdate(true);
};
And you can either create update to useRef or docs to useRef. Anything should work
If you need update to be updated, that means that your useEffect depends on update also. So you should add update as a dependency in the dependency array.
useEffect(() => {
if (update == true) {
// do this
}
}, [update, docs]);
Setting states (setState) is an asynchronous function so you can't rely that one will be executed before or after another.

Is React's useEffect dependency array an anti pattern

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

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

useEffect props callback function causing infinite loop

I have a problem very similar to this - How do I fix missing dependency in React Hook useEffect.
There is one key difference - I am passing a fetch function to a child component to be called from useEffect, so I can't simply move the function into the body of the effect. The fetch function is re-created every render and causes an infinite loop. I have other local component state that I want to cause the effect to fire.
I basically have a Container Component and a Presentational component. MyPage is the parent of MyGrid and sets up all the redux state:
const MyPage = () => {
const dispatch = useDispatch();
const items= useSelector(selectors.getItems);
const fetching = useSelector(selectors.getFetching);
const fetchItems = opts => dispatch(actions.fetchItems(opts));
return (
<>
{fetching && <div>Loading...</div>}
<h1>Items</h1>
<MyGrid
items={items}
fetchItems={fetchItems}
fetching={fetching}
/>
</>
);
}
const MyGrid = ({ fetchItems, items, fetching }) => {
const [skip, setSkip] = useState(0);
const take = 100;
const [sorts, setSorts] = useState([]);
// when query opts change, get data
useEffect(() => {
const options = { skip, take };
const sortString = getSortString(sorts);
if (sortString) options['sort'] = sortString;
fetchItems(options);
}, [fetchItems, skip, sorts]);
In "MyGrid" "skip" and "sorts" can change, and should make the effect fire.
"fetchItems" is re-created everytime and causes an infinite loop. This
is my problem.
Now, the eslint react-hooks/exhaustive-deps rule is making me put fetchItems in the dependency list. I have prettier setup to autofix on save which makes it worse.
I know the Container/Presentational pattern is out of style with hooks, but it works good for my situation - I may allow swapping out MyGrid for MyList dynamically and don't want to repeat all the redux stuff in each child component.
I tried to useCallback and useMemo, but eslint just makes me put all the same dependencies in it's dependency array parameter.
Is there a way other than disabling the eslint rule
// eslint-disable-next-line react-hooks/exhaustive-deps
to make this work?
There are two ways, you can make it work.
Firstly, using useCallback for fetchItem like
const fetchItems = useCallback(opts => dispatch(actions.fetchItems(opts)), [dispatch, actions]);
Secondly using dispatch directly in child component
const dispatch = useDispatch();
useEffect(() => {
const options = { skip, take };
const sortString = getSortString(sorts);
if (sortString) options['sort'] = sortString;
dispatch(actions.fetchItems(options));
}, [dispatch, actions, skip, sorts]);

Resources