Use useEffect to reinitialize useState - reactjs

I have a component with a <div contentEditable /> inside. The component is initialized with a initialValue. The onChange handler updates the value. It looks something like this:
const Editable = ({ initialValue }) => {
const [value, setValue] = useState(initialValue);
return (
<div contentEditable onChange={event => setValue(event.currentTarget.value)}>
{value}
</div>
);
};
Now if the parent components passes a new initialValue, value and therefore the content of the div doesn't update since the state is already initialized.
Is it fine to use useEffect as follows in such a case or is there any other way?
const Editable = ({ initialValue }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => setValue(initialValue), [initialValue]);
return (
<div contentEditable onChange={event => setValue(event.currentTarget.value)}>
{value}
</div>
);
};

It's completely fine to use it that way. You may want to rename initialValue to something like value to make it clear that it's not just the default value.
What you could do to clear up your code is create a custom hook that handles that exact situation like so:
const useUpdatingState = (value) => {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value)
}, [value]);
return [internalValue, setInternalValue];
};
This would make your code a little more readable and make the code reusable in other parts of your code where you would want such behaviour.

As proposed in the question, a useEffect is the correct solution.
const Editable = ({ initialValue }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => setValue(initialValue), [initialValue]);
return (
<div contentEditable onChange={event => setValue(event.currentTarget.value)}>
{value}
</div>
);
};

Related

Debouncing MUI Autocomplete not working properly

I'm trying to implement debounce for mui autocomplete,
but it's not working well.
I want to send a debounced request to server when inputValue change.
Am I miss something?
It looks loadData fired every input change. Only first load debounce works.
https://codesandbox.io/s/debounce-not-working-j4ixgg?file=/src/App.js
Here's the code from the sandbox:
import { useState, useCallback } from "react";
import { Autocomplete, TextField } from "#mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const loadData = () => {
// sleep(1000)
const filteredOptions = topFilms.filter((f) =>
f.title.includes(inputValue)
);
// This log statement added by Ryan Cogswell to show why it isn't working.
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = useCallback(debounce(loadData, 1000), []);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData();
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}
tldr: codesandbox link
You can accomplish this with a few hooks working together. First lets look at a hook for debouncing state:
function useDebounce(value, delay, initialValue) {
const [state, setState] = useState(initialValue);
useEffect(() => {
console.log("delaying", value);
const timer = setTimeout(() => setState(value), delay);
// clear timeout should the value change while already debouncing
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return state;
}
The above takes in a value and returns the debounced value from the hook later.
The callback at the end of the useEffect prevents a bunch of timers triggering one after the other.
Then your component can be reduced to:
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const debouncedValue = useDebounce(inputValue, 1000);
// fetch data from server
useEffect(() => {
console.log("fetching", debouncedValue);
const filteredOptions = topFilms.filter((f) =>
f.title.includes(debouncedValue)
);
setOptions(filteredOptions);
}, [debouncedValue]);
const handleInputChange = (e, v) => {
setInputValue(v);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
// props
/>
</div>
);
}
The useEffect here is run when the dependency debouncedValue is changed via react state business.
When typing in the TextField your console should look like:
delaying s
delaying sp
delaying spi
fetching spi
delaying spir
fetching spir
This useEffect in App leaves you with a good place to do your fetch to a server like you mentioned you'll need.
The useEffect could easily be replaced with something like useQuery
In your sandbox, loadData was being successfully debounced, however it was always executed with inputValue === "" which then matched all of the options. All of the actual filtering was then being immediately done by the Autocomplete component just the same as if you had provided the full set of options statically. Due to the behavior of JavaScript closures, inputValue is always the empty string because that is its initial value as you create the debounced version of loadData.
Whenever creating debounced functions, I recommend declaring and defining the original function and the debounced version at the top level rather than inside your component. Any local variables and state that your debounced function is dependent on can be passed in as arguments to the function (e.g. in my version of your code, the input value and the setOptions function are passed to debouncedLoadData). This avoids accidentally using stale values due to closure behavior and removes the need for useCallback.
Below is a reworked version of your code that moves loadData and debouncedLoadData to the top level. I also changed loadData to provide no data (rather than all data) when it is passed an empty string so that the debounce behavior is easier to see -- otherwise whenever the Autocomplete has the full set of options, the filtering will occur immediately based on the options that the Autocomplete already has. This also more closely simulates what real code would likely do (i.e. avoid calling the back end for empty string).
import { useState } from "react";
import { Autocomplete, TextField } from "#mui/material";
import { debounce } from "lodash";
import topFilms from "./topFilms";
const loadData = (inputValue, setOptions) => {
if (inputValue.length === 0) {
console.log("no options");
setOptions([]);
return;
}
const filteredOptions = topFilms.filter((f) =>
f.title.toLowerCase().includes(inputValue.toLowerCase())
);
console.log(
`loadData with ${filteredOptions.length} options based on "${inputValue}"`
);
setOptions(filteredOptions);
};
const debouncedLoadData = debounce(loadData, 1000);
export default function App() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
const handleInputChange = (e, v) => {
setInputValue(v);
debouncedLoadData(v, setOptions);
};
const handleChange = (e, v) => {
setValue(v);
};
return (
<div className="App">
<Autocomplete
value={value}
inputValue={inputValue}
onChange={handleChange}
onInputChange={handleInputChange}
disablePortal
options={options}
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => option.title === value.title}
id="combo-box-demo"
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</div>
);
}

Don't update children use react useContext

I have two children Components, when I onChange in first children, then the second children re render, I don't want to the second children re render. Online code example: https://codesandbox.io/s/billowing-glitter-r5gnh3?file=/src/App.js:1287-1297
const EditSceneModalStore = React.createContext(undefined);
const Parent = () => {
const [saveValue, setSaveValue] = React.useState({});
const initValue = {
name: "zhang",
age: 3
};
const onSave = () => {
console.log("===saveValue==", saveValue);
};
const onChangeValue = (key, value) => {
const newValue = {
...saveValue,
[key]: value
};
setSaveValue(newValue);
};
return (
<EditSceneModalStore.Provider
value={{
initValue,
onChangeValue
}}
>
<ChildInput1 />
<ChildInput2 />
<Button onClick={onSave} type="primary">
save
</Button>
</EditSceneModalStore.Provider>
);
};
const ChildInput1 = () => {
const { onChangeValue, initValue } = React.useContext(EditSceneModalStore);
const [value, setValue] = React.useState(initValue.name);
return (
<Input
value={value}
onChange={(v) => {
setValue(v.target.value);
onChangeValue("name", v.target.value);
}}
/>
);
};
const ChildInput2 = () => {
const { initValue, onChangeValue } = React.useContext(EditSceneModalStore);
const [value, setValue] = React.useState(initValue.InputNumber);
console.log("====ChildInput2===");
return (
<InputNumber
value={value}
onChange={(v) => {
setValue(v.target.value);
onChangeValue("age", v.target.value);
}}
/>
);
};
when I onChange in ChildInput1, then ChildInput2 re-render, I don't want to the ChildInput2 re-render. Example image
As Andrey explained, you should fix the following line:
//you have
const [value, setValue] = React.useState(initValue.InputNumber);
// should be
const [value, setValue] = React.useState(initValue.age);
Additionally, initValue gets unnecessarily re-computed on every re-render, so it should be outside the scope of Parent:
const initValue = {
name: "zhang",
age: 3
};
const Parent = () => {...}
Regarding re renderings, it is ok. When a Provider gets the value changed, all their childs wrapped in a consumer rerender. This is natural. This post explains why.
A component calling useContext will always re-render when the context
value changes. If re-rendering the component is expensive, you can
optimize it by using memoization.
In this case, it is not expensive enough to consider memoization.
I Hope it helps
You have a typo in your code:
//you have
const [value, setValue] = React.useState(initValue.InputNumber);
// should be
const [value, setValue] = React.useState(initValue.age);
also update like that
<InputNumber
value={value}
onChange={(value) => {
setValue(value);
onChangeValue("age", value);
}}
/>
and when you fix like that do not worry about re-render as state of ChildInput2 will no be changed

useState wouldn't work asynchronously (array)

I would like to reflect state asynchronously. But when I add a value, it is not reflected asynchronously. How can I solve this problem?
const [values, setValues] = useState<string[]>([])
const [input, setInputs] = useState<string>()
const add = useCallback(() => {
if(!input) return
values.push(input)
setValues(values)
}, [input, values])
...
<input onChange={() => setInput(e.target.value) />
<button onClick={() => add()}>Add</button>
// not displayed asynchrnously
{values && values.map((value, idx) => {
return (
<div key={idx}>{value}</div>
)
})}
...
When setting the existing state based on the previous state, the recommended way to do it is to send a callback function to the state setting function.
I have modified your add function to reflect this:
const add = useCallback(() => {
if (input) {
setValues(prev => [...prev, input])
}
}, [input, setValues])
Also, your callback function for setting the input was missing the event parameter and had a typo. I have fixed both issues:
<input onChange={e => setInput(e.target.value)} />

How to make a custom debounce hook works with a useCallback?

I did search for those related issues and found some solutions, but most about the lodash debounce. In my case, I create useDebounce as a custom hook and return the value directly.
My current issue is useCallback works with an old debounced value.
Here are my code snips.
//To makes sure that the code is only triggered once per user input and send the request then.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
};
useDebounce works as expected
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const debounceInput = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setValue(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;
Here are screenshots with console.info to explain what the issue is.
Current quantity
Updated with onChange
I do appreciate it if you have any solution to fix it, and also welcome to put forward any code that needs updates.
This might help you achieve what you want. You can create a reusable debounce function with the callback like below.
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
let timeout;
const setDebounce = (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => setDebouncedValue(newValue), delay);
};
return [debouncedValue, setDebounce];
};
And use the function on your code like this.
export const ShopQuantityCounter = ({ id, qty }) => {
const [value, setValue] = useState(qty);
const [debounceInput, setDebounceInput] = useDebounce(value, 300);
const dispatch = useDispatch();
const handleOnInputChange = useCallback((e) => {
setDebounceInput(e.target.value);
console.info('Inside OnChange: debounceInput', debounceInput);
// dispatch(updateCartItem({ id: id, quantity: debounceInput }));
},[debounceInput]);
console.info('Outside OnChange: debounceInput', debounceInput);
// To fixed issue that useState set method not reflecting change immediately
useEffect(() => {
setValue(qty);
}, [qty]);
return (
<div className="core-cart__quantity">
<input
className="core-cart__quantity--total"
type="number"
step="1"
min="1"
title="Qty"
value={value}
pattern="^[0-9]*[1-9][0-9]*$"
onChange={handleOnInputChange}
/>
</div>
);
};
export default ShopQuantityCounter;

React hook not setting the state

Hey all trying to use a useState react hook to set a state but it does not work, I gone through the official documentation
Seems like i have followed it correctly but still cannot get the hook to set the state:
const [search, setSearch] = useState('');
const { films } = props;
const matchMovieSearch = (films) => {
return films.forEach(item => {
return item.find(({ title }) => title === search);
});
}
const handleSearch = (e) => {
setSearch(e.target.value);
matchMovieSearch(films);
}
<Form.Control
type="text"
placeholder="Search Film"
onChange={(e) => {handleSearch(e)}}
/>
Search var in useState is allways empty even when i debug and can see that e.target.value has to correct data inputed from the html field
setSearch is an async call, you won't be able to get the search immediately after setting the state.
useEffect is here for rescue.
useEffect(() => {
// your action
}, [search]);
Are you sure you are using the hooks inside a component, hooks can only be used in a Functional React Component.
If that is not the case, there must be something wrong with the Form.Control component, possibly like that component did not implement the onChanged parameter properly.
This is the one I tested with the html input element, and it is working fine. I used the useEffect hook to track the changes on the search variable, and the you can see that the variable is being properly updated.
https://codesandbox.io/s/bitter-browser-c4nrg
export default function App() {
const [search, setSearch] = useState("");
useEffect(() => {
console.log(`search was changed to ${search}`);
}, [search]);
const handleSearch = e => {
setSearch(e.target.value);
};
return (
<input
type="text"
onChange={e => {
handleSearch(e);
}}
/>
);
}

Resources