I'm new to react and struggling to get this to work. I can display a mapped array, but I am trying to update the array, map it, then display it. Can't figure out how to get the mapped array to display after it's updated?
Tried to add the mappedItems to my function also, but that didn't seem to solve it. TIA.
import React, { Component } from 'react';
const itemsList = ['item1','item2'];
class RecieveInput extends React.Component {
constructor(){
super();
this.state = {
list: itemsList
}
}
render(){
const pressedEnter = event => {
if (event.key === 'Enter') {
this.state.list.push(event.target.value);
console.log(this.state.list);
}
}
const mappedItemsList = this.state.list.map( (listItem, i) => <li>{this.state.list[i]}</li> )
return(
<div>
<input
type ="text"
id="textInput"
placeholder="Enter Item Here"
onKeyPress={pressedEnter}
/>
<ol className='ol'>{mappedItemsList}</ol>
</div>
)
}
}
export default RecieveInput
enter image description here
Issue
The pressedEnter callback should be declared in the component, not the render function. It should also not mutate the state object by pushing into the state array.
Solution
Move the pressedEnter handler into the component and use this.setState to enqueue an update. You need to shallow copy the existing state and append the new element into it so your list state is a new array reference.
const pressedEnter = event => {
const { value } = event.target;
if (event.key === 'Enter') {
this.setState(prevState => ({
list: prevState.list.concat(value), // or [...prevState.list, value]
}));
}
}
Related
I have the following code. When changing the value of one element, the entire list is re-render. How can it be avoided by redrawing only one element? I use setState and class components.
import React from "react";
import "./styles.css";
class ListItem extends React.Component {
handleUpdate = () => {
this.props.onUpdate(this.props.index);
};
totalRenders = 0;
render() {
const { data: { id, value } } = this.props;
this.totalRenders += 1;
console.log("Row rendered");
return (
<div>
<strong>id: </strong>{id}:
<strong>value: </strong>{value}
<strong>renders count: </strong>{this.totalRenders}
<button className="button" onClick={this.handleUpdate}>
Change row value
</button>
</div>
);
}
}
export default class App extends React.Component {
state = { list: [{id: 'id 1', value: '11'}, {id: 'id 2', value: '22'}]};
handleUpdate = (index) => {
let newState = this.state.list.slice()
newState[index].value = Math.round(Math.random() + 10);
this.setState(() => ({
list: newState
}));
};
render() {
console.log("App rendered");
return (
<div>
{this.state.list.map((el, index) => (
<ListItem
key={el.id}
data={el}
index={index}
onUpdate={this.handleUpdate}
/>
))}
</div>
);
}
}
Sandbox: https://codesandbox.io/s/autumn-architecture-ubgh51?file=/src/App.js
If you update your app state then that component is supposed to be updated. That the expected behaviour and that's how it should be behave. Now coming to your question. If changes in a row should not make your entire app re-render. Then you should manage your state in your item and any changes in that should be maintained in that to unnecessary re-render.
Here's an example of how to achieve it. You can replace your ListItem with this and check it yourself.
function UpdatedListItem({ data }) {
const [row, setRow] = React.useState(data);
React.useEffect(() => setRow(data), [data]);
const { id, value } = row;
console.log("Changes in: ", data);
function handleChange() {
setRow({
...row,
value: Math.round(100 + Math.random() * 5)
});
}
return (
<div>
<strong>id: </strong>
{id}:
<strong>value: </strong>
{value}
<strong>renders count: </strong>
{this.renders}
<button className="button" onClick={handleChange}>
Change row value
</button>
</div>
);
}
One way you can fix this is by using a implementing shouldcomponentupdate
but that method should be implemented when you need some performance improvement not just to stop re renderings
This method only exists as a performance optimization. Do not rely on it to “prevent” a rendering, as this can lead to bugs.
Next thing you can consider is using PureComponent
to implemented in that way you need to change the way you handle state update
handleMyChange = (index) => {
// Don't mutate the state in react it could lead in to unexpected results
// let newState = [...this.state.list];
// newState[index].value = Math.round(Math.random() + 10);
const newState = this.state.list.map((item, idx) => {
if (idx === index) {
// Replacing with a new item, so the other component can understand it's a new item and re-render it.
return {
...item,
value: Math.round(100 + Math.random() * 5)
};
}
// Return the same item, so the other component do not render since it's the same item.
return item;
});
this.setState(() => ({
list: newState
}));
};
class ListItem extends React.PureComponent {
......
React.PureComponent implements it with a shallow prop and state comparison, inorder to detect it as a new update we have to tell the ListItem component the props are different, to do that we should avoid mutating object in the array, instead we have to create a new item and replace the old array element.
Here is an good example of mutating state in react
Here is the updated sandbox
In your case, you can use the shouldComponentUpdate, reference: React document.
Simple sample:
shouldComponentUpdate(nextProps, nextState) {
// Custom update conditions.
return nextProps.data.id !== this.props.data.id;
}
Full sample:
※Additional Notes
If using function component, you can use memo to do it, reference: React document.
I have a parent component housing two children components(AddPersonForm and PeopleList). When I submit a name via the AddPersonForm, I expect it to be rendered in the PeopleList component, but it doesn't.
Here is my AddPersonForm:
class AddPersonForm extends React.Component {
state = {
person: ""
}
handleChange = (e) => this.setState({person: e.target.value});
handleSubmit = (e) => {
if(this.state.person != '') {
this.props.parentMethod(this.state.person);
this.setState({person: ""});
}
e.preventDefault();
}
render() {
return (
<form onSubmit={this. handleSubmit}>
<input type="text" placeholder="Add new contact" onChange={this.handleChange} value={this.state.person} />
<button type="submit">Add</button>
</form>
);
}
My PeopleList component:
class PeopleList extends React.Component {
constructor(props) {
super(props);
const arr = this.props.data;
this.state = {
listItems: arr.map((val, index) => <li key={index}>{val}</li> );
}
}
render() {
return <ul>{this.state.listItems}</ul>;
}
}
Now the parent component, ContactManager:
class ContactManager extends React.Component {
state = {
contacts: this.props.data
}
addPerson = (name) => {
this.setState({contacts: [... this.state.contacts, name]});
render() {
return (
<div>
<AddPersonForm parentMethod={this. addPerson}×/>
<PeopleList data={this.state.contacts} />
</div>
);
Please what I'm I doing wrong, or not doing?
The issue is in your PeopleList component. The state object which renders your list is created in the constructor when the component mounts, but you have no way of updating it when it recieves new values. It will always give you the initial value.
You could introduce a lifecycle method, componentDidUpdate, which would allow you to compare the previous props to the new props when they arrive, and update the state accordingly. I would recommend you not do this for two reasons:
Storing props directly in a components state is not good practice. You are just creating a copy of the state in the component above and that creates opportunities for confusion and stale values when one of them updates. Ideally, each piece of data should live in only one place.
If all PeopleList is doing is rendering your data, then it doesn't need any state at all. It can act as a display component that maps your props in place and doesn't have to worry about updating itself or managing its own data. This would actually make it a good candidate for conversion into a functional component.
class PeopleList extends React.Component {
render() {
return (
<ul>
{this.props.data.map((val, index) => (
<li key={index}>{val}</li>
))}
</ul>
);
}
}
You are initializing PeopleList with props when its created and mounted but then you are not using new values of props for updating it.
To fix your issue use current value of prop when rendering:
class PeopleList extends React.Component {
render() {
return <ul>{ this.props.data.map((val, index) => <li key={index}>{val}</li>) }</ul>;
}
}
I'm writing a React stateful component to process text inputted in a textbox, and to save and load text between a database and the textbox. I can type in the textbox, and I can save what's in the textbox to my database. When I load from the database to the textbox, my component's state updates, and the (child) textbox's prop which handles passing text from the parent component to the textbox updates according to React DevTools in Chrome. The problem is that the value of the textbox is not re-rendered to reflect the new prop.
I did some research, and I found that React components are supposed to re-render whenever the state changes. I tried adding state to the child component using useState(), and I update the state every time the props change using
useEffect(() => setText(propText), [propText])
It didn't work. The props and state in React DevTools show the value I loaded in, but the component doesn't show it in the textbox. Maybe it isn't designed to get its input value from props and state as opposed to a user typing, but its onChange() event handler updates the parent state like I'm trying to do with the database access component, so it's completely abstracted.
Here's the child component as it stands right now:
function InputTextComponent(props) {
const { parentInput, textUpdate } = props;
const [text, setText] = useState(parentInput);
useEffect(() => setText(parentInput), [parentInput]);
const onTextChange = event => {
const {
target: { value }
} = event;
textUpdate(value);
};
return (
<TextAreaGroup
placeholder={'Input text'}
name={'Input text here'}
value={text}
info={'Input text'}
onChange={event => onTextChange(event)}
/>
);
}
Here's the parent component:
class ParentComponent extends Component {
constructor() {
super();
this.state = {
textInput: '',
};
this.textUpdate.bind(this);
}
textUpdate = textInput => this.setState({ textInput });
render() {
const {
textInput,
} = this.state;
return (
<div className="container mt-3">
<InputTextComponent
textUpdate={this.textUpdate}
parentInput={textInput}
/>
<LoadTextsComponent
alias={alias} // alias is irrelevant part of the component
aliasUpdate={this.aliasUpdate}
skuUpdate={this.skuUpdate}
/>
</div>
);
}
}
The database load component uses the textUpdate() function. Here's the full component:
function LoadTextsComponent(props) {
const { alias, aliasUpdate, textUpdate } = props;
const [setList, setListUpdate] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await client.get('/texts/listTexts');
aliasUpdate(result[0].alias);
setListUpdate(result);
};
fetchData();
}, []);
const loadTextsToInputBox = () => {
let aliasSet;
for (let set of setList) {
if (set.alias === alias) {
aliasSet = set.texts;
}
}
let textString = '';
for (let text of aliasSet) {
textString += text + '\n';
}
textString = textString.slice(0, -1);
textUpdate(textString);
};
return (
<div>
<ShowAliasesComponent
setList={setList}
alias={alias}
aliasUpdate={aliasUpdate}
/>
<button
disabled={!alias}
className="btn btn-primary"
onClick={() => loadTextsToInputBox()}
>
Send texts of alias {alias} to input box
</button>
</div>
);
}
What's really confusing is when I type in the child textbox, the parent state updates correctly (passing down a function to update parent state) and the new prop is reflected in the render. But, when I call that same function from the database load component, nothing happens.
I'm a bit stuck on filtering a ToDo list, with the ultimate goal of deleting a completed To Do.
I was successful in passing the deleteTodo() method to the child Component Todo, and also in retrieving the index of the Todo to delete.
However, I tried to move forward and use the filter method to filter the list of Todos and filter out the deleted one (I don't want to use slice, but practice with filter()), but I am not succeeding in using it.
I have 2 files, App.js and the child component ToDo.js
Thanks!
App.js (for full code: https://codeshare.io/24n7Yj)
deleteTodo(index) {
const todos = this.state.todos.filter();
const todo = todos[index];
this.setState({ todos: todos });
}
ToDo.js
import React, { Component } from 'react';
class ToDo extends Component { //define a class that extends Component
render() {
return (
<li>
<input type="checkbox" checked={ this.props.isCompleted } onChange={ this.props.toggleComplete } />
<span>{ this.props.description }</span>
<button onClick ={this.props.deleteTodo}>Delete</button>
</li>
);
}
}
export default ToDo; //the component is made to export the data
The array method filter takes a function as first argument that is invoked with each element in the array, and the element's index. You can return true for all elements except the one with the same index as the one you pass in to the deleteTodo method.
deleteTodo(index) {
this.setState(prevState => {
const todos = prevState.todos.filter((todo, i) => i !== index);
return { todos };
});
}
The filter method takes a callback function as an argument. It will go through each element giving you the element and it's index. You can filter out the one you want by doing the following and checking if the index is the same as the one provided :
deleteTodo = index => {
this.setState(prevState => ({
todos: prevState.todos.filter((todo, i) => i !== index)
}));
}
Im trying to make a search function that renders the name of the people that is matched in a search text input.
The problem is that I set the state to the items that match the search, and then the initial state is lost so no more searching can be done since the state will be empty. So how do I "fill up" the state each time?
Or maybe there is some other way without actually setting the state that im not aware of.
I tried to fix this with an attempt to reset to initial state when the handleSearch function is called right before the filter but that doesnt work.
import React from 'react';
import Header from './Header';
import peopleData from '../persons.json';
class App extends React.Component {
constructor(){
super();
this.handleSearch = this.handleSearch.bind(this);
this.state = {
people: peopleData
}
}
handleSearch(wordToMatch){
this.setState({ people: peopleData }); //Attempt to reset to initial state
const result = this.state.people.filter(d => {
const regex = new RegExp(wordToMatch, 'gi');
return d.Name.match(regex);
});
this.setState({ people: result })
}
render() {
const list = this.state.people.map((d, i) => <li key={i}>{d.Name}</li>);
return (
<div className="myApp">
<Header
tagline={"testing"}
handleSearch={this.handleSearch}
/>
<ul className="contentBody">
{list}
</ul>
</div>
)
}
}
export default App;
Component with the search input:
import React from 'react';
class Header extends React.Component {
render() {
return (
<header>
<input
type="text"
placeholder={this.props.tagline}
ref={(input) => this.searchInput = input}
onChange={() => this.props.handleSearch(this.searchInput.value)}
>
</input>
</header>
)
}
}
export default Header;
How my data looks like
[
{
"Name": "Adam",
"Born": 1971
},
{
"Name": "Bob",
"Born": 1999
},
etc etc for 20 more names
The setState function won't immediately update the state object. So when you reference this.state.people, it will reference the state prior to the setState call. You can update your code to:
handleSearch(wordToMatch) {
const result = peopleData.filter(d => {
const regex = new RegExp(wordToMatch, 'gi');
return d.Name.match(regex);
});
this.setState({
people: result
})
}
In the handleSearch set the state for the searchString variable. Then in the render method, instead of simply mapping the state, you first filter the people list, and that result is what you map.
Change:
const list = this.state.people.map((d, i) => <li key={i}>{d.Name}</li>);
into this:
const list = this.state.people.filter(d => {
const regex = new RegExp(this.state.searchString, 'gi');
return d.Name.match(regex);
}).map((d, i) => <li key={i}>{d.Name}</li>);
This way, the list in the state is left unaltered, and you filter when rendering.