How can I update a single array value using the useState()? - reactjs

I am trying to generate several checkboxes. I want that when you click on a checkbox the value of the check property changes to its opposite value. This would normally be done with:
check =! check
but I'm using hooks,
const [data,setData]=data;
and I don't know how to update this item without affecting the rest of my object. How can I do it?
this is my data, and my hooks definition:
let data={
"documents": {
"trucker": {
"checkDocuments": [
{
'label': 'licencia',
'check': false
},
{
'label': 'identificacion',
'check': false
},
{
'label': 'telefono',
'check': false
},
],
"section": "camionero"
},
"client": {
"checkDocuments": [
{
'label': 'comodato',
'check': false
}
],
"section": "cliente"
},
"container": {
"checkDocuments": [
{
'label': 'BL',
'check': false
}
],
"section": "contenedor"
}
}
}
const [data,setData]=data;
this is the code that goes in the render
return (
<List>
{
Object.keys(data).map((section, i) => {
return
<ListItem itemHeader first key={i}>
<Text>{data[section].section}</Text>
</ListItem>
data[section].checkDocuments.map((document, j) => {
return
<ListItem onPress={() => document.check = !document.check} key={j} > ***I need set only update this element in my data
<CheckBox checked={document.check} onPress={() => document.check = !document.check; } color="blue" /> ****I need set only update this element in my data
<Body>
<Text>{document.label}</Text>
</Body>
</ListItem>
})
})
}
</List>)
How can I have the whole object that I defined in my hook updated from the modification of an element, so that my code is rendered again?

I would recommend you make yourself a compound state hook that can update individual keys. It would behave like the setState method in a React Class Component.
Check this out for reference: https://github.com/fvaldez421/shopping-demo/blob/master/src/hooks/common.js
If one level of interaction isn't enough, you can copy that key from your state, edit the copy, then use it to update your state:
const { state, setState } = useCompound(initialState);
const updateTrucker = (type, updates) => {
if (type === 'documents') {
const { trucker: truckerOriginal } = state;
// copy it to prevent mutations
const trucker = { ...truckerOriginal };
trucker.checkDocuments = trucker.checkDocuments.map(
doc => { /* your document check update logic here */ }
);
setState(trucker);
}
}
The code above should update the trucker key in your state.

Related

React nested items rerender not working as expected

im trying to develop component that will sort new items, depending if item parent is or is not present on the list already. Components can be nested in each other to unlimited depth. Parent have list of children, children have parentId. Now, it works as expected at the first render, but when new item appear on the list (its added by user, using form, up in the structure), it does in fact make its way to components list, but is not shown on the screen until page reload. I can see temporary list that is used to make all calculations have the item as expected in the nested structure. Then i set state list to value of temp, but its not working, and i dont know why. Im quite new to react stuff. In act of desperation i even tried to destructure root parent of the item, hoping it will force rerender, but that didnt worked too. Anybody could help with this?
http://jsfiddle.net/zkfj03um/13/
import React, { useState } from 'react';
function Component(props) {
const [component, setComponent] = useState(props.component);
return (
<div>
{component.id};
{component.name};
<ul>
{component.subcomps && component.subcomps.map((comp) =>
<li key={comp.id} style={{ textAlign: 'left' }}>
<Component component={comp}
id={comp.id}
name={comp.name}
parentId={comp.parentId}
subcomps={comp.subcomps}
/>
</li>)}
</ul>
</div>
);
}
function ComponentsList(props) {
const newComponents = props.newComponents;
const [filteredComponents, setFilteredComponents] = useState();
function deepSearch(collection, key, value, path=[]) {
for (const o of collection) {
for (const [k, v] of Object.entries(o)) {
if (k === key && v === value) {
return {path: path.concat(o), object: o};
}
if (Array.isArray(v)) {
const _o = deepSearch(v, key, value, path.concat(o));
if (_o) {
return _o;
}
}
}
}
}
async function filter() {
let temp = [];
await newComponents.forEach((comp) => {
//parent may be, or may not be on the list. Its not necesary
const parentTuple = deepSearch(filteredComponents, 'id', comp.parentId);
if (!parentTuple) {
//create parent substitute logic
} else {
const parent = parentTuple.object;
const root = parentTuple.path[0];
const mutReplies = [comm, ...parent.replies];
parent.replies = mutReplies;
temp = [{...root}, ...temp]
}
})
setFilteredComponents([...temp])
}
useEffect(() => {
setLoading(false);
}, [filteredComponents]);
useEffect(() => {
setLoading(true);
filter();
}, [newComponents]);
return (<>
{!loading && filteredComponents.map((component, index) =>
<li key={index}>
<Component component={component} />
</li>
)}
</>);
}
const items = [
{ id: 1, name: 'sample1', subcomps: [{ id: 5, name: 'subcomp1', parentId: 1, subcomps: [] }] },
{
id: 2, name: 'sample2', subcomps: [
{ id: 6, name: 'subcomp2', subcomps: [], parentId: 2 },
{ id: 7, name: 'subcomp3', subcomps: [], parentId: 2 }
]
},
]
ReactDOM.render(<ComponentsList newComponents={items} />, document.querySelector("#app"))

Interupt code and wait for user interaction in a loop - React

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

Why i cannot update value of specific index in an array in react js via set State?

I have an array like below
[
1:false,
9:false,
15:false,
19:false,
20:true,
21:true
]
on click i have to change the value of specific index in an array.
To update value code is below.
OpenDropDown(num){
var tempToggle;
if ( this.state.isOpen[num] === false) {
tempToggle = true;
} else {
tempToggle = false;
}
const isOpenTemp = {...this.state.isOpen};
isOpenTemp[num] = tempToggle;
this.setState({isOpen:isOpenTemp}, function(){
console.log(this.state.isOpen);
});
}
but when i console an array it still shows old value, i have tried many cases but unable to debug.
This is working solution,
import React, { Component } from "react";
class Stack extends Component {
state = {
arr: [
{ id: "1", value: false },
{ id: "2", value: false },
{ id: "9", value: false },
{ id: "20", value: true },
{ id: "21", value: true }
]
};
OpenDropDown = event => {
let num = event.target.value;
const isOpenTemp = [...this.state.arr];
isOpenTemp.map(item => {
if (item.id === num) item.value = !item.value;
});
console.log(isOpenTemp);
this.setState({ arr: isOpenTemp });
};
render() {
let arr = this.state.arr;
return (
<React.Fragment>
<select onChange={this.OpenDropDown}>
{arr.map(item => (
<option value={item.id}>{item.id}</option>
))}
</select>
</React.Fragment>
);
}
}
export default Stack;
i hope it helps!
The problem is your array has several empty value. And functions like map, forEach will not loop through these items, then the index will not right.
You should format the isOpen before setState. Remove the empty value
const formattedIsOpen = this.state.isOpen.filter(e => e)
this.setState({isOpen: formattedIsOpen})
Or use Spread_syntax if you want to render all the empty item
[...this.state.isOpen].map(e => <div>{Your code here}</div>)

Unable to update redux store on manipulating the initial state

Using this action for updating the particular componentId in redux initial state. Facing 6the following violation error while updating the initial state.
Uncaught Invariant Violation: A state mutation was detected between dispatches, in the path "currentActivityJSON.draggedItems.1.isDisable". This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)
Code:
export function draggedItem(currentActivityJSON, componentId) {
let draggedItems = currentActivityJSON.draggedItems;
for (var i=0; i < draggedItems.length; i++ ) {
if (componentId === draggedItems[i].componentId) {
draggedItems[i ].isDisable = true;
}
}
return {
type: DRAGGED_ITEM,
currentActivityJSON: Object.assign({}, currentActivityJSON, {
...currentActivityJSON,
draggedItems: draggedItems
})
};
}
This is initial_state.json and need to update the 2nd array item `isDisable=true.
"currentActivityJSON": {
"defaultItems": [],
"expectedItems": [
"GLASS_BOTTLE"
],
"isDoneEnabled": true,
"draggedItems": [
{
"imageUrl": "../src/media/images/activities/box_big#2x.png",
"componentId": "CARTBOARD_BOX",
"componentText": "CardBoard Box",
"width": "100%",
"height": "70%",
"isDisable": false
},
{
"imageUrl": "../src/media/images/activities/bottle_big#2x.png",
"componentId": "PLASTIC_BOTTLE",
"componentText": "Plastic 2L Bottle",
"width": "50%",
"height": "50%",
"isDisable": false
},
{
"imageUrl": "../src/media/images/activities/glasstank_big1#2x.png",
"componentId": "GLASS_BOTTLE",
"componentText": "Glass Tank",
"width": "80%",
"height": "70%",
"isDisable": false
}]
}
This piece of the code mutates items in the original draggedItems array:
let draggedItems = currentActivityJSON.draggedItems;
for (var i=0; i < draggedItems.length; i++ ) {
if (componentId === draggedItems[i].componentId) {
draggedItems[i ].isDisable = true;
}
}
Instead create a new draggedItems array, replace the changed item:
const draggedItems = currentActivityJSON.draggedItems.map((item) => {
if(componentId === draggedItems[i].componentId) {
return {
...item,
isDisable: true
};
}
return item;
});
Avoid mutations outside of the reducer by instantly returning the action in the action creator. Instead of for-loops, use ES6 spread and array methods.
export function draggedItem(currentActivityJSON, componentId) {
return {
type: DRAGGED_ITEM,
currentActivityJSON: {
...currentActivityJSON,
draggedItems: [
...currentActivityJSON.draggedItems.filter(item => item.componentId !== componentId), {
...currentActivityJSON.draggedItems.find(item => item.componentId === componentId),
isDisable: true
}
]
})
};
}
By the way, I wouldn't name it "JSON" becaus in JavaScript, it is just an object (literal). And if you are using the object spread operator (...object), you don't really need to call Object.assign().
But I'm wondering, if it wouldn't be better to just dispatch the componentId in the action and perform the change of currentActivityJSON in the reducer.
This code works like a charm!
const draggedItems = currentActivityJSON.draggedItems.map((item) => {
if(componentId === draggedItems[i].componentId) {
return {
...item,
isDisable: true
};
}
return item;
});

How can I remove an attribute from a React component's state object

If I have a React component that had a property set on its state:
onClick() {
this.setState({ foo: 'bar' });
}
Is it possible to remove "foo" here from Object.keys(this.state)?
The replaceState method looks like the obvious method to try but it's since been deprecated.
You can set foo to undefined, like so
var Hello = React.createClass({
getInitialState: function () {
return {
foo: 10,
bar: 10
}
},
handleClick: function () {
this.setState({ foo: undefined });
},
render: function() {
return (
<div>
<div onClick={ this.handleClick.bind(this) }>Remove foo</div>
<div>Foo { this.state.foo }</div>
<div>Bar { this.state.bar }</div>
</div>
);
}
});
Example
Update
The previous solution just remove value from foo and key skill exists in state, if you need completely remove key from state, one of possible solution can be setState with one parent key, like so
var Hello = React.createClass({
getInitialState: function () {
return {
data: {
foo: 10,
bar: 10
}
}
},
handleClick: function () {
const state = {
data: _.omit(this.state.data, 'foo')
};
this.setState(state, () => {
console.log(this.state);
});
},
render: function() {
return (
<div>
<div onClick={ this.handleClick }>Remove foo</div>
<div>Foo { this.state.data.foo }</div>
<div>Bar { this.state.data.bar }</div>
</div>
);
}
});
ReactDOM.render(<Hello />, document.getElementById('container'))
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
var Hello = React.createClass({
getInitialState: function () {
return {
foo: 10,
bar: 10
}
},
handleClick: function () {
let state = {...this.state};
delete state.foo;
this.setState(state);
},
render: function() {
return (
<div>
<div onClick={ this.handleClick.bind(this) }>Remove foo</div>
<div>Foo { this.state.foo }</div>
<div>Bar { this.state.bar }</div>
</div>
);
}
});
In ReactCompositeComponent.js in the React source on GitHub is a method called _processPendingState, which is the ultimate method which implements merging state from calls to component.setState;
```
_processPendingState: function(props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = replace ? queue[0] : inst.state;
var dontMutate = true;
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
let partialState = typeof partial === 'function'
? partial.call(inst, nextState, props, context)
: partial;
if (partialState) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState);
}
}
}
```
In that code you can see the actual line that implements the merge;
nextState = Object.assign({}, nextState, partialState);
Nowhere in this function is there a call to delete or similar, which means it's not really intended behaviour. Also, completely copying the stat, deleting the property, and calling setState won't work because setState is always a merge, so the deleted property will just be ignored.
Note also that setState does not work immediately, but batches changes, so if you try to clone the entire state object and only make a one-property change, you may wipe over previous calls to setState. As the React document says;
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
What you could look to do is actually add more info;
this.setState({ xSet: true, x: 'foo' });
this.setState({ xSet: false, x: undefined });
This is ugly, granted, but it gives you the extra piece of info you need to differentiate between a value set to undefined, and a value not set at all. Plus it plays nice with React's internals, transactions, state change batching, and any other horror. Better to take a bit of extra complexity here than try to second-guess Reacts internals, which are full of horrors like transaction reconciliation, managing deprecated features like replaceState, etc
When we use undefined or null to remove a property, we actually do not remove it. Thus, for a Javascript object we should use the delete keyword before the property:
//The original object:
const query = { firstName:"Sarah", gender: "female" };
//Print the object:
console.log(query);
//remove the property from the object:
delete query.gender;
//Check to see the property is deleted from the object:
console.log(query);
However, in React Hooks we use hooks and the above method might cause some bugs especially when we use effects to check something when the state changes. For this, we need to set the state after removing a property:
import { useState, useEffect } from "react";
const [query, setQuery] = useState({firstName:"Sarah", gender:"female"});
//In case we do something based on the changes
useEffect(() => {
console.log(query);
}, [query]);
//Delete the property:
delete query.gender;
//After deleting the property we need to set is to the state:
setQuery({ ...query });
Previous solution - is antipattern, because it change this.state. It is wrong!
Use this (old way):
let newState = Object.assign({}, this.state) // Copy state
newState.foo = null // modyfy copyed object, not original state
// newState.foo = undefined // works too
// delete newState.foo // Wrong, do not do this
this.setState(newState) // set new state
Or use ES6 sugar:
this.setState({...o, a:undefined})
Pretty sweet, don't you? ))
In old React syntax (original, not ES6), this has this.replaceState, that remove unnecessary keys in store, but now it is deprecated
You can use Object.assign to make a shallow copy of your application's state at the correct depth and delete the element from your copy. Then use setState to merge your modified copy back into the application's state.
This isn't a perfect solution. Copying an entire object like this could lead to performance / memory problems. Object.assign's shallow copy helps to alleviate the memory / performance concerns, but you also need to be aware of which parts of your new object are copies and which parts are references to data in the application state.
In the example below, modifying the ingredients array would actually modify the application state directly.
Setting the value of the undesired element to null or undefined doesn't remove it.
const Component = React.Component;
class App extends Component {
constructor(props) {
super(props);
this.state = {
"recipes": {
"1": {
"id": 1,
"name": "Pumpkin Pie",
"ingredients": [
"Pumpkin Puree",
"Sweetened Condensed Milk",
"Eggs",
"Pumpkin Pie Spice",
"Pie Crust"
]
},
"2": {
"id": 2,
"name": "Spaghetti",
"ingredients": [
"Noodles",
"Tomato Sauce",
"(Optional) Meatballs"
]
},
"3": {
"id": 3,
"name": "Onion Pie",
"ingredients": [
"Onion",
"Pie Crust",
"Chicken Soup Stock"
]
},
"4": {
"id": 4,
"name": "Chicken Noodle Soup",
"ingredients": [
"Chicken",
"Noodles",
"Chicken Stock"
]
}
},
"activeRecipe": "4",
"warningAction": {
"name": "Delete Chicken Noodle Soup",
"description": "delete the recipe for Chicken Noodle Soup"
}
};
this.renderRecipes = this.renderRecipes.bind(this);
this.deleteRecipe = this.deleteRecipe.bind(this);
}
deleteRecipe(e) {
const recipes = Object.assign({}, this.state.recipes);
const id = e.currentTarget.dataset.id;
delete recipes[id];
this.setState({ recipes });
}
renderRecipes() {
const recipes = [];
for (const id in this.state.recipes) {
recipes.push((
<tr>
<td>
<button type="button" data-id={id} onClick={this.deleteRecipe}
>×</button>
</td>
<td>{this.state.recipes[id].name}</td>
</tr>
));
}
return recipes;
}
render() {
return (
<table>
{this.renderRecipes()}
</table>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<main id="app"></main>
Only way is to create a deep copy and then delete that property from the deep clone and return the deep clone from the setState updater function
this.setState(prevState => {
const newState = {
formData: {
...prevState.formData,
document_details: {
...prevState.formData.document_details,
applicant: {
...prevState.formData?.document_details?.applicant
keyToBeDeleted: dummVAlue //this is redundant
}
}
}
};
delete newState.formData.document_details.applicant.keyToBeDeleted;
return newState;
});
Use dot-prop-immutable
import dotPropImmutable from "dot-prop-immutable";
onClick() {
this.setState(
dotPropImmutable.delete(this.state, 'foo')
);
}
If you want to completely reset the state (removing a large number of items), something like this works:
this.setState(prevState => {
let newState = {};
Object.keys(prevState).forEach(k => {
newState[k] = undefined;
});
return newState;
});
Using this variant of setState allows you to access the whole state during the call, whereas this.state could be a little out of date (due to prior setState calls not yet having been fully processed).
I think this is a nice way to go about it =>
//in constructor
let state = {
sampleObject: {0: a, 1: b, 2: c }
}
//method
removeObjectFromState = (objectKey) => {
let reducedObject = {}
Object.keys(this.state.sampleObject).map((key) => {
if(key !== objectKey) reducedObject[key] = this.state.sampleObject[key];
})
this.setState({ sampleObject: reducedObject });
}
If the removal is in a function and the key needs to be a variable, try this :
removekey = (keyname) => {
let newState = this.state;
delete newState[keyname];
this.setState(newState)
// do not wrap the newState in additional curly braces
}
this.removekey('thekey');
Almost the same as steve's answer, but in a function.

Resources