I was trying to setState and use the new value inside of same function.
Functions:
const { setTasks, projects, setProjects } = useContext(TasksContext)
const [input, setInput] = useState('')
const handleNewProject = () => {
setProjects((prevState) => [...prevState, {[input]: {} }])
tasksSetter(input)
setInput('')
}
const tasksSetter = (findKey) => {
const obj = projects?.find(project => Object.keys(project)[0] === findKey)
const taskArray = Object.values(obj)
setTasks(taskArray)
}
Input:
<input type='text' value={input} onChange={(e) => setInput(e.target.value)}></input>
<button onClick={() => handleNewProject()}><PlusCircleIcon/></button>
I understand, that when we get to tasksSetter's execution my projects state hasn't been updated yet. The only way to overcome it, that I can think of, is to use useEffect:
useEffect(() => {
tasksSetter(input)
}, [projects])
But I can't really do that, because I would like to use tasksSetter in other places, as onClick function and pass another values as argument as well. Can I keep my code DRY and don't write the same code over again? If so, how?
Here's something you can do when you can't rely on the state after an update.
const handleNewProject = () => {
const newProjects = [...projects, {[input]: {} }];
setProjects(newProjects);
tasksSetter(input, newProjects);
setInput('')
}
const tasksSetter = (findKey, projects) => {
const obj = projects?.find(project => Object.keys(project)[0] === findKey)
const taskArray = Object.values(obj)
setTasks(taskArray)
}
We make a new array with the update we want, then we can use it to set it to the state and also pass it to your tasksSetter function in order to use it. You do not use the state itself but you do get the updated array out of it and the state will be updated at some point.
I created a custom hook to store Objects in a useState hook and allow changing properties without loosing the other entries.
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
setState({
...state,
...newState
});
}
];
};
This hook works in my component but doesn't when I assign it to my context.
Here is what I did:
I created a context:
export const navigation = createContext();
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/store.js:40-83
I created a useObject variable and assigned it as value to my Context Provider
<navigation.Provider value={useObject()}>
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/Layout.js:234-284
I load the context via useContext and change its value
const [navigationState, setNavigationState] = useContext(navigation);
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js:476-616
Result:
The context always stores the new entry and removes all existing entries.
Anyone knows why ?
Here is the Sandbox link. You can test it by clicking the filter button. I expected to see {search:true, icon: 'times'} as context value. Thx!
https://codesandbox.io/s/keen-glitter-3nob7?file=/src/App.js
There is one important things to note here. useEffect in App.js is run once and hence the onClick function set with setNavigationState will use the values from its closure at the point at which it is defined i.e initial render.
Due to this, when you call the function within Header.js from context's the value along with the localState are being reset to the initial value.
SOLUTION 1:
One solution here is to use callback approach to state update. For that you need to modify your implementation on useObject a bit to provide the use the capability to use the callback value from setState
const useObject = initialValue => {
const [state, setState] = useState(initialValue);
return [
state,
newState => {
if(typeof newState === 'function') {
setState((prev) => ({ ...prev, ...newState(prev)}));
} else {
setState({
...state,
...newState
});
}
}
];
};
and then use it in onContextClick function like
const onContextClick = () => {
setState(prevState => {
setNavigationState(prev => ({ icon: ICON[prevState.isOpen ? 0 : 1] }));
return { isOpen: !prevState.isOpen };
});
};
Working DEMO
SOLUTION 2:
The other simpler approach to solving the problem is to use useCallback for onContextClick and update the navigation state with useEffect, everytime the closure state is updated like
const onContextClick = React.useCallback(() => {
setNavigationState({ icon: ICON[state.isOpen ? 0 : 1] });
setState({ isOpen: !state.isOpen });
}, [state]);
useEffect(() => {
setNavigationState({
search: true,
icon: ICON[0],
onClick: onContextClick
});
}, [onContextClick]);
Working demo
Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.
Following is a simplified example:
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = constantIdArray.map((id) => useCallback(() => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
}));
return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput(idArray);
return (
<div>
<div onClick={callbacks[0]}>
{state[0]}
</div>
<div onClick={callbacks[1]}>
{state[1]}
</div>
<div onClick={callbacks[2]}>
{state[2]}
</div>
</div>
)
}
Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:
const createUseHookWithConstantInput = (constantIdArray) => () => {
...
}
const idArray = ['id-1', 'id-2', 'id-3'];
const useHookWithConstantInput = createUseHookWithConstantInput(idArray)
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput();
return (
...
)
}
How do you solve situations like this?
One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:
const useHookWithConstantInput = (constantIdArray) => {
const [state, changeState] = useState({});
const [callbacks, setCallbacks] = useState([]);
useEffect(() => {
changeState(buildInitialState(constantIdArray));
const callbacksArray = constantIdArray.map((id) => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
});
setCallbacks(callbacksArray);
}, []);
return { state, callbacks };
}
Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.
If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.
Another solution to go for would be with useMemo. This is what I ended up implementing.
const createCallback = (id, changeState) => () => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
};
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = useMemo(() =>
constantIdArray.map((id) => createCallback(id, changeState)),
[],
);
return { state, callbacks };
};
I'm trying to add debouncing with lodash to a search function, called from an input onChange event. The code below generates a type error 'function is expected', which I understand because lodash is expecting a function. What is the right way to do this and can it be done all inline? I have tried nearly every example thus far on SO to no avail.
search(e){
let str = e.target.value;
debounce(this.props.relay.setVariables({ query: str }), 500);
},
With a functional react component try using useCallback. useCallback memoizes your debounce function so it doesn't get recreated again and again when the component rerenders. Without useCallback the debounce function will not sync with the next key stroke.
`
import {useCallback} from 'react';
import _debounce from 'lodash/debounce';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useCallback(_debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
`
The debounce function can be passed inline in the JSX or set directly as a class method as seen here:
search: _.debounce(function(e) {
console.log('Debounced Event:', e);
}, 1000)
Fiddle: https://jsfiddle.net/woodenconsulting/69z2wepo/36453/
If you're using es2015+ you can define your debounce method directly, in your constructor or in a lifecycle method like componentWillMount.
Examples:
class DebounceSamples extends React.Component {
constructor(props) {
super(props);
// Method defined in constructor, alternatively could be in another lifecycle method
// like componentWillMount
this.search = _.debounce(e => {
console.log('Debounced Event:', e);
}, 1000);
}
// Define the method directly in your class
search = _.debounce((e) => {
console.log('Debounced Event:', e);
}, 1000)
}
This is how I had to do it after googling the whole day.
const MyComponent = (props) => {
const [reload, setReload] = useState(false);
useEffect(() => {
if(reload) { /* Call API here */ }
}, [reload]);
const callApi = () => { setReload(true) }; // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
function handleChange() {
debouncedCallApi();
}
return (<>
<input onChange={handleChange} />
</>);
}
That's not so easy question
On one hand to just work around error you are getting, you need to wrap up you setVariables in the function:
search(e){
let str = e.target.value;
_.debounce(() => this.props.relay.setVariables({ query: str }), 500);
}
On another hand, I belive debouncing logic has to be incapsulated inside Relay.
A lot of the answers here I found to be overly complicated or just inaccurate (i.e. not actually debouncing). Here's a straightforward solution with a check:
const [count, setCount] = useState(0); // simple check debounce is working
const handleChangeWithDebounce = _.debounce(async (e) => {
if (e.target.value && e.target.value.length > 4) {
// TODO: make API call here
setCount(count + 1);
console.log('the current count:', count)
}
}, 1000);
<input onChange={handleChangeWithDebounce}></input>
Improving on this answer: https://stackoverflow.com/a/67941248/2390312
Using useCallback and debounce is known to cause an eslint exhaustive deps warning.
Here's how to do it with functional components and useMemo
import { useMemo } from 'react';
import { debounce } from 'lodash';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useMemo(() => debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
We are using useMemo to return a memoized value, where this value is the function returned by debounce
Some answers are neglecting that if you want to use something like e.target.value from the event object (e), the original event values will be null when you pass it through your debounce function.
See this error message:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property nativeEvent on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
As the message says, you have to include e.persist() in your event function. For example:
const onScroll={(e) => {
debounceFn(e);
e.persist();
}}
Then of course, your debounceFn needs to be scoped outside of the return statement in order to utilize React.useCallback(), which is necessary. My debounceFn looks like this:
const debounceFn = React.useCallback(
_.debounce((e) =>
calculatePagination(e),
500, {
trailing: true,
}
),
[]
);
#Aximili
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
looks strange :) I prefare solutions with useCallback:
const [searchFor, setSearchFor] = useState('');
const changeSearchFor = debounce(setSearchFor, 1000);
const handleChange = useCallback(changeSearchFor, []);
for your case, it should be:
search = _.debounce((e){
let str = e.target.value;
this.props.relay.setVariables({ query: str });
}, 500),
class MyComp extends Component {
debounceSave;
constructor(props) {
super(props);
}
this.debounceSave = debounce(this.save.bind(this), 2000, { leading: false, trailing: true });
}
save() is the function to be called
debounceSave() is the function you actually call (multiple times).
This worked for me:
handleChange(event) {
event.persist();
const handleChangeDebounce = _.debounce((e) => {
if (e.target.value) {
// do something
}
}, 1000);
handleChangeDebounce(event);
}
This is the correct FC approach
#
Aximili answers triggers only one time
import { SyntheticEvent } from "react"
export type WithOnChange<T = string> = {
onChange: (value: T) => void
}
export type WithValue<T = string> = {
value: T
}
// WithValue & WithOnChange
export type VandC<A = string> = WithValue<A> & WithOnChange<A>
export const inputValue = (e: SyntheticEvent<HTMLElement & { value: string }>): string => (e.target as HTMLElement & { value: string }).value
const MyComponent: FC<VandC<string>> = ({ onChange, value }) => {
const [reload, setReload] = useState(false)
const [state, setstate] = useState(value)
useEffect(() => {
if (reload) {
console.log('called api ')
onChange(state)
setReload(false)
}
}, [reload])
const callApi = () => {
setReload(true)
} // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000))
function handleChange(x:string) {
setstate(x)
debouncedCallApi()
}
return (<>
<input
value={state} onChange={_.flow(inputValue, handleChange)} />
</>)
}
const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);
const handleChange = (e) => {
let eventData = { id: e.id, target: e.target };
delayedHandleChange(eventData);
}