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()
}
}
Related
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.
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.
Main problem: I tried to use a function inside the selector to reestructur the data and join another variable, in this case my group and put together with their children as items, the problem is that the function is called every time on an infinite loop despite the state is not being altered.
I have this selector:
const groups = useSelector(state => selectProductGroups(state));
And the function is this one:
const groups = state.PlatformsReducer.groups;
const items = state.PlatformsReducer.items;
return groups.reduce((ac, g) => {
g.items = items.filter(i => i.groupId == g.productNumber);
if (ac[g.platformId]) {
ac[g.platformId].push(g);
} else {
ac[g.platformId] = [g];
}
return ac;
}, {});
};
So when I use a useEffect to detect if the groups variable has changed the useEffect is triggered in a loop despite the variable groups still empty.
Do you know why? or How to prevent this.
I now the problem is the function in the selector, but I don't know how to prevent this case.
This has to do with what the useSelector hook does internally.
useSelector runs your selector and checks if the result is the same as the previously received result (reference comparison). If the results differ then the new result is stored and a rerender is forced. If the results are the same then the old result is not replaced and no rerender is triggered.
What this does mean is that every time the store updates, even if it is an unrelated part of the state, your complex function will be run to determine whether the result has changed. In your case it is always a new reference and therefore always a change.
I think the best way to handle this is to keep your selectors as simple as possible, or use some form of more complex memoization like provided by reselect.
Below is an example of how you might be able to keep your selectors simple but still achieve an easy way to reuse your product group selection using a custom hook.
const useProductGroups = () => {
// Get groups from the store.
// As the selector does not create a new object it should only
// trigger a rerender when groups changes in the store.
const groups = useSelector(state => state.PlatformsReducer.groups);
// Get items from the store,
// As the selector does not create a new object it should only
// trigger a rerender when items changes in the store.
const items = useSelector(state => state.PlatformsReducer.items);
// Reduce the group collection as desired inside of a useMemo
// so that the reduction only occurs when either items or groups
// changes.
const productGroups = useMemo(() => {
return groups.reduce((ac, g) => {
g.items = items.filter(i => i.groupId == g.productNumber);
if (ac[g.platformId]) {
ac[g.platformId].push(g);
} else {
ac[g.platformId] = [g];
}
return ac;
}, {});
}, [groups, items] /* dependency array on items / groups */);
// return the calculated product groups
return productGroups;
}
You can then use the custom hook in your function components:
const groups = useProductGroups();
I need to remove an item from my array state and it doesnt work the way I need it to. I get the state from a details obj from the server and save it to name. It is an array of objects.
const [name, setName] = useState(
[...details?.name] || []
);
My add function works as needed:
const addName = () => {
nameForm.validateFields().then(values => {
setName([...name, values]);
nameForm.resetFields();
setModalVisible(false);
});
};
The remove function however doesnt. Calling the function the first i works but every time I call that function again, it uses the initial declaration of the name state. Ex, if the array is size 4 first call would remove an element and it would be size 3. If i call that function again, the name is still size 4.
const removeName = (obj) => {
setName([...name.filter(i => i !== obj)]);
};
You cannot compare objects using the !== operator. Rather you need to compare some properties of the object. You did not say which properties your object has but let us assume they have an id property then:
setName([...name.filter(i => i.id !== obj.id)]);
would work.
I have an array of objects in state: const [objects, setObjects] = useState([]);
I have an add button that adds an object to the array, this is the body of the onclick event:
setObjects([...objects, {
id: 20,
property1: value1
}]);
I have a remove button that removes an object from the array, this is the body of the onclick event:
const newObjects = objects.filter(object => {
return object.id !== idToRemove; // idToRemove comes from the onclick event
});
setObjects(newObjects);
Now I want to do something with the updated state if an object gets removed from the state.
The problem is I have to wait till the state is updated and I don't want to listen for every state change, only if something is removed.
This is what I have so far:
useEffect(() => {
//execute a function that uses the updated state
}, [objects.length]);
But this also fires of if an object gets added to the state.
In short: I want to do something when an object gets removed from the objects array and the state is finished updating
I would like to do this with hooks.
Thanks in advance!
You could write your own hook or add some other variable that will keep the array's length and which could be used to check whether element was removed or added.
const [len, setLen] = useState(0);
useEffect(() => {
if (objects.length < len) {
// Your code
}
setLen(objects.length);
}, [objects.length]);