Update state by changing an entire object within state array - reactjs

I'm trying to create rows of inputs that updates the state of my application. The app is currently only two components, TimeCalc and ItemsList. State is stored in TimeCalc, and ItemsList handles the conditional render (either show state, or show rows of input.
I have managed to fetch the edited and updated object, but I'm struggling to replace the updated object with the correct object in state. The objects contain the props _id, title, start and end, and preferably I'd like to search for the matching _id, and replace the entire object in the state. But seeing as the _id prop is a sibling prop of the other props, I'm not sure how to do this.
Here is TimeCalc:
import React from 'react';
import ItemsList from '../components/ItemsList';
const items = [
{
_id: '112233',
title: 'M1',
start: 900,
end: 1800
},
{
_id: '223344',
title: 'M2',
start: 1000,
end: 1900
}
];
export default class TimeCalc extends React.Component {
state = {
items
}
handleUpdate = (update) => {
}
render = () => {
return (
<div class="timeCalc flex center">
<ItemsList items={this.state.items} handleUpdate={this.handleUpdate}/>
</div>
)
}
}
And here is ItemsList:
import React from 'react';
export default class ItemsList extends React.Component {
state = {
editing: null
}
toggleEditing = (itemId) => {
this.setState({
editing: itemId
})
}
handleEditItem = () => {
let itemId = this.state.editing;
this.handleItemsUpdate({
_id: itemId,
title: this.refs[`title_${itemId}`].value,
start: this.refs[`start_${itemId}`].value,
end: this.refs[`end_${itemId}`].value,
})
}
handleItemsUpdate = (update) => {
console.log(update);
this.props.handleUpdate(update);
this.setState( { editing: null } );
}
renderItemOrEditField = (item) => {
if(this.state.editing === item._id) {
return <li key={`editing-${item._id} `} class="list-item flex row">
<input
onKeyDown={this.handleEditField}
type="text"
class="form-input"
ref={`title_${item._id}`}
name="title"
defaultValue={item.title}
/>
<input
onKeyDown={this.handleEditField}
type="text"
class="form-input"
ref={`start_${item._id}`}
name="start"
defaultValue={item.start}
/>
<input
onKeyDown={this.handleEditField}
type="text"
class="form-input"
ref={`end_${item._id}`}
name="end"
defaultValue={item.end}
/>
<button onClick={this.handleEditItem} label="Update Item"/>
</li>
} else {
return <li
onClick = {this.toggleEditing.bind(null, item._id)}
key = {item._id}
class = "list-position">
{` ${item.title} & ${item.start} && ${item.end} && ${item._id}`}
</li>
}
}
render = () => {
return (
<ul class="itemsList">
{this.props.items.map((item) => {
return this.renderItemOrEditField(item);
})}
</ul>
)
}
}
I'm trying to recreate MeteorChef's "Click to Edit fields in React", but he's storing the state in a Meteor way.

I'd suggest that you move all of the state to the parent component. Keeping ItemsList stateless separates your concerns more neatly and makes the code much simpler to reason about. In this scenario, all ItemsList needs to be concerned with is presenting data based on the state stored in the parent. In your TimeCalc component, add an "editing" key to the state object, and add a method to handle updates from ItemsList:
state = {
items,
editing: null,
}
handleUpdate = (editing) => {
this.setState({ editing });
}
Then when rendering the child component, pass this handler function, the items array, and the "editing" key as props:
<ItemsList items={this.state.items} editing={this.state.editing} handleUpdate={this.handleUpdate}/>
Now ItemsList can become a stateless functional component, like this:
import React from 'react';
const renderItemOrEditField = (item, editing, handleUpdate) => (
{editing === item._id ? (
<li key={item._id} className="list-item flex row">
<input onKeyDown={handleUpdate(item._id}
// The rest of your content to render if item is being edited
) : (
// content to be rendered if item isn't being edited goes here
)}
);
export default const ItemsList = (props) => (
<ul className="itemsList">
{props.items.map((item) => {
return renderItemOrEditField(item, props.editing, props.handleUpdate);
})}
</ul>
);
This strategy of using "smart" container components that manage state and business logic and "dumb" presentational components can go a long way towards keeping your application more maintainable and less fragile. Hope this helps!

Related

Passing data from child to parent component in React

Edited to add solution at bottom
I have a project created with React and Typescript.
There is a parent component (Home) that displays a child component depending on the value of the state variable 'currentDemo'. The goal is to have a navigation component that will display whatever item was clicked. Each nav item has an id associated that relates to the component to be displayed. Ie, nav item 'a' should display component 'a', nav item 'b' should show component 'b', etc. Here is a snippet of the code.
Home.tsx (Parent):
import React, { useState } from 'react';
import { Intro } from 'app/components/intro/Intro';
import { SidebarNav } from 'app/components/sidebarNav/SidebarNav';
import { ComponentA } from 'app/components/ComponentA/ComponentA';
import { ComponentB } from 'app/components/ComponentB/ComponentB';
export function Home() {
//use state to track which demo is currently displayed ('intro' is default)
const [currentDemo, setCurrentDemo] = useState('intro');
return (
<>
<Header />
<div className="home">
<SidebarNav setCurrentDemo={setCurrentDemo} />
{currentDemo === 'intro' && <Intro />}
{currentDemo === 'ComponentA' && <ComponentA/>}
{currentDemo === 'ComponentB' && <ComponentB/>}
</div>
</>
);
}
SidebarNav.tsx(child):
import React, { useState } from 'react';
const navData = [
{
title: 'Introduction',
id: 'intro'
},
{
title: 'Component A',
id: 'ComponentA'
},
{
title: 'Component B',
id: 'ComponentB'
}
];
export function SidebarNav(setCurrentDemo: any) {
//GOAL: PASS ID OF SELECTED NAV ITEM TO PARENT COMPONENT AND SET VALUE OF 'CURRENTDEMO' TO THAT ID
const handleCurrentClick = (id: any) => {
if (id === 'intro') {
setCurrentDemo('ComponentA');
} else if (id === 'ComponentA') {
setCurrentDemo('ComponentB');
} else if (id === 'ComponentB') {
setCurrentDemo('intro');
}
};
return (
<div className="sidebarNav">
<div className="sidebarNav__container">
{navData?.map((item, index) => (
<div key={index}>
<button
onClick={() => {
handleCurrentClick(item.id);
}}
id={item.id}
>
{item.title}
</button>
</div>
))}
</div>
</div>
);
}
The specific implementation of Component A and B don't matter for this scenario. I've tested by manually setting the value of 'currentDemo' and the correct demo will display. I also confirmed that the id's for each nav item are correctly displaying via console.log(item.id).
How can I pass the pass the value of the id from SidebarNav to Home, setting the value of currentDemo to the ID of the nav item that was clicked? I feel like I'm close, but it's not quite right.
When clicking any of the nav elements there is a console error stating that setCurrentDemo is not a function. Which makes sense because it's the setter for the state, but how can I specify that we want to actually set currentDemo to the value of the item's ID?
Here is the solution that worked for this application. Changes made are in the navigation component. Added an interface in the nav and adjusted as such:
interface SidebarNavProps {
setCurrentDemo: React.Dispatch<SetStateAction<string>>;
}
export function SidebarNav(props: SidebarNavProps) {
const { setCurrentDemo } = props;
...rest of function remains the same
};
Each component receives props as an object. In SidebarNav component, props will look like this { setCurrentDemo } :any not setCurrentDemo:any.
Here's the interface for SidebarNav Component
import { SetStateAction } from "react";
interface SidebarNavProps {
setCurrentDemo: SetStateAction<string>
}
And your SidebarNav component will look like this:
export function SidebarNav(props: SidebarNavProps) {
const { setCurrentDemo } = props;
//GOAL: PASS ID OF SELECTED NAV ITEM TO PARENT COMPONENT AND SET VALUE OF 'CURRENTDEMO' TO THAT ID
const handleCurrentClick = (id: any) => {
if (id === 'intro') {
setCurrentDemo('ComponentA');
} else if (id === 'ComponentA') {
setCurrentDemo('ComponentB');
} else if (id === 'ComponentB') {
setCurrentDemo('intro');
}
};
return (
<div className="sidebarNav">
<div className="sidebarNav__container">
{navData?.map((item, index) => (
<div key={index}>
<button
onClick={() => {
handleCurrentClick(item.id);
}}
id={item.id}
>
{item.title}
</button>
</div>
))}
</div>
</div>
);
}
This will fix that error and you can store the id in state using setCurrentDemo.
you can identify state in parent and set this state on child component to pass data from child to parent

Modify class name of react element programmatically

Is there a native way to add a style class name to a react element passed as a property WITHOUT using jQuery or any 3rd-party libraries.
The following example should demonstrate what I'm trying to do. Note, react class names are made up.
Edit: The point is to modify the class name of a react element that is passes as a property to the Books class! The Books class needs to modify the class name. Apparently, it does not have access to Authors class's state to use within Authors class.
File authors.js
class Authors {
render() {
return (
<ul>
<li>John Doe</li>
<li>Jane Doe</li>
</ul>
);
}
}
File shelf.js
class Shelf {
render() {
return (
<div>
<Books authors={<Authors/>}/>
</div>
);
}
}
File books.js
class Books {
this.props.authors.addClass('style-class-name'); <- HERE
render() {
return (
<div>
{this.props.authors}
</div>
);
}
}
Potentially need more context, but in this kind of scenario, I would use state to dynamically add/remove a class. A basic example would be:
const App = () => {
const [dynamicClass, setDynamicClass] = useState("");
return (
<div className={`normalClass ${dynamicClass}`}>
<button onClick={() => setDynamicClass("red")}>Red</button>
<button onClick={() => setDynamicClass("green")}>Green</button>
<button onClick={() => setDynamicClass("")}>Reset</button>
</div>
);
};
The state changes schedule a re-render, hence you end up with dynamic classes depending on the state. Could also pass the class in as a property, or however you want to inject it into the component.
React elements do have an attribute called className. You can use that to set CSS classes to your component. You can pass static data (strings) or dynamic ones (basically calculated ones):
<div className="fixedValue" />
<div className={fromThisVariable} />
Keep in mind, that you have to pass down your className, if you wrap native HTML elements in a component:
class Books {
render() {
const {
authors,
// other represents all attributes, that are not 'authors'
...other
}
return (
<div {...other}>
{this.props.authors}
</div>
);
}
}
If you want to add data to your authors attribute (which I assume is an array), you could implement a thing like the following:
let configuredAuthors = this.props.authors.map((author) => ({
return {
...author,
className: `${author.firstName}-${author.lastName}`
}
}))
Keep in mind, that either way, you have to manually assign this className property in your child components (I guess an Author component)
To handle updates from a child component, use functions: https://reactjs.org/docs/faq-functions.html
Full example:
import React from "react";
class Shelf extends React.Component {
render() {
const authors = [
{
firstName: "John",
lastName: "Tolkien"
},
{
firstName: "Stephen",
lastName: "King"
}
];
return (
<div>
<Books authors={authors} />
</div>
);
}
}
const Books = ({authors, ...other}) => {
const [configuredAuthors, setAuthors] = React.useState(authors)
const updateClassName = (authorIndex, newClassName) => {
const newAuthors = [...configuredAuthors]
newAuthors[authorIndex] = {
...configuredAuthors[authorIndex],
className: newClassName
}
setAuthors(newAuthors)
}
return (
<ul {...other}>
{configuredAuthors.map((author, index) => {
return <Author key={index} author={author}
index={index}
updateClassName={updateClassName}
/>;
})}
</ul>
);
}
const Author = ({ author, index, updateClassName, ...other }) => {
const [count, setCount] = React.useState(0);
return (
<li className={author.className} {...other}>
<span>{`${author.firstName} ${author.lastName}`}</span>
<button
onClick={() => {
updateClassName(index, `author-${count}`);
setCount(count + 1);
}}
>
update Class ()
{`current: ${author.className || '<none>'}`}
</button>
</li>
);
};
export default function App() {
return <Shelf />;
}

How do I render two inputs into one list in Reactjs

I'm trying to teach myself how to code and created a little todo app. In the rendering of each todo input I have the element and then a checkbox to click for it to be removed. I tried to create a separate input to give the amount of time it will take for each item to be created. When I tried to link that up to my rendering method, nothing renders and I have zero error messages.
import React from 'react';
class InputBar extends React.Component {
state={ todo: '',
time: null
}
onInputSubmit = e =>{
e.preventDefault();
this.props.todoSubmit(this.state.todo)
this.props.timeSubmit(this.state.time)
this.setState({
todo: '',
time: this.state.time
})
}
render() {
return (
<div className="input-group mb-3">
<form onSubmit={ this.onInputSubmit } >
<label>Input Todo</label>
<div className='input-control'>
<input
type='text'
className="form-control"
aria-label="Sizing example input"
aria-describedby="inputGroup-sizing-default"
value={this.state.todo}
onChange={e => this.setState({
todo: e.target.value
})}
/>
<input
type='number'
required
className='input-control'
defaultValue={0}
value={this.state.time}
placeholder='How long will it take?'
onChange={e => this.setState({
time: e.target.value
})} />
</div>
</form>
</div>
)
}
}; export default InputBar
import React from 'react';
import InputBar from './inputbar';
class List extends React.Component {
state = {
list: [],
nextId: 1
};
componentDidMount() {
const list = JSON.parse( localStorage.getItem( "list" ) );
this.setState( { list } );
}
addToList = (todo, time, list) => {
this.setState({
list: [
{
name: todo,
text: time,
id: this.state.nextId
},
...this.state.list,
],
nextId: this.state.nextId + 1
},
() => {
localStorage.setItem("list", JSON.stringify(this.state.list));
});
}
removeFromList = (id) => {
this.setState({
list: this.state.list.filter(entry => entry.id !== id )
},
() => {
localStorage.setItem("list", JSON.stringify(this.state.list));
}
);
}
renderList = () => {
return this.state.list.map((element) => {
return (
<div>
<li>
{element.name}
<input
style={{marginLeft: '15px'}}
type='checkbox'
onClick={()=> this.removeFromList(element.id)}
/>
</li>
</div>
)
})
}
render() {
console.log(this.state.todo, this.state.time)
return (
<div>
<InputBar
todoSubmit={this.addToList}
timeSubmit={this.addToList}
/>
<ul>
{ this.renderList() }
</ul>
</div>
)
}
};
export default List;
//this is then send to imported an app component to be rendered
Hi & welcome to Stack Overflow, Elias.
You pass two handlers to your InputBar component that both resolve to the addToList handler defined in your list component. However, when you call these handlers, the arguments do not match what addToList is expecting, which is a list, a todo and a time.
You obviously don't need a list argument (it's never used in addToList as you manage the list present in that component state's anyway, which is fine), so list can be removed.
And in my opinion, you do not need 2 handlers (one for the todo, one for the time value). One handler that adds both todo AND time would be better (after all, the idea is to submit a todo as a whole object) and would line up with what addToList would expect.
In summary, here are the changes I suggest:
In inputbar.js:
onInputSubmit = e => {
e.preventDefault();
const { todo, time } = this.state
this.props.handleSubmit(todo, time)
this.setState({
todo: '',
time: this.state.time
})
}
In your List component:
addToList = (todo, time) => {
// just removed the unnecessary 'list' param
// actual code left untouched
}
// other code
render() {
console.log(this.state.todo, this.state.time)
return (
<div>
<InputBar handleSubmit={this.addToList} />
<ul>
{ this.renderList() }
</ul>
</div>
)
}

State not updating in Component

Hey I am trying to create a simple to-do list and I have added the components necessary. However, the state is not being updated in the Title {this.state.data.length} and the TodoList {this.state.data}. A Codepen and the relevant code is below.
https://codepen.io/skasliwal12/pen/BREYXK
const TodoForm = ({addTodo}) => {
let input;
return (
<div>
<input ref={node => {input = node;}} />
<button onClick={(e) => {
e.preventDefault();
addTodo(input.value);
input.value='';
}}> +
</button>
</div>
);
};
const TodoList = ({todos}) => {
let todoNodes = todos.map(todo => {
return <li>{todo}</li>
});
return <div> {todoNodes} </div>;
}
const Title = ({todoCount}) => {
return (
<div>
<div>
<h1>To-do App {todoCount} items</h1>
</div>
</div>
);
}
class TestApp extends React.Component {
constructor(props) {
super(props);
this.state = { data : [] }
}
addTodo(val) {
let todo = {text: val}
this.state.data.push(todo);
this.setState = ({data: this.state.data});
console.log('state updated?')
}
render(){
return (
<div>
<Title todoCount={this.state.data.length}/>
<TodoForm addTodo={this.addTodo.bind(this)}/>
<TodoList todos={this.state.data}/>
</div>
);
}
}
ReactDOM.render(<TestApp />, document.getElementById('root'));
Quite simply it is important that you DO NOT MUTATE the state like you are doing here
this.state.data.push(todo);
It is hard to debug and adds side effects that are hard to keep track of. Following your approach you should copy the state to a var, update that var and then pass it as the new field in your state. Which could work but it's also something I do not recommend. A general good approach is to to compute the new state based on the old one
// this.state.data.push(todo); You can remove this line
this.setState(prevState => ({ data: prevState.data.concat(todo) }))
This will fix your issue and avoid mutating the state, which is something you should never do, only update the state using the setState method.
I also updated your TodoList which was not displaying properly, you have to access the text field of the todo in order to show something.
const TodoList = ({todos}) => {
let todoNodes = todos.map(todo => {
return <li>{todo.text}</li>
});
return <div> {todoNodes} </div>;
}
https://codepen.io/anon/pen/MmRVmX?editors=1010

React checkbox in stateless component is not updating immediately

I created a basic interface using checkboxes that used a react design pattern that has served me well before and that I thought worked well - namely lifting up state and passing down props to UI components. My checkbox components are passed a value(a metric), an state-changing method, and a boolean for checked. The problem is that the checkboxes do not update immediately, even though you can see them updating in the React dev tools. They only update on the next click, as in when another checkbox is checked. Here is the code:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
metricsSelected: []
}
this.selectMetric = this.selectMetric.bind(this)
}
selectMetric(metric) {
const metricsSelected = this.state.metricsSelected
const index = metricsSelected.indexOf(metric)
if (index !== -1){
metricsSelected.splice(index, 1)
}
else {
metricsSelected.push(metric)
}
this.setState({
metricsSelected,
});
}
render() {
return (
<div>
<Sidebar
metricsSelected={this.state.metricsSelected}
selectMetric={this.selectMetric}/>
<SomethingElse/>
</div>
)
}
}
const SomethingElse = () => (<div><h2>Something Else </h2></div>)
const Sidebar = ({ metricsSelected, selectMetric }) => {
const metrics = ['first thing', 'second thing', 'third thing']
return (
<div>
<h3>Select Metrics</h3>
{ metrics.map( (metric, i) =>
<Checkbox
key={i}
metric={metric}
selectMetric={selectMetric}
checked={metricsSelected.includes(metric)}/>
)}
</div>
)
}
const Checkbox = ({ metric, selectMetric, checked }) => {
const onChange = e => {
e.preventDefault()
selectMetric(e.target.value)
}
return (
<ul>
<li>{metric}</li>
<li><input
type='checkbox'
value={metric}
checked={checked}
onChange={onChange} /></li>
</ul>
)
}
I've read pretty much everything I can get my hands on about checkboxes for react and most of the applications of the checkbox are doing something different from what I want to do. I've tried adding state to the Checkbox component, but that didn't seem to help, since the checked value still needs to come in from elsewhere. I thought react components rerendered when the props changed. What gives?
Here's a codepen: https://codepen.io/matsad/pen/QpexdM
Here is a working version: http://codepen.io/TLadd/pen/oWvOad?editors=1111
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
metricsSelected: {}
}
this.selectMetric = this.selectMetric.bind(this)
}
selectMetric(metric) {
this.setState(({ metricsSelected }) => ({
metricsSelected: {
...metricsSelected,
[metric]: !metricsSelected[metric]
}
}))
}
render() {
return (
<div>
<Sidebar
metricsSelected={this.state.metricsSelected}
selectMetric={this.selectMetric}/>
<SomethingElse/>
</div>
)
}
}
const SomethingElse = () => (<div><h2>Something Else </h2></div>)
const Sidebar = ({ metricsSelected, selectMetric }) => {
const metrics = ['first thing', 'second thing', 'third thing']
return (
<div>
<h3>Select Metrics</h3>
{ metrics.map( (metric, i) =>
<Checkbox
key={i}
metric={metric}
selectMetric={selectMetric}
checked={Boolean(metricsSelected[metric])}/>
)}
</div>
)
}
const Checkbox = ({ metric, selectMetric, checked }) => {
return (
<ul>
<li>{metric}</li>
<li>
<input
type='checkbox'
name={metric}
checked={checked}
onChange={() => selectMetric(metric)}
/>
</li>
</ul>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
The couple of things that were causing issues were that you were mutating state in selectMetric, and your checkbox input's onChange function is using e.target.value instead of e.target.checked.
I changed the metricsSelected state to be an object, since I think it makes the management of it quite a bit easier.

Resources