There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.
Related
How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
I am trying to create a function that updates an object in react functional component.
What i was trying to do is:
const [content, setContent] = useState({});
const applyContent = (num: number, key: string, val: string) => {
if (content[num] === undefined) {
content[num] = {};
}
content[num][key] = val;
setNewContent(newInput);
};
But I keep getting an error stating that content doesnt have a num attribute,
In vanilla JS it would work, what am i missing to make it work with react functional component?
The setter for your component state has been incorrectly spelled. Have a look at the code below.
import React, { useState } from 'react';
import './style.css';
export default function App() {
const [content, setContent] = useState({});
const applyContent = (num, key, val) => {
//gets the appropriate inputs
let updatedContent = content;
let value = {};
value[key] = val;
updatedContent[num] = value; //this inserts a new object if not present ot updates the existing one.
setContent({ ...updatedContent });
};
return (
<div>
<h1>Click buttons to change content</h1>
<p>{JSON.stringify(content)}</p>
<button onClick={(e) => applyContent(0, 'a', 'b')}>Add</button>
<button onClick={(e) => applyContent(1, 'c', 'd')}>Add</button>
<button onClick={(e) => applyContent(0, 'e', 'f')}>Add</button>
</div>
);
}
content is a read only value. You must not directly mutate this. Use it only to show data or to copy this data to another helper value.
setContent is a function that sets content.
There are two ways to set data
setContent(value) <-- set directly
setContent(prevState => {
return {
...prevState,
...value
}
})
In second example, you will use the previous value, copy it, and then override it with new value. This is useful if you are only updating a part of an object or array.
If you you are working with a deeply nested object, shallow copy might not be enough and you might need to deepcopy your content value first. If not, then use the prevState example to only update the part of that content
const [content, setContent] = useState({});
const applyContent = (num:number,key:string,val:string) => {
const newContent = {...content} // Shallow copy content
if (content[num] === undefined) {
//content[num] = {}; <-- you cant directly change content. content is a readOnly value
newContent[num] = {}
}
newContent[num][key] = val;
//setNewContent(newInput);
setContent(newContent) // <-- use "setContent" to change "content" value
}
As you can see in below screencast, manipulating the second input field in my form also changes the value of the first one. I've completely reproduced it on JSfiddle in an isolated environment to track down the bug but didn't find it.
Quick overview of my code base:
A functional component ModuleBuilder has an Array in its state called blocks and a renderFunction which renders for each element of blocks one instance of a subcomponent called ModuleElement.
The subcomponent ModuleElement displays an input form element. When I create n instances of the subcomponent and try to type something in the the form element of the second instance then the first one's value gets updated as well.
const ModuleElement = (props) => {
const blockElement = props.config;
const key = props.index;
const changeValue = (e) => {
blockElement[e.target.name] = e.target.value;
props.updateBlock(key, blockElement);
};
return (
<div key={`block_${key}`}>
<input
key={`block_input_${key}`}
type="text"
id={`fieldname_${key}`}
name="fieldName"
onChange={changeValue}
placeholder="Field Name"
defaultValue={blockElement.fieldName}
/>
</div>
);
};
const ModuleBuilder = () => {
const emptyBlock = {
fieldName: "",
required: true,
};
const [blocks, setBlocks] = React.useState([emptyBlock]);
const updateBlock = (key, data) => {
//console.log(`i was asked to update element #${key}`);
//console.log("this is data", data);
let copyOfBlocks = blocks;
//copyOfBlocks[key] = data; // WHY DOES COMMENTING THIS OUT STILL MAKE THE UPDATE WORK??
setBlocks([...copyOfBlocks]);
// console.log("this is blocks", blocks);
};
const add1Block = () => {
setBlocks([...blocks, emptyBlock]);
};
const renderBlockFormElements = () => {
return blocks.map((value, index) => {
return (
<li key={index}>
<b>Subcomponent with Index #{index}</b>
<ModuleElement
index={index}
config={value}
updateBlock={updateBlock}
/>
</li>
);
});
};
return (
<div>
<h1>
Click twice on "add another field" then enter something into the second
field.
</h1>
<h2>Why is 1st field affected??</h2>
<form>
<ul className="list-group">{renderBlockFormElements()}</ul>
<button type="button" onClick={add1Block}>
Add another field
</button>
</form>
<br></br>
<h2>This is the state:</h2>
{blocks.map((value, index) => (
<p key={`checkup_${index}`}>
<span>{index}: </span>
<span>{JSON.stringify(value)}</span>
</p>
))}
</div>
);
};
ReactDOM.render(<ModuleBuilder />, document.querySelector("#app"));
see full code on https://jsfiddle.net/g8yc39Lv/3/
UPDATE 1:
I solved the problem (see my own answer below) but am still confused about one thing: Why is copyOfBlocks[key] = data; in the updateBlocks() not necessary to update the state correctly? Could it be that the following code is manipulating the props directly??
const changeValue = (e) => {
blockElement[e.target.name] = e.target.value;
props.updateBlock(key, blockElement);
};
If yes, what would be the react way to structure my use case?
UPDATE 2:
It turns out indeed that I was manipulating the props directly. I changed now the whole setup as follows.
The Module Element component now has its own state. OnChange the state is updated and the new target.value is pushed to the props.updateblocks method. See updated JSfiddle here. Is this the react way to do it ?
Suggested changes to your new answer:
Your updated answer is mostly correct, and more React-ish. I would change a couple things:
You have two state variables, which means you have two sources of truth. It works now because they're always in sync, but this has the potential to cause future bugs, and is not actually necessary. ModuleElement doesn't need a state variable at all; you can just render props.config.fieldName:
<input
...
onChange={(e) => {
props.updateBlock(key, {fieldName:e.target.value, required:true})
}}
value={props.config.fieldName}
/>
Then, you can eliminate the state variable in ModuleElement:
const ModuleElement = (props) => {
const key = props.index;
return (
<React.Fragment>
...
I would write updateBlock a little differently. copyOfBlocks is not actually a copy, so copyOfBlocks[key] = data; actually mutates the original data, and a copy is not made until setBlocks([...copyOfBlocks]);. This isn't a problem per se, but a clearer way to write it could be:
const updateBlock = (key, data) => {
let copyOfBlocks = [...blocks];
copyOfBlocks[key] = data;
setBlocks(copyOfBlocks);
};
Now, copyOfBlocks is actually a shallow copy ([...blocks] is what causes the copy), and not a pointer to the same data.
Here's an updated fiddle.
Answers to your remaining questions:
//copyOfBlocks[key] = data; // WHY DOES COMMENTING THIS OUT STILL MAKE THE UPDATE WORK??
Because this line doesn't actually copy:
let copyOfBlocks = blocks;
// This outputs `true`!
console.log(copyOfBlocks === blocks);
This means that, when you do this in ModuleElement - changeValue...
blockElement[e.target.name] = e.target.value;
... you're mutating the same value as when you do this in ModuleBuilder - updateBlock ...
copyOfBlocks[key] = data;
... because copyOfBlocks[key] === blocks[key], and blocks[key] === ModuleBuilder's blockElement. Since both updaters had a reference to the same object, you were updating the same object twice.
But beyond that, mutation of state variables in React is an anti-pattern. This is because React uses === to detect changes. In this case, React cannot tell that myState has changed, and so will not re-render the component:
const [myState, setMyState] = useState({'existing key': 'existing value'});
// ...
// This is BAD:
myState['key'] = ['value'];
const [myState, setMyState] = useState({'existing key': 'existing value'});
// ...
// This is still BAD, because the state isn't being copied:
const newState = myState;
newState['key'] = ['value'];
// At this point, myState === newState
// React will not recognize this change!
setMyState(newState);
Instead, you should write code that
performs a shallow copy on myState, so the old state !== the new state, and
uses setMyState() to tell React that the state has changed:
const [myState, setMyState] = useState({'existing key': 'existing value'});
// ...
// This is correct, and uses ES6 spread syntax to perform a shallow copy of `myState`:
const newState = {...myState, key: 'value'};
// Now, newState !== myState. This is good :)
setMyState(newState);
Or, as a one-liner:
setMyState({...myState, key: 'value'});
The Module Element component now has its own state. onChange the state is updated and the new target.value is pushed to the props.updateblocks method. Is this the react way to do it?
For the most part, yes. You're duplicating your state in two variables, which is unnecessary and more likely to cause bugs in the future. See above for a suggestion on how to eliminate the additional state variable.
I was able to solve the problem after coincidentally reading this question.
I replaced the setBlocks line here:
const emptyBlock = {
fieldName: "",
dataType: "string",
required: true,
};
const add1Block = () => {
setBlocks([
...blocks,
emptyBlock
]);
};
by this statement
const add1Block = () => {
setBlocks([
...blocks,
{ fieldName: "", dataType: "string", required: true },
]);
};
and it turned out that by placing emptyBlock as a default value for a new element I was just apparently just re-referencing it.
I'm trying to update my react className when the active changes in the sites variable which is mapped to loop through the items.
What happens is that the className 'inactive' does not go away if the active status changes to true or visa versa.
Code:
// Context: this code is inside of the component
const [sites, setSites] = useState([]); <--- Updated dynamically with fetch()
const changeActive = (id) => {
const tmpSites = sites;
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
return (
{sites.length ? sites.map((item, i) => {
return (
<tr className={`${!Boolean(item.active) ? 'inactive' : ''}`} key={item.id}>
// inbetween data
</tr>
)
}) : null}
)
You need to create a copy of the sites array and make changes to the copy and then set it in state. Never mutate state directly as it might not cause a re-render as we are updating the state with the same object reference.
const changeActive = (id) => {
const tmpSites = [...sites];
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
Because you are mutating the original sites Object and not cloning it before making the changes, the useState ("setSites") does not actually re-renders the component because it cannot compare previous Object to current, because they are the same.
You must do a deep-clone of the sites Array of Objects:
const changeActive = (id) => {
setSites(sites => {
sites.map(site => ({ // ← first-level clone
...site // ← second-level clone
active: site.id === id ? !site.active : site.active
}))
})
}
It is imperative to use the setSites function that returns the current state and then you can reliably deep-clone it.
I am trying to create a dynamic input form where user can add new inputs.
Problem
The component is not rendering when State is updated but when i open a Model that exists in the same component that is using a State to open/close it, all pushed elements in the array displayed at one time.
I didn't write the code of the Modal to make it easier to read the code
My Code
const [supply_detail_list,setSupply_detail_list] = React.useState([
{supplyDetail:'',supplyDetailAmount:''}
]);
const addNewSuppDetailInput = () => {
supply_detail_list.push({supplyDetail:'',supplyDetailAmount:''});
setSupply_detail_list(supply_detail_list)
}
JSX
<div onClick={addNewSuppDetailInput}>+</div>
{
supply_detail_list.map((val,index) => {
console.log('rendered')
const detalTextID = 'supply_detail_text' + index;
const detalValtID = 'supply_detail_val'+ index;
return (
<div className='supply-details'>
•<input type='text' name={detalTextID} onChange={supply_detail_handler}/> <input className ='detail-value' onChange={supply_detail_handler} name={detalValtID} type='number'/>
</div>
);
})
}
</div>
react expects you to return a new array when you want to update it instead of mutating it. Try something like this:
const addNewSuppDetailInput = () => {
setSupply_detail_list([
...supply_detail_list,
{ supplyDetail: '', supplyDetailAmount: '' },
]);
};