Function never uses updated state - reactjs

In the sample component below, myFunc's state1 is never updated while the console.log in the useEffect outputs the updated state correctly. What could be the reason for this?
const TestComponent = () => {
const [state1, setState1] = useState();
useEffect(() => {
console.log(state1);
}, [state1]);
const myFunc(() => {
const newState1 = getNewState1();
console.log(state1); // never outputs updated state, even when myFunc is called multiple times
if (state1 !== newState1) {
console.log('updated state');
setState1(newState1);
}
});
}
Obviously, my real component is much more complicated, but the only time setState1 is called is in myFunc which is confirmed by the useEffect.
Edit:
const TestComponent2 = () => {
useFocusEffect(
useCallback(() => {
myFunc();
}),
);
};
I am trying to call myFunc when TextComponent2 is focused/loaded. I realize that useEffect with no dependencies may be the best option here. Thanks!

I was using useCallback with no relevant dependencies which most likely caused state1 to be the same.

Related

setState inside useEffect or useCallback, dependency issue

I'm trying to remove console warnings from my code, but I'm confused about how to solve this dependency issue.
I have a useEffect hook, that calls a method, removeMessage, which is defined inside my component. I get a warning that this should be in the useEffect dependency array, but if I do that, I create an infinite loop, since the function reference is re-created when the component rerenders.
const Component = () => {
const [list, setList] = useState();
const removeMessage = (message: string) => {
const list = list.filter(x => x.message !== message);
setList(list);
}
useEffect(() => {
...
removeMessage("test");
});
So I read that I'm supposed to use the useCallback hook, to ensure the reference is not changed:
const removeMessage = useCallback((message: string) => {
const list = list.filter(x => x.message !== message);
setList(list);
}, [list]);
But, unless I provide my list as a dependency for that hook, I will get a similar warning. But if I do, I create yet another infinite loop.
This code, and usage of useEffect, is propably bad practice, but I don't know how to work around it, since my method removeMessage is dependent on the state to do its filtering.
Thank you.
I think this should work (using the setState function variant). That way you do not have a dependency on list (and it is also more correct in edge cases)
const Component = () => {
const [list, setList] = useState<Array<{ message: string }>>([]);
const removeMessage = useCallback((message: string) => {
setList(prev => prev.filter(x => x.message !== message));
}, []);
useEffect(() => {
removeMessage('test');
}, [removeMessage]);
};

Debounce or throttle with react hook

I need to make an api request when a search query param from an input fields changes, but only if the field is not empty.
I am testing with several answers found on this site, but can't get them working
Firstly this one with a custom hook
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
now in my component I do this
const debouncedTest = useDebounce(() => {console.log("something")}, 1000);
but this seems to gets called every rerender regardless of any parameter, and I need to be able to call it inside a useEffect, like this
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
which of course does not work
Another approach using lodash
const throttledTest = useRef(throttle(() => {
console.log("test");
}, 1000, {leading: false}))
But how would i trigger this from the useEffect above? I don't understand how to make this work
Thank you
Your hook's signature is not the same as when you call it.
Perhaps you should do something along these lines:
const [state, setState] = useState(''); // name it whatever makes sense
const debouncedState = useDebounce(state, 1000);
useEffect(() => {
if (debouncedState) functionCall(debouncedState);
}, [debouncedState])
I can quickly point out a thing or two here.
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
technically you can't do the above, useEffect can't be nested.
Normally debounce isn't having anything to do with a hook. Because it's a plain function. So you should first look for a solid debounce, create one or use lodash.debounce. And then structure your code to call debounce(fn). Fn is the original function that you want to defer with.
Also debounce is going to work with cases that changes often, that's why you want to apply debounce to reduce the frequency. Therefore it'll be relatively uncommon to see it inside a useEffect.
const debounced = debounce(fn, ...)
const App = () => {
const onClick = () => { debounced() }
return <button onClick={onClick} />
}
There's another common problem, people might take debounce function inside App. That's not correct either, since the App is triggered every time it renders.
I can provide a relatively more detailed solution later. It'll help if you can explain what you'd like to do as well.

Is this useEffect() unecessarily expensive?

This is a very simple version of my code:
const MyComponent = (props)=>{
const [randVar, setRandVar] = useState(null);
const randomFunction = ()=>{
console.log(randVar, props);
};
useCustomHook = ()=>{
useEffect(()=>{
document.addEventListener('keydown', randomFunction);
return ()=>{
document.removeEventListener('keydown', randomFunction);
}
}, [props, randVar]);
}
useCustomHook();
...
};
I want randomFunction to log accurate values for randVar and props (i.e. logs update when those variables change values), but I'm concerned that adding an event listener and then dismounting it every time they change is really inefficient.
Is there another way to get randomFunction to log updated values without adding props and randVar as dependencies in useEffect?
A few things, your useEffect does not need to be in that custom hook at all... It should probably look like this:
const MyComponent = (props)=>{
const [randVar, setRandVar] = useState(null);
const randomFunction = useCallback(()=>{
console.log(randVar, props);
}, [props, randVar]);
useEffect(()=>{
document.addEventListener('keydown', randomFunction);
return ()=>{
document.removeEventListener('keydown', randomFunction);
}
}, [randomFunction]);
...
};
useCallback will keep your function from being redefined on every render, and is the correct dependency for that useEffect as well. The only thing bad about the performance ehre is that you are logging props so it needs to be in the dependency array of the useCallback and since it is an object that may get redefined a lot, it will cause your useCallback to get redefined on nearly every render, which will then cause your useEffect to be fired on nearly every render.
My only suggestion there would be to separate your logging of props from where you log changes to randVar.
I don't think you need randVar as a dependency and if you had ESLint, it would tell you the same since you never acutally reference randVar in the effect.
If you don't want the function to get rebuilt over and over, you need to either memoize it or useRef. Unfortunately, once it's a ref it's not reactive. Maybe I'm overthinking this, but you could have a ref that you update in an effect with the new value and print out the value of that ref in the callback you pass to randFunction. See my example below.
I don't like how I did this, but it doesn't remake the function. I'm trying to think how I could do it better, but I think this works how you want.
const {
useRef,
useState,
useEffect
} = React;
const useCustomHook = (fn) => {
useEffect(() => {
document.addEventListener("keydown", fn);
return () => {
document.removeEventListener("keydown", fn);
};
}, [fn]);
};
const MyComponent = (props) => {
const [randVar, setRandVar] = useState("");
const randVarRef = useRef("");
useEffect(() => {
randVarRef.current = randVar;
}, [randVar]);
const randFn = useRef(() => {
console.log(randVarRef.current);
});
useCustomHook(randFn.current);
return React.createElement("input", {
type: "text",
value: randVar,
onChange: e => {
setRandVar(e.target.value);
}
});
};
ReactDOM.render(
React.createElement(MyComponent),
document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="app"></div>

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

React: Trying to rewrite ComponentDidUpdate(prevProps) with react hook useEffect, but it fires when the app starts

I'm using a componentDidUpdate function
componentDidUpdate(prevProps){
if(prevProps.value !== this.props.users){
ipcRenderer.send('userList:store',this.props.users);
}
to this
const users = useSelector(state => state.reddit.users)
useEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
but it I get the message 'users changed' when I start the app. But the user state HAS NOT changed at all
Yep, that's how useEffect works. It runs after every render by default. If you supply an array as a second parameter, it will run on the first render, but then skip subsequent renders if the specified values have not changed. There is no built in way to skip the first render, since that's a pretty rare case.
If you need the code to have no effect on the very first render, you're going to need to do some extra work. You can use useRef to create a mutable variable, and change it to indicate once the first render is complete. For example:
const isFirstRender = useRef(true);
const users = useSelector(state => state.reddit.users);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log('users changed')
console.log({users})
}
}, [users]);
If you find yourself doing this a lot, you could create a custom hook so you can reuse it easier. Something like this:
const useUpdateEffect = (callback, dependencies) => {
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
return callback();
}
}, dependencies);
}
// to be used like:
const users = useSelector(state => state.reddit.users);
useUpdateEffect(() => {
console.log('users changed')
console.log({users})
}, [users]);
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
As from: Using the Effect Hook
This, it will be invoked as the component is painted in your DOM, which is likely to be closer to componentDidMount.

Resources