Rendering a list of items in React with shared state - javascript

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>

Related

Select Next/Previous list item in React without re-rendering

I'm learning React. Say that I have a ListContainer which renders several ListItems. I want to keep track of the currently selected ListItem, render it in another color, and be able to navigate up and down.
One way would be to store selectedItem as state in ListContainer, and send it down as a prop to ListItem. But if I do it this way, then every time I change selectedItem I will rerender all ListItems because they are dependent on selectedItem. (I should only have to re-render two ListItems, the one that gets deselected, and the one that gets selected).
Is there a way to implement next and previous function without re-rendering all items?
Note: I know that React doesn't re-render unnecessarily in the DOM, but I'm trying to optimize operations on virtual DOM also.
Edit: Here is my example in code. It renders a list, and when the user click one item it gets selected. We also see that "ListItem update" gets printed 100 times, each time we change selection, which happens regardless of PureComponent or React.memo.
let mylist = []
for (let i = 0; i < 100; i++) {
mylist.push({ text: "node:" + i, id: i })
}
window.mylist = mylist
const ListItem = React.memo (class extends Component {
componentDidUpdate() {
console.log('ListItem update')
}
render() {
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
return (
<li
style={{ backgroundColor }}
onMouseDown={() => this.props.setSelected(this.props.item.id)}
>
{this.props.item.text}
</li>
)
}
})
class ListContainer extends Component {
constructor(props) {
super(props)
this.state = {
selectedItem: 10
}
this.setSelected = this.setSelected.bind(this)
}
setSelected(id) {
this.setState({ selectedItem: id })
this.forceUpdate()
}
render() {
return (
<ul>
{this.props.list.map(item =>
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
</ul>
)
}
}
function App() {
return (
<ListContainer list={mylist} />
);
}
The state you suggesting is the right way to implement it...
The other problem with unnecessary renders of the list item can easily be soved by wrapping the export statement like this:
export default React.memo(ListItem)
This way the only elements that has changed their props will rerender.. be aware that overuse of This can cause memory leaks when using it unnecessarily...
UPDATE
according to your example in addition to the React.memo you can update the way you transfer props to avoid senfing the selected item in each item...
istead of:
let backgroundColor = this.props.item.id === this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={this.state.selectedItem}
setSelected={this.setSelected}
/>)}
do :
let backgroundColor = this.props.selectedItem ? 'lightgreen' : 'white'
...
<ListItem
item={item}
key={item.id}
selectedItem={item.id === this.state.selectedItem}
setSelected={this.setSelected}
/>)}
this way the react memo will prevent rerenders when it is possible...

How to append a dynamic HTML element to React component using JSX?

I'm new to Reactjs. I'm creating an App with a Survey creation like Google Forms. My component has a button to create a new Div with some HTML elements to create a survey question. To do that, on the button click function, I'm creating a JSX element and push it to an array. And then, I set the array inside the render function to display what inside it.
The problem is, Even though the Array is updating, the dynamic HTML part can not be seen on the page. The page is just empty except the button. How can I fix this?
Component:
import '../../styles/css/surveycreation.css';
import React, { Component } from 'react';
let questionId = 0;
class SurveyCreation extends Component {
constructor(props) {
super(props);
this.questionsList = [];
this.state = {
}
}
addQuestion = (e) => {
e.preventDefault();
questionId = questionId + 1;
this.questionsList.push(
<div key={questionId}>
question block
</div>
)
}
render() {
return (
<div>
<button onClick={e => this.addQuestion(e)}>Add Question</button>
<div>
{this.questionsList}
</div>
</div>
);
}
};
export default SurveyCreation;
The only way a react component knows to rerender is by setting state. So you should have an array in your state for the questions, and then render based on that array. In the array you want to keep data, not JSX elements. The elements get created when rendering.
constructor(props) {
super(props);
this.state = {
questions: [],
}
}
addQuestion = () => {
setState(prev => ({
// add some object that has the info needed for rendernig a question.
// Don't add jsx to the array
questions: [...prev.questions, { questionId: prev.questions.length }];
})
}
render() {
return (
<div>
<button onClick={e => this.addQuestion(e)}>Add Question</button>
<div>
{this.state.questions.map(question => (
<div key={question.questionId}>
</div>
)}
</div>
</div>
);
}
I think you're component is not re-rendering after you fill the array of elements.
Try adding the questionsList to the component's state and modify your addQuestion method so that it creates a new array, finally call setState with the new array.
You need to map your this.questionsList variable.
You can save the 'question string' in the array and then iterate the array printing your div..
Something like this.
<div>
{this.state.questionsList.map(questionString, i => (
<div key={i}>
{questionString}
</div>
)}
</div>

State Change Causing Sidebar Reload

I am building an online store. When you click the checkout button, a sidebar slides into view showing the list of items in your cart. Inside of the cart is its list of items. You can change the quantity by toggling the up/down arrows in a Form.Control element provided by Bootstrap-React.
The way my code works is that when you toggle the up/down arrows to add or decrease the product quantity the state changes in the parent regarding what's in your cart. This triggers the child cart sidebar to close then reopen. I do not want this to happen! The sidebar should remain open.
I've tried two things; one is to use event.preventDefault() to try and make it so the page isn't refreshed, but this hasn't worked.
The other thing is trying to use shouldComponentUpdate and checking for whether the item quantity was changed, then preventing the app from re-rendering. This is the code I was using:
shouldComponentUpdate(nextProps, nextState) {
if (
nextState.cart &&
nextState.cart.length > 0 &&
this.state.cart.length > 0
) {
console.log("Next state cart num= " + nextState.cart[0].num)
console.log("curr state cart num= " + this.state.cart[0].num)
if (nextState.cart[0].num != this.state.cart[0].num) {
return false;
}
}
return true;
}
The problem is that my previous and future props are the same! Hence I can't write any code preventing re-rendering on item quantity change.
Can anyone provide some advice?
If your component is re rendering but its props and state aren't changing at all then you could prevent this with either React memo if you're using a function or if you're using a class based component then extending React.PureComponent instead of React.Component.
Both ways will do a shallow prop and state comparison and decide whether it should re render or not based on the result of said comparison. If your next props and state are the same as before then a re render will not be triggered.
Here's a codepen example so you can decide which one to use.
class App extends React.Component {
state = {
count: 0
};
handleClick = event => {
event.preventDefault();
this.setState(prevState => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<span>Click counter (triggers re render): {this.state.count}</span>
<button style={{ marginLeft: "10px" }} onClick={this.handleClick}>
Click me to re render!
</button>
<SingleRenderClassComponent />
<SingleRenderFunctionComponent />
<AlwaysReRenderedClassComponent />
<AlwaysReRenderedFunctionComponent />
</div>
);
}
}
class SingleRenderClassComponent extends React.PureComponent {
render() {
console.log("Rendered React.PureComponent");
return <div>I'm a pure component!</div>;
}
}
const SingleRenderFunctionComponent = React.memo(
function SingleRenderFunctionComponent() {
console.log("Rendered React.memo");
return <div>I'm a memoized function!</div>;
}
);
class AlwaysReRenderedClassComponent extends React.Component {
render() {
console.log("Rendered React.Component");
return <div>I'm a class!</div>;
}
}
function AlwaysReRenderedFunctionComponent() {
console.log("Rendered function component");
return <div>I'm a function!</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));

Can't display what is held in State property, but can display what I add to it - ReactJS

Please find my code at this link
I am writing a simple todo list (basically crud) on my own after watching a tutorial, so that I can get to grips with React properly.
After playing around with the code, I eventually had it where the pre-populated list of tasks were being displayed - but I could not add anything to the list.
Now however, I can add things to the list (which is good) - but I can't seem to display the pre-populated items within the list (which is bad).
I feel that my main culprit is the following, but I'm not sure what is going on:
ListBody
displayTasks() {
return this.props.tasks.map((task, index) =>
<ListItem key={index} {...task} />
);
}
listTasks() {
console.log(this.props.tasks);
}
render() {
return(
<div>
<button onClick={this.listTasks.bind(this)}>list</button>
{this.displayTasks()}
</div>
)
}
ListItem
render() {
return (
<div>
{this.props.task}
</div>
)
}
Update
After some further investigation - I was creating an edit/delete button for each entry - To which I found that the 3 pre-populated tasks in my list actually displayed the buttons, but not the text of the task itself...
You are spreading all the task properties onto ListItem when it looks like you want to just set a task prop. Try this instead:
<ListItem key={index} task={task} />
I could make your example code work with a few modifications:
//list-item.js
class ListItem extends React.Component {
render() {
return (
<div>
{this.props.task.name}
</div>
)
}
}
//list-body.js
class ListBody extends React.Component {
displayTasks() {
return this.props.tasks.map((task, index) =>
<ListItem key={index} task={task} />
);
}
//app.js
createTask(task) {
console.log('Creating task...');
console.log(task);
this.setState((prevState) => {
prevState.tasks.push({ name: task, isComplete: false });
return {
tasks: prevState.tasks
}
})
this.printTasks();
}
Here is the working demo: link
var that = this
<ListItem key={index} onClick={that.listTasks.bind(task) />

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