React: Associate values of input fields with removed components - javascript

I have an array of items stored in this.state.items and user can add/remove items by clicking buttons which modify this.state.items.
I have something like this. (This code is untested, and may not compile, but you probably get the idea.)
TextField = React.createClass({
render() {
return <input type="text"/>;
}
});
TextList = React.createClass({
getInitialState () {
return {
items: [<TextField />, <TextField />, <TextField />]
};
},
addItem() {
// Adds a new <TextField /> to this.state.items
},
removeItem(index) {
// Filters out the item with the specified index and updates the items array.
this.setState({items: this.state.items.filter((_, i) => i !== index)});
},
render() {
return (
<ul>
{this.state.items.map((item, index) => {
return (
<li key={index}>
{item}
<button onClick={this.props.removeItem.bind(null, index)}>Remove</button>
</li>
);
})}
</ul>
<button onClick={this.addItem}>Add New Item</button>
);
}
});
This can remove the specified item in the this.state.items. I saw it in console, and this part is working properly. But that's not what is presented to the user.
For example, if there are 3 input fields, and user types "One", "Two", and "Three" respectively, then if he clicks on the remove button for "Two", the input field with "Three" is removed instead. In other words, always the last field is removed.
How can I fix this so that the value of the input fields are properly associated with the removed ones?

This is because react recycles items based on their key, for speed and efficiency. Using an index that always is 0,1,2 etc therefore has unwanted consequences.
How react works:
you have a list of items indexed 0,1,2: which react renders.
user deletes first item
the list is now 2 items long: index 0,1
when react re-renders, it deducts (falsely) from your keys that item 0,1 are unchanged (because the keys are identical), and that the third item is removed.
Solution: make the key unique to the specific item. Better to base on item content.

Related

React not rendering correctly after state change

I have a simple form. You click an "Add Item" button and a textbox appears. On blur, the text entered in the textbox gets added to a state variable array. Click the "Add Item" button again, another textbox appears and so on.
For each textbox, there is also a "Remove Item" button. When this button is clicked, the current item is removed from the array and the current textbox is removed from the page.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
}
}
addItem() {
this.setState({
items: [...this.state.items, []]
}
)
}
removeItem(index) {
//var items = this.state.items;
var items = [...this.state.items];
items.splice(index, 1);
this.setState({
items: items
})
}
changeItem(e, index) {
var items = this.state.items;
items[index] = e.target.value;
this.setState({
items: items
})
}
render() {
return (
<div>
{
this.state.items.map((item, index) => {
return (
<React.Fragment key={index}>
<hr />
<Row>
<Col column sm="8">
<Form.Control
type="text"
name="item"
onBlur={(e) => this.changeItem(e, index)}
/>
</Col>
</Row>
<Row>
<Col column sm="8">
<Button
onClick={() => this.removeItem(index)}
variant="link"
size="sm">
Remove Item
</Button>
</Col>
</Row>
</React.Fragment>
)
})
}
<br />
<Button
onClick={(e) => this.addItem(e)}
variant="outline-info">Add item
</Button>
</div>
)
}
}
The problem I have is, although the array is successfully modified in removeItem(index), the textbox that gets removed from the page is always the last one added, not the one that should be removed. For example:
Click "Add Item", type: aaa items: ['aaa']
Click "Add Item", type: bbb items: ['aaa', 'bbb']
Click "Add Item", type: ccc items: ['aaa', 'bbb', 'ccc']
Click "Remove Item" under aaa. Items gets successfully updated: items: ['bbb', 'ccc']
The page should show a textbox with bbb and one with ccc. But it shows:
How can I remove the correct textbox from the page?
There are a few problems with your code:
Firstly, you are directly changing the this.state without using this.setState() in the changeItem function, I have changed it to var items = [...this.state.items];
You are using index as key for a list item, that you are rendering using the this.state.items.map((item, index) => {...}) in <React.Fragment key={index}>. This key should be a unique identifier for the list item, usually, it is the unique id from the database. Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity. However, in your case, since you don't have unique ids for the items, I am creating those using uuid module. learn more about keys: https://reactjs.org/docs/lists-and-keys.html and https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318
I have removed column attribute from the Col component, because it was giving some warning
Since, now I am adding a unique id to each list item, I have changed the structure of the this.state.items from array of strings to array of objects, where each object has a id and data, where data is text
you were using Form.Control component without using value prop. Suppose you added one list item, wrote something in the input, clicked on the add item button again. at this point, since you changed focus, the onBlur event would trigger, and your this.state.items would change accordingly, so far, so good. BUT now when it re-renders the whole thing again, it is going to re-render the Form.Control component, but without the value prop, this component will not know what data to show, hence it will render as empty field. Hence, I added value prop to this component
Since I added value prop in Form.Control component, react now demands that I add onChange event to the component, otherwise it will render as read-only input, hence I changed onBlur to onChange event. There is no need for onBlur to change the state value, when onChange is already there.
Here is the finished code:
import React from "react";
import { v4 } from 'uuid';
import { Button, Row, Col, Form } from "react-bootstrap";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
}
addItem() {
this.setState({
items: [...this.state.items, {data: "", id: v4()}]
});
}
removeItem(index) {
console.log("dbg1", index);
//var items = this.state.items;
var items = [...this.state.items];
items.splice(index, 1);
this.setState({
items: items
});
}
changeItem(e, index) {
console.log("dbg2", this.state.items);
var items = [...this.state.items];
items[index].data = e.target.value;
this.setState({
items: items
});
}
render() {
console.log("dbg3", this.state.items);
return (
<div>
{this.state.items.map((item, index) => {
return (
<React.Fragment key={item.id}>
<hr />
<Row>
<Col sm="8">
<Form.Control
type="text"
name="item"
value={item.data}
onChange={(e) => this.changeItem(e, index)}
// onBlur={(e) => this.changeItem(e, index)}
/>
</Col>
</Row>
<Row>
<Col sm="8">
<Button
onClick={() => this.removeItem(index)}
variant="link"
size="sm"
>
Remove Item
</Button>
</Col>
</Row>
</React.Fragment>
);
})}
<br />
<Button onClick={(e) => this.addItem(e)} variant="outline-info">
Add item
</Button>
</div>
);
}
}
export default App;

FlatList update is slow

I have a FlatList that holds 5000 items. There is an option to switch on the "Select Mode", which renders a checkbox next to each item. Whenever I press the corresponding button to switch to that mode, there is a noticeable delay before checkboxes are actually rendered.
This is my state:
{
list: data, // data is taken from an external file
selectedItems: [] // holds ids of the selected items,
isSelectMode: false
}
My list:
<FlatList
data={list}
extraData={this.state}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
removeClippedSubviews
/>
My list item:
<ListItem
title={item.title}
isCheckboxVisible={isSelectMode}
isChecked={isChecked}
onPress={() => this.toggleItem(item)}
/>
Each list item implements shouldComponentUpdate:
...
shouldComponentUpdate(nextProps) {
const { isCheckboxVisible, isChecked } = this.props;
return isChecked !== nextProps.isChecked || isCheckboxVisible !== nextProps.isCheckboxVisible;
}
...
I always thought that FlatList only renders items that a currently visible within the viewport, but here it feels like the entire list of 5000 items is re-rendered. Is there anything I can improve here? Or perhaps I am doing something totally wrong?
Full code: https://snack.expo.io/#pavermakov/a-perfect-flatlist

How to select group based checkbox antd in reactjs

I want to select a group based checkbox. The problem is that when I click on the group, the entire checkbox is selected. I don't want to select the entire checkbox. this is my Initial State.
const plainOptions = ["can_view", "can_create", "can_update"];
state = {
checkedList: [],
indeterminate: true,
checkAll: false
};
Method: onchange method basically works each individual checkbox.
onChange = checkedList => {
console.log(checkedList);
this.setState({
checkedList,
indeterminate:
!!checkedList.length && checkedList.length < plainOptions.length,
checkAll: checkedList.length === plainOptions.length
});
};
This method works for selected all checkbox
onCheckAllChange = e => {
console.log(e.target.checked);
this.setState({
checkedList: e.target.checked ? plainOptions : [],
indeterminate: false,
checkAll: e.target.checked
});
};
{
["group", "topGroup"].map(item => (
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
{item}
</Checkbox>
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
));
}
However, my accepted Data format is
{group:["can_view","can_create"],topGroup:["can_view","can_create"}
I want to get this format output when user selected on the checkbox
Here is the code sandbox : https://codesandbox.io/s/agitated-sea-1ygqu
The reason both groups change when you click something in one of them is because both groups use the same internal state.
["group", "topGroup"].map(item => (
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
{item}
</Checkbox>
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
));
Both the group and topGroup use the same this.state.checkList state.
The easiest way to solve this is by extracting each group into its own component. This way they have their own state separate of each other.
You could also opt to keep one component, but you must manage multiple internal states. You could for example use state = { checkList: [[], []] } where the first sub-array is to store the group state and the second sub-array is to store the topGroup state.
If groups are dynamic you can simply map over the groups and create your states that way:
state = { checkList: groups.map(() => []) };
You would also need to manage multiple indeterminate and checkAll states. This can be avoided when you deduce those from the checkList state. For example:
isIndeterminate(index) {
const checkList = this.state.checkList[index];
return checkList.length > 0 && checkList.length < plainOptions.length;
}
This would also avoid conflicting state, since there is one source of truth.

Rendering a list of items in React with shared state

Original Question
I'm trying to render a list of items using React. The key is that the items share a common state, which can be controlled by each item.
For the sake of simplicity, let's say we have an array of strings. We have a List component that maps over the array, and generates the Item components. Each Item has a button that when clicked, it changes the state of all the items in the list (I've included a code snippet to convey what I'm trying to do).
I'm storing the state at the List component, and passing down its value to each Item child via props. The issue I'm encountering is that the button click (within Item) is not changing the UI state at all. I believe the issue has to do with the fact that items is not changing upon clicking the button (rightfully so), so React doesn't re-render the list (I would have expected some kind of UI update given the fact that the prop isEditing passed onto Item changes when the List state changes).
How can I have React handle this scenario?
Note: there seems to be a script error when clicking the Edit button in the code snippet, but I don't run into it when I run it locally. Instead, no errors are thrown, but nothing in the UI gets updated either. When I debug it, I can see that the state change in List is not propagated to its children.
Edited Question
Given the original question was not clear enough, I'm rephrasing it below.
Goal
I want to render a list of items in React. Each item should show a word, and an Edit button. The user should only be able edit one item at a time.
Acceptance Criteria
Upon loading, the user sees a list of words with an Edit button next to each.
When clicking Edit for item 1, only item 1 becomes editable and the Edit button becomes a Save button. The rest of the items on the list should no longer show their corresponding Edit button.
Upon clicking Save for item 0, the new value is shown for that item. All the Edit buttons (for the rest of the items) should become visible again.
Problem
On my original implementation, I was storing an edit state in the parent component (List), but this state wasn't properly being propagated to its Item children.
NOTE: My original implementation is lacking on the state management logic, which I found out later was the main culprit (see my response below). It also has a bind bug as noted by #Zhang below. I'm leaving it here for future reference, although it's not really a good example.
Here's my original implementation:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditing: false
};
}
toggleIsEditing() {
this.setState((prevState) => {
return {
isEditing: !prevState.isEditing
}
});
}
render() {
return (
<ul>
{items.map((val) => (
<Item value={val}
toggleIsEditing={this.toggleIsEditing}
isEditing={this.state.isEditing}/>
))}
</ul>
);
}
}
class Item extends React.Component {
render() {
return (
<li>
<div>
<span>{this.props.value}</span>
{ !this.props.isEditing &&
(<button onClick={this.props.toggleIsEditing}>
Edit
</button>)
}
{ this.props.isEditing &&
(<div>
<span>...Editing</span>
<button onClick={this.props.toggleIsEditing}>
Stop
</button>
</div>)
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, 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>
<body>
<div id="app" />
</body>
you didn't bind the parent scope when passing toggleIsEditing to child component
<Item value={val}
toggleIsEditing={this.toggleIsEditing.bind(this)}
isEditing={this.state.isEditing}/>
I figured out the solution when I rephrased my question, by rethinking through my implementation. I had a few issues with my original implementation:
The this in the non-lifecycle methods in the List class were not bound to the class scope (as noted by #ZhangBruce in his answer).
The state management logic in List was lacking other properties to be able to handle the use case.
Also, I believe adding state to the Item component itself was important to properly propagate the updates. Specifically, adding state.val was key (from what I understand). There may be other ways (possibly simpler), in which case I'd be curious to know, but in the meantime here's my solution:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
editingFieldIndex: -1
};
}
setEdit = (index = -1) => {
this.setState({
editingFieldIndex: index
});
}
render() {
return (
<ul>
{items.map((val, index) => (
<Item val={val}
index={index}
setEdit={this.setEdit}
editingFieldIndex={this.state.editingFieldIndex} />
))}
</ul>
);
}
}
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
val: props.val
};
}
save = (evt) => {
this.setState({
val: evt.target.value
});
}
render() {
const { index, setEdit, editingFieldIndex } = this.props;
const { val } = this.state;
const shouldShowEditableValue = editingFieldIndex === index;
const shouldShowSaveAction = editingFieldIndex === index;
const shouldHideActions =
editingFieldIndex !== -1 && editingFieldIndex !== index;
const editableValue = (
<input value={val} onChange={(evt) => this.save(evt)}/>
)
const readOnlyValue = (
<span>{val}</span>
)
const editAction = (
<button onClick={() => setEdit(index)}>
Edit
</button>
)
const saveAction = (
<button onClick={() => setEdit()}>
Save
</button>
)
return (
<li>
<div>
{ console.log(`index=${index}`) }
{ console.log(`editingFieldIndex=${editingFieldIndex}`) }
{ console.log(`shouldHideActions=${shouldHideActions}`) }
{
shouldShowEditableValue
? editableValue
: readOnlyValue
}
{
!shouldHideActions
? shouldShowSaveAction
? saveAction
: editAction
: ""
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, 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>
<body>
<div id="app" />
</body>

ReactJS - array in this.state is updated correctly on delete but result isn't rendered until later state changes?

I've seen variations of this question posted but none match my problem. I am displaying an array of text fields and want to be able to delete one of them. When the delete button next to any text field is clicked, the last item in the array disappears. However, I've checked this.state and the correct item was deleted, just the wrong one was taken from the rendered array. When I click a button that hides the text fields and then un-hide, the array is fixed and shows the correct item deleted. Why is this happening? The state is updated with what I want removed, but it seems like it renders something other than the state. I've tried using this.forceUpload(); in the setState callback but that does not fix it.
I've included some relevant snippets from the code and can include more if needed.
export class Questions extends React.Component {
constructor(props) {
super(props);
this.state = {
...
choices: []
...
};
}
deleteChoice = (index) => {
let tempChoices = Object.assign([], this.state.choices);
tempChoices.splice(index, 1);
this.setState({choices: tempChoices});
};
renderChoices = () => {
return Array.from(this.state.choices).map((item, index) => {
return (
<li key={index}>
<TextField defaultValue={item.text} style={textFieldStyle} hintText="Enter possible response..."
onChange={(event) => { this.handleChoice(event, index); }}/>
<i className="fa fa-times" onClick={() => { this.deleteChoice(index); }}/>
</li>
);
});
};
render() {
let choices = (
<div>
<div className="choices">Choices</div>
<ul>
{this.renderChoices()}
<li>
<RaisedButton label="Add another" style={textFieldStyle} secondary
onClick={() => { this.addChoice(); }}/>
</li>
</ul>
</div>
);
return (
{choices}
);
}
}
Thanks for any help, this is wicked frustrating.
You need to use a key other than the array index in your renderChoices function. You can read more about why this is the case in React's docs:
https://facebook.github.io/react/docs/multiple-components.html#dynamic-children
When React reconciles the keyed children, it will ensure that any
child with key will be reordered (instead of clobbered) or destroyed
(instead of reused).
Consider using item.text as the key or some other identifier specific to that item.

Categories

Resources