How to update key value inside array when state changes? - javascript

I am using React.js and in render method I have <Range/> component. What I want to do, is to be able to change the value and update the state's array accordingly. This is what I am talking about:
...
{
this.state.tableData.map((i, k) => {
return (
<>
<tr key={k}>
<td>
<div>
<Range
values={[i.s_rate]}
min={MIN}
max={MAX}
onChange={(values) => this.onSliderChangeS(values, k)}
...
So I trigger the onSliderChangeS function, which takes the new value of s_rate as values from the user, and it has to update and show my new this.state.tableData accordingly. I am trying to do it this way:
onSliderChangeS = (values, key) => {
this.setState({
valuesS: values,
tableData[key].s_rate: parseInt(values)
})
}
But this line this.state.tableData[key].s_rate: parseInt(values) doesn't seem to work at all. How can I do that?

setState takes an object with key/value entries. this.state.tableData[key].s_rate itself is not a key name in your state. See if this helps:
onSliderChangeS = (values, key) => {
const newTableData = [...this.state.tableData]
newTableData[key].s_rate = parseInt(values)
this.setState({
valuesS: values,
tableData: newTableData
})
}
Also note the missing comma as pointed out by #FaFa

Related

Dynamic input value in React

So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.

Mutate prevState in setState hooks updates the view without re-render. Why?

My changeProductName function called setState which return a "mutated prevState". I pass the function to and call it in children component via ContextAPI. The function successfully updated a product name displayed in children and parent, but the parent did to fire a re-render. How does the parent updated the view without re-rendering? Can anyone explain what the prevState actually is in setState?
const App = () => {
const [products, setProducts] = useState(initialValues);
const changeProductName = (id, newName) => {
setProducts((prevState) => { //is preState a copy of state?
prevState.products.filter(
(product) => product.id === id
)[0].name = newName; //Mutates prevState
return prevState; //Did I return a new state?
});
};
useEffect(() =>
console.log("I would know when App re-renders")); //No re-render!
return (
<> //Some React Switch and Routers
<div>
{product.map(product=>product.name)} //Successfully Updated!
</div>
<ProductContext value={(products, changeProductName)}>
<ProductPage /> //call changeProductName and it works!
</ProductContext>
</>
);
};
If I change the function not touching prevState, the parent re-renders as expected. Is this method better?
//this will trigger parent re-render.
const changeProductName = (id, newName) => {
setProducts((prevState) => {
prevState.products.filter(
(product) => product.id === id
)[0].name = newName;
return prevState;
});
};
Can anyone explain what the prevState actually is in setState?
prevState is a reference to the previous state. It is not a copy of the state, it is a reference of the object that sits inside the state. So changing that object will not alter the object reference.
Therefore it should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState.
For example, if you do a check inside your changeProduct name like:
setProducts(prevState => {
prevState.filter(product => product.id == id)[0].name = newName;
console.log(prevState === products); // This will console true
return prevState;
});
Also, as you are using hooks, when you write setProducts((prevState) => { prevState.products}... the prevState itself is already the products. So you will get an undefined error in your example when trying to access .products.
So I would recommend you to do:
const changeProductName = (id, newName) => {
setProducts(prevProducts =>
prevProducts.map(product =>
product.id === id ? { ...product, name: newName } : product
)
);
};
.map will build a new array based on prevState, and change the name of the products that have the id called in the function.
As far as I know, mutating the state is generally a bad idea.
According to this answer, mutating the state may not result in a re-render, since the reference to the state object is not changed during the mutation.
I'd rather use some kind of redux-like immutable pattern:
const changeProductName = (id, newName) => {
setProducts((prevState) => (
prevState.map(product=>{
if(product.id!==id){
// name is not changed since the id does not match
return product;
} else {
// change it in the case of match
return {...product, name:newName}
}
}
)
}

How to sort React components based on specific value?

I have React component. This components take 'units' - (array of objects) prop. Based on that I render component for each of item. I want to sort my components based on 'price' value, which is one of state items property. But when i trigger the sorting - state changes correctly but my components order not changing.
const SearchBoxes = ({units}) => {
const [unitsState, setUnitsState] = useState([])
useEffect(() => {
setUnitsState(units)
}, [units])
const sortByPrice = () => {
const sortedUnits = sort(unitsState).desc(u => u.price); // sorting is correct
setUnitsState(sortedUnits) // state is changing correctly
}
return (
<div>
{unitsState.map((u,i) => {
return <UnitBox key={u.price} unit={u} />
})}
</div>
)
}
Can somebody help me, please ?
Why my components order do not changing when the state is changing after sort triggering ?
You aren't calling sortByPrice anywhere--all you've done is to define the function. I haven't tried it, but what if you changed useEffect to:
useEffect(() => {
setUnitsState(sort(unitsState).desc(u => u.price));
}, [units])
Then you don't need the sort method at all.

Filter Array of Objects based on input field in React

Got in a rather troublesome situation
I have an array of objects
[
{
"title":"placeholder",
"text":"placeholder"
},
{
"title":"test",
"text":"placeholder"
},
{
"title":"javascript",
"text":"placeholder"
}
]
I am displaying them in a div,but thats not important
I got an input field which users should type in title's and as they type the array should only show matching object.
Inputing java would show the javascript titled object
I need to somehow change the array so it doesnt display anything but the entered title and if the input is empty shows the whole array
I am using React but i can only use hooks
So i copy the json
var [arrayOfObjects, setArray] = useState(Json)
the Json is imported from a local file
arrayOfNotes is the array that i need to change
pointing out so its easier to understand
ty in advance
The array filter method is what you're looking for.
Here's what your component might looks like.
const List = ({ data }) => {
const [value, setValue] = useState('')
return (
<div>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
{data
.filter(item => {
if (!value) return true
if (item.title.includes(value) || item.text.includes(value)) {
return true
}
})
.map(item => (
<div>
<h1>{item.title}</h1>
<p>{item.text}</p>
</div>
))
}
</div>
)
}
And you pass your json data to that component
<List data={Json} />
Here's a working example of the above code
You didn't share your component so I'll assume you know how to get the input value and call the corresponding variable input, besides you have your original array, from your example I judge it is called Json.
Then you can filter your value as follows:
const [arrayOfObjects, setArray] = useState(Json);
const filteredArray = input ?
originalArray.filter(item => item.title.includes(input) :
originalArray;
So that later you can render the filteredArray as follows:
<ul>
{filteredArray.map(item => (<li>{item.title}</li>))}
</ul>

onClick on "a" tag not working properly if it has some child elements in react

Below is the dynamic form which I created. It works fine. but Inside "a" tag if i add some child element to "a" tag, onClick event on "a" tag does not execute properly and pass proper name.
import React, { Component } from "react";
import "./stylesheet/style.css";
export default class Main extends Component {
state = {
skills: [""]
};
dynamicInputHandler = (e, index) => {
var targetName = e.target.name;
console.log(targetName);
var values = [...this.state[targetName]];
values[index] = e.target.value;
this.setState({
[targetName]: values
});
};
addHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
let values = [...this.state[targetName]];
values.push("");
this.setState({
[targetName]: values
});
};
removeHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
console.log(e.target.name);
let values = [...this.state[targetName]];
values.splice(index, 1);
this.setState({
[targetName]: values
});
};
render() {
return (
<div>
<form className="form">
{this.state.skills.map((value, index) => {
return (
<div className="input-row row">
<div className="dynamic-input">
<input
type="text"
placeholder="Enter skill"
name="skills"
onChange={e => {
this.dynamicInputHandler(e, index);
}}
value={this.state.skills[index]}
/>
</div>
<div>
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
</div>
</div>
);
})}
<button
name="skills"
onClick={e => {
this.addHandler(e);
}}
>
Add
</button>
</form>
{this.state.skills[0]}
</div>
);
}
}
i want to add Icon inside "a" tag , after adding icon tag, forms breaks and gives error "TypeError: Invalid attempt to spread non-iterable instance"
This works -
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
This does not (after adding icon inside a tag)
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
<i>Remove</i>
</a>
</span>
Inside removeHandler let targetName = e.target.parentElement.name;. When you wrap Remove in another tag, the event target is now the i tag, for which name is undefined.
I think this mixture of DOM manipulation (getting target of an event) is okay, but in React, where data is preferred over DOM values, you could work entirely on the component state and touch the DOM element values only once in the input where the skill value is displayed. Like this:
class Main extends Component {
state = {
skills: [],
lastSkillAdded: 0
};
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
render() {
return (
<form>
{this.state.skills.map((skill, index) => (
<div key={skill.id}>
<input
type="text"
value={skill.value}
onChange={e => this.updateSkillById(skill.id, e.target.value)}
/>
<button onClick={() => this.removeSkillById(skill.id)}>
Remove
</button>
</div>
))}
<button onClick={() => this.appendEmptySkill()}>Add</button>
</form>
);
}
}
Let's deconstruct what's going on there.
First, the lastSkillAdded. This is a controversial thing, and I guess someone will correct me if I'm wrong, but
when we iterate over an array to render, say, list items, it's recommended that we provide key prop that represents the item key,
and the value of key prop is not recommended to be the index of the array element.
So we introduce an artificial counter that only goes up and never goes down, thus never repeating. That's a minor improvement though.
Next, we have the skills property of component state. It is where all the values are going to be stored. We agree that each skill is represented by an object of two properties: id and value. The id property is for React to mark array items property and optimize reconciliation; the value is actual text value of a skill.
In addition, we have three methods to work on the list of skills that are represented by three component methods:
appendEmptySkill to add an empty skill to the end of the state.skills array,
updateSkillById to update the value of a skill knowing id and new value of it,
and removeSkillById, to just remove the skill by its id from state.skills.
Let's walk through each of them.
The first one is where we just append a new empty skill:
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
Because appending a new skill never depends on any input values, this method takes zero arguments. It updates the skill property of component state:
[...prevState.skills, { id: lastSkillAdded, value: '' }]
which returns a new array with always the same empty skill. There, we also assign the value to id property of a skill to our unique counter.
The next one is a bit more interesting:
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
We agree that in order to change a skill, we need to know its id and the new value. So our method needs to receive these two values. We then map through the skills, find the one with the right id, and change its value. A pattern
(id, value) => array.map(item => item.id !== id ? item : ({ ...item, value }));
is, I believe, a pretty common one and is useful on many different occasions.
Note that when we update a skill, we don't bother incrementing our skill id counter. Because we're not adding one, it makes sense to just keep it at its original value.
And finally, removing a skill:
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
Here, we only need to know the id of a skill to remove it from the list of skills. We filter our state.skills array and remove the one skill that matches the id. This is also a pretty common pattern.
Finally, in render, four things are happening:
existing skills are iterated over, and for each skill, input and "Remove" button are rendered,
input listens to change event and refers to EventTarget#value attribute in updateSkillById function call, because that's the only way to feed data from DOM back into React, and
the "Remove" buttons refers to skill id and calls removeSkillById,
and outside the loop, the "Add" button listens to click events and calls appendEmptySkill whenever it happens without any arguments, because we don't need any.
So as you can see, this way you're in total control of the render and state updates and never depend on DOM structure. My general advice is, if you find yourself in a situation when DOM structure dictates the way you manage component or app state, just think about a way to separate DOM from state management. So hope this answer helps you solve the issue you're having. Cheers!

Categories

Resources