Am new to react and am creating a custom select component where its supposed to set an array selected state and also trigger an onchange event and pass it to the parent with the selected items and also get initial value as prop and set some data.
let firstTime = true;
const CustomSelect = (props)=>{
const [selected, setSelected] = useState([]);
const onSelectedHandler = (event)=>{
// remove if already included in the selected items remove
//otherwise add
setSelected((prev)=>{
if (selected.includes(value)) {
values = prevState.filter(item => item !== event.target.value);
}else {
values = [...prevState, event.target.value];
}
return values;
})
// tried calling props.onSelection(selected) but its not latest value
}
//watch when the value of selected is updated and pass onchange to parent
//with the newest value
useEffect(()=>{
if(!firstTime && props.onSelection){
props.onSelection(selected);
}
firstTime = false;
},[selected])
return (<select onChange={onSelectedHandler}>
<option value="1"></option>
</select>);
};
Am using it on a parent like
const ParentComponent = ()=>{
const onSelectionHandler = (val)=>{
//do stuff with the value passed
}
return (
<CustomSelect initialValue={[1,2]} onSelection={onSelectionHandler} />
);
}
export default ParentComponent
The above works well but now the issue comes in when i want to set the initialValue passed from the parent onto the customSelect by updating the selected state. I have added the followin on the CustomSelect, but it causes an infinite loop
const {initialValue} = props
useEffect(()=>{
//check if the value of initialValue is an array
//and other checks
setSelected(initialValue)
},[initialValue]);
I understand that i could have passed the initialValue in the useState but i would like to do a couple of checks before setting the selected state.
How can i resolve this, am still new to react.
In your //do stuff with the value passed you are most likely update the states of your component parent component and it causes to rerender parent component. When passing the prop initialValue={[1,2]} creates a new instance of [1,2] array on each render and causes the infinite render on useEffect. In order to solve this, you can move the initialValue prop to somewhere else as const value like this:
const INITIAL_VALUE_PROP = [1,2];
const ParentComponent = ()=>{
const onSelectionHandler = (val)=>{
//do stuff with the value passed
}
return (
<CustomSelect initialValue={INITIAL_VALUE_PROP} onSelection={onSelectionHandler}
/>
);
}
export default ParentComponent
Okay, so the reason this is happening is that you're passing the initialValue prop as an array, and since this is a reference value in JavaScript, it means that each time it's passed(updated), it's passed with a different reference value/address, and so the effect will continue to re-run infinitely. One way to solve this is to use React.useMemo, documentation here to store/preserve the reference value of the array passed, not to cause unnecessary side effects running.
Related
I have multiple components with checkboxes. This function is called on onChange on checkboxes.
const { selectedDocuments, setSelectedDocuments } = useContext(Context);
const [isSelected, setIsSelected] = useState(false);
const onSelect: React.ChangeEventHandler<HTMLInputElement> | undefined = (
e
) => {
if (isSelected) {
setSelectedDocuments((docs) =>
docs.filter((doc) => doc.data.id !== card.data.id)
);
setIsSelected(false);
} else {
setSelectedDocuments((docs) => [...docs, card]);
setIsSelected(true);
}
};
I am trying to update the context and flip the check state. But every time context is updating the state of component isSelected is getting reset (on very other children which is using the same context).
I am not sure why all the states are getting reset.
I think you have multiple components which are instances of a single component.
for example, you could have a <Checkboxes/> component and then you are simply reusing it everywhere.
If you are doing this then I think you will run into trouble with Context API.
You can define separate and unique state variables and then pass these state variables as props to each component to solve this issue.
For example say you have state variables selectedOne, selectedTwo,selectedThree ....
then you can do something like this
<Checkboxes selected = {selectedOne} />
<Checkboxes selected = {selectedTwo} />
<Checkboxes selected = {selectedThree} />
I am struggling to get the real previous state of my inputs.
I think the real issue Which I have figured out while writing this is my use of const inputsCopy = [...inputs] always thinking that this creates a deep copy and i won't mutate the original array.
const [inputs, setInputs] = useState(store.devices)
store.devices looks like this
devices = [{
name: string,
network: string,
checked: boolean,
...etc
}]
I was trying to use a custom hook for getting the previous value after the inputs change.
I am trying to check if the checked value has switched from true/false so i can not run my autosave feature in a useEffect hook.
function usePrevious<T>(value: T): T | undefined {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
I have also tried another custom hook that works like useState but has a third return value for prev state. looked something like this.
const usePrevStateHook = (initial) => {
const [target, setTarget] = useState(initial)
const [prev, setPrev] = useState(initial)
const setPrevValue = (value) => {
if (target !== value){ // I converted them to JSON.stringify() for comparison
setPrev(target)
setTarget(value)
}
}
return [prev, target, setPrevValue]
}
These hooks show the correct prevState after I grab data from the api but any input changes set prev state to the same prop values.
I think my issue lies somewhere with mobx store.devices which i am setting the initial state to or I am having problems not copying/mutating the state somehow.
I have also tried checking what the prevState is in the setState
setInputs(prev => {
console.log(prev)
return inputsCopy
})
After Writing this out I think my issue could be when a value changes on an input and onChange goes to my handleInputChange function I create a copy of the state inputs like
const inputsCopy = [...inputs]
inputsCopy[i][prop] = value
setInputs(inputsCopy)
For some reason I think this creates a deep copy all the time.
I have had hella issues in the past doing this with redux and some other things thinking I am not mutating the original variable.
Cheers to all that reply!
EDIT: Clarification on why I am mutating (not what I intended)
I have a lot of inputs in multiple components for configuring a device settings. The problem is how I setup my onChange functions
<input type="text" value={input.propName} name="propName" onChange={(e) => onInputChange(e, index)} />
const onInputChange = (e, index) => {
const value = e.target.value;
const name = e.target.name;
const inputsCopy = [...inputs]; // problem starts here
inputsCopy[index][name] = value; // Mutated obj!?
setInputs(inputsCopy);
}
that is What I think the source of why my custom prevState hooks are not working. Because I am mutating it.
my AUTOSAVE feature that I want to have the DIFF for to compare prevState with current
const renderCount = useRef(0)
useEffect(() => {
renderCount.current += 1
if (renderCount.current > 1) {
let checked = false
// loop through prevState and currentState for checked value
// if prevState[i].checked !== currentState[i].checked checked = true
if (!checked) {
const autoSave = setTimeout(() => {
// SAVE INPUT DATA TO API
}, 3000)
return () => {
clearTimeout(autoSave)
}
}
}
}, [inputs])
Sorry I had to type this all out from memory. Not at the office.
If I understand your question, you are trying to update state from the previous state value and avoid mutations. const inputsCopy = [...inputs] is only a shallow copy of the array, so the elements still refer back to the previous array.
const inputsCopy = [...inputs] // <-- shallow copy
inputsCopy[i][prop] = value // <-- this is a mutation of the current state!!
setInputs(inputsCopy)
Use a functional state update to access the previous state, and ensure all state, and nested state, is shallow copied in order to avoid the mutations. Use Array.prototype.map to make a shallow copy of the inputs array, using the iterated index to match the specific element you want to update, and then also use the Spread Syntax to make a shallow copy of that element object, then overwrite the [prop] property value.
setInputs(inputs => inputs.map(
(el, index) => index === i
? {
...el,
[prop] = value,
}
: el
);
Though this is a Redux doc, the Immutable Update Patterns documentation is a fantastic explanation and example.
Excerpt:
Updating Nested Objects
The key to updating nested data is that every level of nesting must be
copied and updated appropriately. This is often a difficult concept
for those learning Redux, and there are some specific problems that
frequently occur when trying to update nested objects. These lead to
accidental direct mutation, and should be avoided.
This question already has answers here:
Using a Set data structure in React's state
(2 answers)
Closed 1 year ago.
I have a parent and a child component. There are 3 props the parent provides out of which 1 is not updating correctly.
Following is the parent component. The prop in question is selectedFilters (which is an object where keys are mapped to sets) and the relevant update function is filterChanged (this is passed to the child)
import filters from "../../data/filters"; //JSON data
const Block = (props) => {
const [selectedFilters, setSelectedFilters] = useState({versions: new Set(), languages: new Set()});
console.log(selectedFilters);
const filterChanged = useCallback((filter_key, filter_id) => {
setSelectedFilters((sf) => {
const newSFSet = sf[filter_key]; //new Set(sf[filter_key]);
if (newSFSet.has(filter_id)) {
newSFSet.delete(filter_id);
} else {
newSFSet.add(filter_id);
}
const newSF = { ...sf, [filter_key]: new Set(newSFSet) };
return newSF;
});
}, []);
return (
<FilterGroup
filters={filters}
selectedFilters={selectedFilters}
onFilterClick={filterChanged}
></FilterGroup>
);
};
export default Block;
The following is the child component: (Please note that while the Filter component runs the filterChanged function, I think it is irrelevant to the error)
import Filter from "./Filter/Filter";
const FilterGroup = (props) => {
const { filters, selectedFilters, onFilterClick } = props;
console.log(selectedFilters);
const filter_view = (
<Container className={styles.container}>
{Object.keys(filters).map((filter_key) => {
const filter_obj = filters[filter_key];
return (
<Filter
key={filter_obj.id}
filter_key={filter_key}
filter_obj={filter_obj}
selectedFilterSet={selectedFilters[filter_key]}
onFilterClick={onFilterClick}
/>
);
})}
</Container>
);
return filter_view;
};
export default FilterGroup;
When running the application, I find that the selectedFilters updates correctly only once. After that, it only changes temporarily in the main Block.tsx, but eventually goes back to the first updated value. Also, FilterGroup.tsx only receives the first update. After that, it never receives any further updated values.
Here are the logs:
After some experimentation, it is clear that the problem originates from the filterChanged function. But I cannot seem to figure out why the second update is temporary AND does not get passed on to the child.
Any ideas? Thanks in advance.
(If any other info is required, pls do mention it)
I don't think you actually want your filterChanged function to be wrapped with useCallback, especially with an empty deps array. with the empty deps array, I believe useCallback will fire once on initial render, and memoize the result. You may be able to add filter_key and filter_id to the dependency array, but useCallback tends to actually slow simple functions down, instead of adding any real performance benefit, so you may just want to get rid of the useCallback completely and switch filterChanged to a regular arrow function.
I'm trying to send information from parent to child component with a state.
In the parent component, I'm getting the value and storing it inside an array named valueArray. Later on, this array will be stored in a state and sent through child component to map and display the values.
1-) Function below takes the values selected by user and pushes them inside an empty array valueArray.
let valueArray = [];
const getSelectedItem = (event) =>{
if(valueArray.includes(item)){
valueArray.splice(item.index,1);
}else{
valueArray.push(item);
}
}
2-) In here, after user done with selecting values, another function to store values in state will be initiated by onClick, this function uses valueArray as argument.
<button onClick={() => handleState(valueArray)}>Buton</button>
3-) After the button click, handleState function executes and updates state with the elements in valueArray, values array goes to child component to map and perform display.
const [values,setValues] = useState([]);
const handleState = (arr) =>{
setValues([...arr])
}
Major problem in here is, when button clicked at the first time after selection, state becomes empty array and child component can't execute mapping because valueArray's initial value is []. Second click works and state changes with values inside the valueArray.
Use State like this.
const [valueArray, setValueArray] = [];
const getSelectedItem = (event) =>{
const array = [...valueArray];
if(array.includes(item)){
array.splice(item.index, 1);
// setValueArray([...valueArray.filter((ite) => ite != item)]);
setValueArray([...array]);
}else{
// valueArray.push(item);
setValueArray([...array, item]); // instead of push we should use spread or concat()
}
}
I had a class component named <BasicForm> that I used to build forms with. It handles validation and all the form state. It provides all the necessary functions (onChange, onSubmit, etc) to the inputs (rendered as children of BasicForm) via React context.
It works just as intended. The problem is that now that I'm converting it to use React Hooks, I'm having doubts when trying to replicate the following behavior that I did when it was a class:
class BasicForm extends React.Component {
...other code...
touchAllInputsValidateAndSubmit() {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let inputs = {};
for (let inputName in this.state.inputs) {
inputs = Object.assign(inputs, {[inputName]:{...this.state.inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in inputs) {
inputs[inputName].touched = true;
}
// UPDATE STATE AND CALL VALIDATION
this.setState({
inputs
}, () => this.validateAllFields()); // <---- SECOND CALLBACK ARGUMENT
}
... more code ...
}
When the user clicks the submit button, BasicForm should 'touch' all inputs and only then call validateAllFields(), because validation errors will only show if an input has been touched. So if the user hasn't touched any, BasicForm needs to make sure to 'touch' every input before calling the validateAllFields() function.
And when I was using classes, the way I did this, was by using the second callback argument on the setState() function as you can see from the code above. And that made sure that validateAllField() only got called after the state update (the one that touches all fields).
But when I try to use that second callback parameter with state hooks useState(), I get this error:
const [inputs, setInputs] = useState({});
... some other code ...
setInputs(auxInputs, () => console.log('Inputs updated!'));
Warning: State updates from the useState() and useReducer() Hooks
don't support the second callback argument. To execute a side effect
after rendering, declare it in the component body with useEffect().
So, according to the error message above, I'm trying to do this with the useEffect() hook. But this makes me a little bit confused, because as far as I know, useEffect() is not based on state updates, but in render execution. It executes after every render. And I know React can queue some state updates before re-rendering, so I feel like I don't have full control of exactly when my useEffect() hook will be executed as I did have when I was using classes and the setState() second callback argument.
What I got so far is (it seems to be working):
function BasicForm(props) {
const [inputs, setInputs] = useState({});
const [submitted, setSubmitted] = useState(false);
... other code ...
function touchAllInputsValidateAndSubmit() {
const shouldSubmit = true;
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
for (let inputName in inputs) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
// UPDATE STATE
setInputs(auxInputs);
setSubmitted(true);
}
// EFFECT HOOK TO CALL VALIDATE ALL WHEN SUBMITTED = 'TRUE'
useEffect(() => {
if (submitted) {
validateAllFields();
}
setSubmitted(false);
});
... some more code ...
}
I'm using the useEffect() hook to call the validateAllFields() function. And since useEffect() is executed on every render I needed a way to know when to call validateAllFields() since I don't want it on every render. Thus, I created the submitted state variable so I can know when I need that effect.
Is this a good solution? What other possible solutions you might think of? It just feels really weird.
Imagine that validateAllFields() is a function that CANNOT be called twice under no circunstances. How do I know that on the next render my submitted state will be already 'false' 100% sure?
Can I rely on React performing every queued state update before the next render? Is this guaranteed?
I encountered something like this recently (SO question here), and it seems like what you've come up with is a decent approach.
You can add an arg to useEffect() that should do what you want:
e.g.
useEffect(() => { ... }, [submitted])
to watch for changes in submitted.
Another approach could be to modify hooks to use a callback, something like:
import React, { useState, useCallback } from 'react';
const useStateful = initial => {
const [value, setValue] = useState(initial);
return {
value,
setValue
};
};
const useSetState = initialValue => {
const { value, setValue } = useStateful(initialValue);
return {
setState: useCallback(v => {
return setValue(oldValue => ({
...oldValue,
...(typeof v === 'function' ? v(oldValue) : v)
}));
}, []),
state: value
};
};
In this way you can emulate the behavior of the 'classic' setState().
I have tried to solve it using the useEffect() hook but it didn't quite solve my problem. It kind of worked, but I ended up finding it a little too complicated for a simple task like that and I also wasn't feeling sure enough about how many times my function was being executed, and if it was being executed after the state change of not.
The docs on useEffect() mention some use cases for the effect hook and none of them are the use that I was trying to do.
useEffect API reference
Using the effect hook
I got rid of the useEffect() hook completely and made use of the functional form of the setState((prevState) => {...}) function that assures that you'll get a current version of your state when you use it like that. So the code sequence became the following:
// ==========================================================================
// FUNCTION TO HANDLE ON SUBMIT
// ==========================================================================
function onSubmit(event){
event.preventDefault();
touchAllInputsValidateAndSubmit();
return;
}
// ==========================================================================
// FUNCTION TO TOUCH ALL INPUTS WHEN BEGIN SUBMITING
// ==========================================================================
function touchAllInputsValidateAndSubmit() {
let auxInputs = {};
const shouldSubmit = true;
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
return({
...auxInputs
});
});
validateAllFields(shouldSubmit);
}
// ==========================================================================
// FUNCTION TO VALIDATE ALL INPUT FIELDS
// ==========================================================================
function validateAllFields(shouldSubmit = false) {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs =
Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// ... all the validation code goes here
return auxInputs; // RETURNS THE UPDATED STATE
}); // END OF SETINPUTS
if (shouldSubmit) {
checkValidationAndSubmit();
}
}
See from the validationAllFields() declaration that I'm performing all my code for that function inside a call of setInputs( (prevState) => {...}) and that makes sure that I'll be working on an updated current version of my inputs state, i.e: I'm sure that all inputs have been touched by the touchAllInputsValidateAndSubmit() because I'm inside the setInputs() with the functional argument form.
// ==========================================================================
// FUNCTION TO CHECK VALIDATION BEFORE CALLING SUBMITACTION
// ==========================================================================
function checkValidationAndSubmit() {
let valid = true;
// THIS IS JUST TO MAKE SURE IT GETS THE MOST RECENT STATE VERSION
setInputs((prevState) => {
for (let inputName in prevState) {
if (inputs[inputName].valid === false) {
valid = false;
}
}
if (valid) {
props.submitAction(prevState);
}
return prevState;
});
}
See that I use that same pattern of the setState() with functional argument call inside the checkValidationAndSubmit() function. In there, I also need to make sure that I'm get the current, validated state before I can submit.
This is working without issues so far.