React not rendering correctly after state change - javascript

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;

Related

How to arrange React child components order?

With react-semantic-ui, I made a searchable, paginationable Table React Component
working example: https://codesandbox.io/s/23q6vlywy
Usage
<PaginationTable
items={[
{
id: 1,
name: "test-item-1 ???"
},
{
id: 2,
name: "test-item-2"
},
{
id: 3,
name: "test-item-3"
}
]}
columnOption={[
{
header: "idHeader",
cellValue: "id",
onItemClick: item => {
alert(item.id);
},
sortingField: "id"
},
{
header: "Name~",
cellValue: item =>
`*custom text cannot be searched* property can item.name => ${
item.name
} `,
onItemClick: item => {
alert(item.name);
},
sortingField: "name"
}
]}
initSortingField={"id"}
initSortingOrderAsc={false}
pagination
itemsPerPage={2}
searchKeyProperties={["id", "name"]}
/>
But this component currently can only have a certain order
1.SearchBar on top
2.Table on middle
3.PaginationBar on bottom
And actually, I didn't write 3 child components in the Component, SearchBar, Table, PaginationBar
So it's hard for me to rewrite it to render props way to change the order such as below
<PaginationTable {...props}>
{({ SearchBar, Table, PaginationBar })=>
<div>
<SearchBar />
<Table />
<SearchBar />
</PaginationTable>
</div>
</PaginationTable>
Because when I tried to change to render props, I first have to write 3 components independently, which means I have to change all variables under this( this.state, this.props, this.xxxFunction ) to something else.
For example:
In , I can use
<Input onChange={()=>{
this.setState({ searchBarText: e.target.value });
}}/>
But If I change it to 3 components, I have to rewrite it to something like
const SearchBar = ({onTextChange}) => <Input onChange=
{onTextChange}/>
<SearchBar onTextChange={()=>{
this.setState({
searchBarText: e.target.value
});
}} />
Is there any way I can adjust child components order elegantly or is there any way I can write render props easier?
Updated # 2018.10.27 17:36 +08:00
I modified <PaginationTable>'s render function as below but it will be lost mouse cursor focus when typing on <SearchInput>
render(){
const SearchBar = ()= (<Input onChange={()=>{...} />);
const TableElement = ()=>(<Table {...} > ... </Table>);
const PaginationBar = ()=>(<Menu {...} > ... </Menu>);
enter code here
return(
<React.Fragment>
{this.props.children({ SearchBar,TableElement, PaginationBar })}
</React.Fragment>)};
Then I found out, in this way, DOM will update every time when my state updated because every render will make a new component reference by const SearchBar = ()=>(...).
Thanks to #Ralph Najm
If I declare child component as below, DOM will not update every time.
render(){
const SearchBar = (<input onChange={...} />);
const TableElement = (<Table .../>);
const PaginationBar = (<Menu .../>);
//... same as above
}
In this way, I can change my component to render props in order to arrange the order very easily.
Thanks in advance.
In the PaginationTable component change your render function and before the return statement, assign the elements to variables (SearchBar, Table, PaginationBar, etc...) then just reference those variables in the render function.
I modified your example here https://codesandbox.io/s/vy4799zp45

Material-UI Disabled attribute not working

I'm trying to disable the edit button once i click on complete but it is not working. I have passed in the state in disabled attribute but it seems not doing anything, don't know maybe because of setState's asynchronous nature. I passed callback while calling setState method and it seems logging data randomly, Can someone suggest what should be done ?
class App extends Component {
state = {
buttons: {
id: "test"
}
};
handleCheckBox = id => {
let buttons = Object.assign({}, this.state.buttons);
buttons.id = !this.state.buttons[id]
this.setState({buttons}, ()=>console.log(this.state.buttons));
}
render() {
return (
<div>
{todos.map(todo => (
<List key={todo.id}>
<ListItem
role={undefined}
dense
button
>
<Checkbox
onClick={()=>this.handleCheckBox(todo.id)}
checked={todo.complete}
tabIndex={-1}
disableRipple
/>
<ListItemText primary={todo.text} />
<ListItemSecondaryAction>
<Button mini color="secondary" variant="fab" disabled={this.state.buttons[todo.id]}>
<Icon>edit_icon</Icon>
</Button>
ListItemSecondaryAction>
</ListItem>
</List>
))}
</div>
);
}
}
Instead of using id to change the state use index of Array to update the state
Create an array in Component state which tracks the disabled attribute of each buttons
state = {
buttons: Array(todos.length).fill(false),
};
In componentDidMount initialise the array according to todos
componentDidMount(){
const buttons=this.state.buttons.slice();
for(var i=0;i<buttons.length;i++)
buttons[i]=todos[i].complete;
this.setState({buttons:buttons})
}
Now use the value in buttons state for disabled attribute of button based on the index of the component being rendered.
<Button mini color="secondary" variant="fab"
disabled={buttons[todos.indexOf(todo)]}>
Whenever CheckBox is clicked pass the index to the handleChange function and update the value corresponding to the index value
<Checkbox
onClick={() =>this.handleCheckBox(todos.indexOf(todo))}
checked={buttons[todos.indexOf(todo)]}{...other}
/>
handleCheckBox = index => {
const buttons=this.state.buttons.slice();
buttons[index] = !buttons[index];
this.setState({
buttons:buttons
})
}

React: json index undefined passed with array iterator through onclick button

so for this component, it just shows a list of cards with some information passed through the props which is a JSON array set into events state. So, since it's a variable number of results each time I created an array of the number of times I want to the component to be rendered which works fine. Each card has its relevant information passed through
this.state.events[i]
Ok all that works fine EXCEPT one spot, which is the FlatButton onClick action.
<FlatButton label="Download ICS" onClick={() => console.log(this.state.events[i].location)} />
If I try access the state with i, then I will get a
TypeError: Cannot read property 'location' of undefined
or whatever property of the JSON I choose.
BUT, if i use an actual manual index like 5, 4, 3, etc then it works just fine. Thing is, I need it to be different for each button, otherwise other event cards have a button that does the wrong thing for that event. Keep in mind again that for all the other times I use i to access the JSON array, it works fine above such as in the CardText component.
Here is a visual:
I don't know, it might have something to do with component life cycle, but I don't know enough to know precisely.
Thanks
class SearchResultsList extends Component {
constructor(props) {
super(props);
this.state = {
events: [],
eventsLength: 0,
};
}
componentDidMount() {
var count = Object.keys(this.props.events).length;
this.setState({ events: this.props.events });
this.setState({ eventsLength: count });
};
render() {
var cardSearchHolder = [];
for(var i = 0; i < this.state.eventsLength; i++) {
cardSearchHolder.push(
(
<div key={i}>
<Card>
key={this.state.events[i]._id}
<CardHeader
title={this.state.events[i].event_name}
subtitle={this.convertDateTime(this.state.events[i].start_date, this.state.events[i].end_date)}
actAsExpander={true}
showExpandableButton={true}
/>
<CardText expandable={true}>
<div>
Category: {this.state.events[i].event_category}
</div>
<div>
Where: {this.state.events[i].location}
</div>
<div id="miniSpace"></div>
<div>
Description: {this.state.events[i].event_description}
</div>
</CardText>
<CardActions>
<FlatButton label="Download ICS" onClick={() => console.log(this.state.events[i].location)} />
</CardActions>
</Card>
<div id="space"></div>
</div>)
);
}
return(
<div>
<MuiThemeProvider>
<Card>
{cardSearchHolder}
</Card>
</MuiThemeProvider>
<div id="miniSpace"></div>
</div>
);
}
}

Filtering an array in React

I am making a To-Do Application in React. I got stuck at some point.
I'm mapping through items in an array, and displaying it in an unordered list. I'm trying to use the filter function, to remove the deleted items from the array.
I assume that the problem in my code can be somewhere there, that I am passing the event object but pointing to the button, instead of the list item.
How can I do it in React? You can find my code attached below. Also it would be great to clear the input field after submitting an item.
import React, { Component } from 'react';
class ToDoList extends Component {
constructor(props) {
super(props);
this.state = {list: [], items: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRemove = this.handleRemove.bind(this);
}
handleChange(event) {
this.setState({items: event.target.value})
console.log(event.target.value);
}
handleSubmit(event) {
this.setState({ list: [...this.state.list, this.state.items]})
event.preventDefault();
}
handleRemove(event) {
const filteredArray = this.state.list.filter(item => item !== event.target.value)
this.setState({list: filteredArray});
console.log(event.target);
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
<input
type="text"
value={this.state.value}
onChange={this.handleChange} />
</label>
<input
onClick={this.handleSubmit}
type="submit"
value="Submit" />
</form>
<ul>
{this.state.list.map((i, index) => (
<li key={index+1}>
{i}
<button
onClick={this.handleRemove}>
X
</button>
</li>
))}
</ul>
<p>Remaining: {this.state.list.length}</p>
</div>
);
}
}
export default ToDoList;
I would recommend using the optional additional arguments to .bind in order to change what's passed to your handler.
Call .bind(this. index) within your map method to pass the index of the element to be removed to the handler:
<ul>
{this.state.list.map((i, index) => (
<li key={index+1}>{i}<button onClick={this.handleRemove.bind(this, index)}>X</button></li>
))}
</ul>
And then update the handler to just remove the specified element:
handleRemove(index) {
const filteredArray = this.state.list.filter((_, i) => i !== index);
this.setState({
list: filteredArray
});
}
As to your second question, you should first fix up your input so its value is actually controlled by the state by changing value={this.state.value} to value={this.state.items}:
<input type="text" value={this.state.items} onChange={this.handleChange} />
And then simply clear this.state.items upon submission:
handleSubmit(event) {
this.setState({
list: [...this.state.list, this.state.items],
items: ''
})
event.preventDefault();
}
Check at this line:
<li key={index+1}>{i}<button onClick={this.handleRemove}>X</button></li>
Inside handleRemove, the instruction event.target point to the button. A button doesn't have a value attribute. So, you need to go up to parent and get it's content:
// childNodes[0] is a TextNode, so, to get it's content use data pop
const toRemove = event.target.parentNode.childNodes[0].data.trim();
const filteredArray = this.state.list.filter(item => item !== toRemove);
Another option is wrap the item in a element, like span:
<li key={index+1}><span>{i}</span><button onClick={this.handleRemove}>X</button></li>
And on remove get it to get the item value:
const toRemove = event.target.parentNode.children[0].textContent.trim();
const filteredArray = this.state.list.filter(item => item !==
event.target.value)

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