I am in need of updating deeply nested object in a React state from a recursively rendered component. The items look like this and can be nested dynamically:
const items = [
{
id: "1",
name: "Item 1",
isChecked: true,
children: []
},
{
id: "3",
name: "Item 3",
isChecked: false,
children: [
{
id: "3.1",
name: "Child 1",
isChecked: false,
children: [
{
id: "3.1.1",
name: "Grandchild 1",
isChecked: true,
children: []
},
{
id: "3.1.2",
name: "Grandchild 2",
isChecked: true,
children: []
}
]
},
{
id: "3.2",
name: "Child 2",
isChecked: false,
children: []
}
]
}
]
I have a problem figuring out how to update the top level state from within a deeply nested component, because it only "knows" about itself, not the entire data structure.
class App extends React.Component {
state = {
items
};
recursivelyRenderItems(items) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<input type="checkbox" checked={item.isChecked} onChange={(event) => {
// TODO: Update the item's checked status
}} />
{this.recursivelyRenderItems(item.children)}
</li>
))}
</ul>
)
}
render() {
return (
<div>
{this.recursivelyRenderItems(this.state.items)}
</div>
);
}
}
How can I achieve this, please?
This works in your fiddle you posted.
Basically, each component needs to know about its item.isChecked and its parent's isChecked. So, create a component that takes 2 props, item and parentChecked where the latter is a boolean from the parent and the former becomes an mutable state variable in the constructor.
Then, the component simply updates its checked state in the event handler and it all flows down in the render method:
import React from "react";
import { render } from "react-dom";
import items from "./items";
class LiComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
item: props.item
}
}
render() {
var t = this;
return (
<ul>
<li key={t.state.item.id}>
{t.state.item.name}
<input
type="checkbox"
checked={t.state.item.isChecked || t.props.parentChecked}
onChange={event => {
t.setState({item: {...t.state.item, isChecked: !t.state.item.isChecked}})
}}
/>
</li>
{t.state.item.children.map(item => (
<LiComponent item={item} parentChecked={t.state.item.isChecked}/>
))}
</ul>
);
}
}
class App extends React.Component {
state = {
items
};
render() {
return (
<div>
{this.state.items.map(item => (
<LiComponent item={item} parentChecked={false} />
))}
</div>
);
}
}
render(<App />, document.getElementById("root"));
https://codesandbox.io/s/treelist-component-ywebq
How about this:
Create an updateState function that takes two args: id and newState. Since your ids already tell you what item you are updating: 3, 3.1, 3.1.1, you can from any of the recursive items call your update function with the id of the item you want to modify. Split the id by dots and go throw your items recursively to find out the right one.
For example from the recursive rendered item 3.1.1, can call updateState like this: updateState('3.1.1', newState).
import React from "react";
import { render } from "react-dom";
import items from "./items";
class App extends React.Component {
state = {
items
};
updateState = item => {
const idArray = item.id.split(".");
const { items } = this.state;
const findItem = (index, children) => {
const id = idArray.slice(0, index).join(".");
const foundItem = children.find(item => item.id === id);
if (index === idArray.length) {
foundItem.isChecked = !item.isChecked;
this.setState(items);
return;
} else {
findItem(index + 1, foundItem.children);
}
};
findItem(1, items);
};
recursivelyRenderItems(items) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<input
type="checkbox"
checked={item.isChecked}
onChange={event => this.updateState(item)}
/>
{this.recursivelyRenderItems(item.children)}
</li>
))}
</ul>
);
}
render() {
return <div>{this.recursivelyRenderItems(this.state.items)}</div>;
}
}
render(<App />, document.getElementById("root"));
Working example.
Related
I am trying to create an ObjectList component, which would contain a list of Children.
const MyList = ({childObjects}) => {
[objects, setObjects] = useState(childObjects)
...
return (
<div>
{childObjects.map((obj, idx) => (
<ListChild
obj={obj}
key={idx}
collapsed={false}
/>
))}
</div>
)
}
export default MyList
Each Child has a collapsed property, which toggles its visibility. I am trying to have a Collapse All button on a parent level which will toggle the collapsed property of all of its children. However, it must only change their prop once, without binding them all to the same state. I was thinking of having a list of refs, one for each child and to enumerate over it, but not sure if it is a sound idea from design perspective.
How can I reference a dynamic list of child components and manage their state?
Alternatively, is there a better approach to my problem?
I am new to react, probably there is a better way, but the code below does what you explained, I used only 1 state to control all the objects and another state to control if all are collapsed.
Index.jsx
import MyList from "./MyList";
function Index() {
const objList = [
{ data: "Obj 1", id: 1, collapsed: false },
{ data: "Obj 2", id: 2, collapsed: false },
{ data: "Obj 3", id: 3, collapsed: false },
{ data: "Obj 4", id: 4, collapsed: false },
{ data: "Obj 5", id: 5, collapsed: false },
{ data: "Obj 6", id: 6, collapsed: false },
];
return <MyList childObjects={objList}></MyList>;
}
export default Index;
MyList.jsx
import { useState } from "react";
import ListChild from "./ListChild";
const MyList = ({ childObjects }) => {
const [objects, setObjects] = useState(childObjects);
const [allCollapsed, setallCollapsed] = useState(false);
const handleCollapseAll = () => {
allCollapsed = !allCollapsed;
for (const obj of objects) {
obj.collapsed = allCollapsed;
}
setallCollapsed(allCollapsed);
setObjects([...objects]);
};
return (
<div>
<button onClick={handleCollapseAll}>Collapse All</button>
<br />
<br />
{objects.map((obj) => {
return (
<ListChild
obj={obj.data}
id={obj.id}
key={obj.id}
collapsed={obj.collapsed}
state={objects}
setState={setObjects}
/>
);
})}
</div>
);
};
export default MyList;
ListChild.jsx
function ListChild(props) {
const { obj, id, collapsed, state, setState } = props;
const handleCollapse = (id) => {
console.log("ID", id);
for (const obj of state) {
if (obj.id == id) {
obj.collapsed = !obj.collapsed;
}
}
setState([...state]);
};
return (
<div>
{obj} {collapsed ? "COLLAPSED!" : ""}
<button
onClick={() => {
handleCollapse(id);
}}
>
Collapse This
</button>
</div>
);
}
export default ListChild;
Please help me with this I don't understand exactly where I doing wrong. So when I click on the checkbox values are not changing(if it's by default true when I click on the click it should the false). For that in onChange in todoList component I am calling handleClick function there I change the todo.completed value(basically toggling the values).
In App.js inside the handleClick method When do console.log(todo) before returning from the map function value is toggling fine, but it is not updated in the updatedTodo.
App.js
import TodosData from "./todoData";
import TodoList from "./todoList";
import "./styles.css";
class App extends Component {
constructor() {
super();
this.state = {
todos: TodosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState(prevState => {
const updatedTodo = prevState.todos.map(todo => {
// console.log(updatedTodo);
if(todo.id === id) {
console.log("before the opt "+todo.completed);
todo.completed = !todo.completed
console.log("after the opt "+todo.completed);
}
//console.log(todo);
return todo;
})
console.log(updatedTodo);
return {
todos: updatedTodo
}
});
}
render() {
const todoDataComponents = this.state.todos.map(item => {
return <TodoList key = {item.id} item = {item} handleChange = {this.handleChange} />
})
return (
<div className="todo-list">{todoDataComponents}</div>
);
}
}
export default App;
todoList.jsx
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
// console.log(this.props)
return (
<div className="todo-item">
<input
type="checkbox"
checked={this.props.item.completed}
onChange = {() => this.props.handleChange(this.props.item.id)}
/>
<p>{this.props.item.text}</p>
</div>
);
}
}
export default TodoList;
todoData.js
const TodosData = [
{ id: 1, text: "Coding", completed: true },
{ id: 2, text: "Exercise", completed: false },
{ id: 3, text: "Learning", completed: true },
{ id: 4, text: "Programming", completed: true },
{ id: 5, text: "inspire", completed: false },
{ id: 6, text: "motivation", completed: true }
];
export default TodosData;
I can't see any error in your handleChange() method, it should work fine. However, I've just updated some of your code which you can test below.
I changed the name of your TodoList as it's not really a list but an item. I also changed it to a functional component as it's only presentational, there is no need to have its own state. Instead of adding a p tag after the input, you should use a label to make it accessible.
I haven't really changed anything inside your handleChange() method, only removed the console.logs and it works as expected.
Update: You're using React.StrictMode, where React renders everything twice on dev. As your handleChange() runs twice, it sets the clicked todo's completed state twice, making it to set back to its original state. So if it's false on first render it sets to true on click, but it's rendered again and on the second one it's back to false. You won't notice it as it's pretty fast.
To avoid it, you need to avoid mutating anything. So I've updated your onChange handler, it returns a new object if the completed property is changed.
Feel free to run the code snippet below and click on the checkboxes or the item texts.
const TodosData = [
{ id: 1, text: 'Coding', completed: true },
{ id: 2, text: 'Exercise', completed: false },
{ id: 3, text: 'Learning', completed: true },
{ id: 4, text: 'Programming', completed: true },
{ id: 5, text: 'Inspire', completed: false },
{ id: 6, text: 'Motivation', completed: true },
];
function TodoItem(props) {
const { handleChange, item } = props;
return (
<div className="todo-item">
<input
type="checkbox"
id={`item-${item.id}`}
checked={item.completed}
onChange={() => handleChange(item.id)}
/>
<label htmlFor={`item-${item.id}`}>{item.text}</label>
</div>
);
}
class App extends React.Component {
constructor() {
super();
this.state = {
todos: TodosData,
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(id) {
this.setState((prevState) => {
const updatedTodo = prevState.todos.map((todo) => {
return todo.id === id ? {
...todo,
completed: !todo.completed,
} : todo;
});
return {
todos: updatedTodo,
};
});
}
render() {
const { todos } = this.state;
return (
<div className="todo-list">
{todos.map((item) => (
<TodoItem
key={item.id}
item={item}
handleChange={this.handleChange}
/>
))}
</div>
);
}
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
body {
font-family: sans-serif;
line-height: 1.6;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Instead of using an additional variable, you can do it in one line. Can u see if this works for you.
handleChange(id) {
this.setState(prevState => prevState.map(todo => todo.id === id ? {...todo, completed: !todo.completed} : todo )
}
My React App doesn't work like it should be. The problem is that the checkboxes dont change at all.
I managed to show the checked boxes (the ones with the property of completed=true) and debugging it seems that it works fine when I click but for some reason the box that needs to be changed automatically re-changes on its own.
Do you have any idea why ?
//APP.JS
import React from "react"
import './App.css';
import Header from "./Header"
import TodoItem from "./todoItem";
import todosData from "./todosData"
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState((prevState) => {
const newArray = prevState.todos.map((elem) => {
if(elem.id === id) {
elem.completed = !(elem.completed)
}
return elem
})
return {
todos: newArray
}
})
}
render() {
const todosArray = this.state.todos.map(item =>
<TodoItem
key={item.id}
item={item}
handleChange={this.handleChange}
/>)
return (
<div className="App">
<Header />
<div className="container">
{todosArray}
</div>
</div>
)
}
}
export default App;
//TODOITEM.JS
import React from "react"
function TodoItem(props) {
return (
<div className="elem-container">
<input type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<span className="span-container">{props.item.text}</span>
</div>
)
}
export default TodoItem
//TODOSDATA.JS
const todosData = [
{
id: 1,
text: "Take out the trash",
completed: true
},
{
id: 2,
text: "Grocery shopping",
completed: false
},
{
id: 3,
text: "Clean gecko tank",
completed: false
},
{
id: 4,
text: "Mow lawn",
completed: true
},
{
id: 5,
text: "Catch up on Arrested Development",
completed: false
}
]
export default todosData
Thank you for the help in advance
you need to change two things and it will work just fine
first:
inside todoItem.js
onChange={(e) => props.handleChange(e,props.item.id)}
second:
inside the parent file
handleChange(event, id) {
this.setState((prevState) => {
const newArray = prevState.todos.map((elem) => {
if(elem.id === id) {
elem.completed = event.target.checked
}
return elem
})
return {
todos: newArray
}
})
}
now everything will work as you expected
have a nice day
I'm no expert as I'm learning React myself but looking at the code handleChange(id) doesn't have an else state in its 'if' statement, have you tried adding?
Adding to #mouheb answer, you can simplify one more step. you don't need to map the all elements to update single item. you can change directly item (if it is mutable).
// todoItem.js
onChange={(e) => props.handleChange(e, props.item) }
// parent file
handleChange(event, prop) {
prop.completed = event.target.
this.setState({ todos: this.state.todos }) or this.setState({ todos: [...this.state.todos] })
}
I'm working under the to-do list project and have got a problem with adding a comment to my list item. This is what I have right now:
App flow
After adding a list item you should be able to click in this item and a new window with comments will appear. In the comments section, comments should be added with Ctrl+Enter combination.
I've got a problem with adding comments to the list item (they should be added to the "comments" array of a particular list item).
Could you please explain what I'm doing wrong and why my comments aren't adding.
UPDATE: I've updated my code but the following mistake appears when I press Ctr+Enter to add a comment: [Error] (http://joxi.ru/Q2KR1G3U4lZJYm)
I've tried to bind the addItem method but no result. What's wrong with the addItem method?
Here is my main component:
App.js
import React, { Component } from 'react';
import './App.css';
import ListInput from './components/listInput'
import ListItem from './components/listItem'
import SideBar from './components/sideBar'
import CommentsSection from './components/commentsSection'
class App extends Component {
constructor(props){
super(props);
this.state = {
items: [
{
id: 0,
text: 'First item',
commentsCount: 0,
comments: [],
displayComment: false
},
{
id: 1,
text: 'Second item',
commentsCount: 0,
comments: [],
displayComment: false
},
{
id: 2,
text: 'Third item',
commentsCount: 0,
comments: [
'Very first comment',
'Second comment',
],
displayComment: false
},
],
nextId: 3,
activeComment: [],
}
}
// Add new item to the list
addItem = inputText => {
let itemsCopy = this.state.items.slice();
itemsCopy.push({id: this.state.nextId, text: inputText});
this.setState({
items: itemsCopy,
nextId: this.state.nextId + 1
})
}
// Remove the item from the list: check if the clicked button id is match
removeItem = id =>
this.setState({
items: this.state.items.filter((item, index) => item.id !== id)
})
setActiveComment = (id) => this.setState({ activeComment: this.state.items[id] });
addComment = (inputComment, activeCommentId ) => {
// find item with id passed and select its comments array
let commentCopy = this.state.items.find(item => item.id === activeCommentId)['comments']
commentCopy.push({comments: inputComment})
this.setState({
comments: commentCopy
})
}
render() {
return (
<div className='App'>
<SideBar />
<div className='flex-container'>
<div className='list-wrapper'>
<h1>Items</h1>
<ListInput inputText='' addItem={this.addItem}/>
<ul>
{
this.state.items.map((item) => {
return <ListItem item={item} key={item.id} id={item.id} removeItem={this.removeItem} setActiveComment={() => this.setActiveComment(item.id)}/>
})
}
</ul>
</div>
<CommentsSection inputComment='' items={this.state.activeComment}/>
</div>
</div>
);
}
}
export default App;
and my Comments Section component:
commentsSection.js
import React from 'react';
import './commentsSection.css';
import CommentInput from './commentInput'
import CommentsItem from './commentsItem'
export default class CommentsSection extends React.Component {
constructor(props){
super(props);
this.state = {value: this.props.inputComment};
this.handleChange = this.handleChange.bind(this);
this.handleEnter = this.handleEnter.bind(this);
this.addComment = this.addComment.bind(this)
}
handleChange = event => this.setState({value: event.target.value})
handleEnter(event) {
if (event.charCode === 13 && event.ctrlKey) {
this.addComment(this.state.value)
}
}
addComment(comment) {
// Ensure the todo text isn't empty
if (comment.length > 0) {
this.props.addComment(comment, this.props.activeComment);
this.setState({value: ''});
}
}
render() {
return (
<div className='component-section'>
<h1>{this.props.items.text}</h1>
<ul>
{ this.props.items.comments &&
this.props.items.comments.map((comment, index) => <p key={index}>{comment}</p>)
}
</ul>
<CommentsItem />
{/*<CommentInput />*/}
<div className='comment-input'>
<input type='text' value={this.state.value} onChange={this.handleChange} onKeyPress={this.handleEnter}/>
</div>
</div>
)
}
}
Change your CommentSection component addComment method and handleEnter method
addComment(comment) {
// Ensure the todo text isn't empty
if (comment.length > 0) {
// pass another argument to this.props.addComment
// looking at your code pass "this.props.items"
this.props.addComment(comment, this.props.items.id);
this.setState({value: ''});
}
}
handleEnter(event) {
if (event.charCode === 13 && event.ctrlKey) {
// passing component state value property as new comment
this.addComment(this.state.value)
}
}
Change your App Component addComment method
addComment = (inputComment, activeCommentId )=> {
// find item with id passed and select its comments array
let commentCopy = this.state.items.find(item => item.id === activeCommentId)['comments']
// if you want to push comments as object
// but looking at your code this is not you want
// commentCopy.push({comments: inputComment})
// if you want to push comments as string
commentCopy.push( inputComment)
this.setState({
comments: commentCopy
})
}
I have some issue in React that seems to keep last or old state.
I have a parent component called MyLists.js that contain a loop function where I rendered child component called Item.js
{
this.state.listProducts.map(d =>
<Item data={d} />
)}
And in my Item.js component I set state in constructor :
this.state = { showFullDescription: false }
The variable "showFullDescription" allows me to see the entire description of a product. Now I have for example 2 products and all states "showFullDescription" are set to false so :
Product 1 => (showFullDescription = false)
Product 2 => (showFullDescription = false)
Next, I show full description for Product 2 by clicking a button and I set state to true so Product 2 => (showFullDescription = true)
The problem is when I add another product, let's call it "Product 3", the full description of "Product 3" is directly shown and for "Product 2" it is hidden. It seems that last state is reflected on "Product 3".
I am really sorry for my english, it's not my native language
Here is full source code :
MyLists.js
import React, { Component } from 'react';
import ProductService from '../../../../services/ProductService';
import Item from './Item';
class MyLists extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.productService = new ProductService();
this.productService.getAllProducts().then((res) => {
this.setState({
products: res
})
});
}
addProduct(data){
this.productService.addProduct(data).then((res) => {
var arr = this.state.products;
arr.push(res);
this.setState({
products: arr
})
})
}
render() {
return (
<div>
{
this.state.products.map(d =>
<Item data={d} />
)}
</div>
)
}
}
export default MyLists;
Item.js
import React, { Component } from 'react';
import Truncate from 'react-truncate';
class Item extends Component {
constructor(props) {
super(props);
this.state = {
showFullDescription: false
}
}
render() {
return (
<div>
<h2>{this.props.data.title}</h2>
{
!this.state.showFullDescription &&
<Truncate lines={10} ellipsis={<a className="btn btn-primary read-more-btn" onClick={() => this.setState({showFullDescription: true})}>Show More</a>}>
{this.props.data.description}
</Truncate>
)}
{
this.state.showFullDescription &&
<span>
{this.props.data.description}
</span>
}
</div>
)
}
}
export default Item;
You have some syntax problems and missing && for !this.state.showFullDescription.
I've slightly changed the component and use ternary operator to render conditionally. It is a little bit ugly right now, the logic can be written outside of the render. Also, I suggest you to use a linter if you are not using.
MyLists.js
class MyLists extends React.Component {
state = {
products: [
{ id: 1, description: "Foooooooooooooooooooooooooooooooooooooooooo", title: "first" },
{ id: 2, description: "Baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarrrrrrrrrrr", title: "second" },
{ id: 3, description: "Baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz", title: "third" },
]
}
render() {
return (
<div>
{
this.state.products.map(d =>
<Item data={d} />
)}
</div>
)
}
}
Item.js
class Item extends React.Component {
state = {
showFullDescription: false,
}
render() {
return (
<div>
<h2>{this.props.data.title}</h2>
{ !this.state.showFullDescription
?
<Truncate lines={3} ellipsis={<span>... <button onClick={() => this.setState({showFullDescription: true})}>Read More</button></span>}>
{this.props.data.description}
</Truncate>
:
<span>
{this.props.data.description}
</span>
}
</div>
)
}
}
Here is working sandbox: https://codesandbox.io/s/x24r7k3r9p
You should try mapping in the second component as:
class Item extends React.Component {
state = {
showFullDescription: false,
}
render() {
return (
<div>
{this.props..data.map(data=>
<h2>{this.props.data.title}</h2>
{ !this.state.showFullDescription
?
<Truncate lines={3} ellipsis={<span>... <button onClick={() =>
this.setState({showFullDescription: true})}>Read More</button>
</span>}>
{this.props.data.description}
</Truncate>
:
<span>
{this.props.data.description}
</span>)}
}
</div>
)
}
}
You should have a 'key' property (with unique value) in 'Item' - No warnings about it in console?