I have two components. One components renders a "title" input.
The other component renders a "note" input with 2 buttons.
I have the title input values stored in state called "title"
I have the note input value stored in state called "note"
Now i'm trying to get my title and note values in an object like so:
const [completedNote, setCompletedNote] = useState([{ id=1, title: "", note=""}])
//App component
import React, { useState } from "react"
import NoteTitle from "./components/note-title/NoteTitle";
export default function App() {
const [title, setTitle] = useState("");
const [note, setNote] = useState("");
const [completedNote, setCompletedNote] = useState([
{ id: 1, title: "", note: "" },
]);
return (
<NoteTitle
title={title}
setTitle={setTitle}
note={note}
setNote={setNote}
/>
);
}
//Title Component
import React, { useState } from "react";
import Note from "../note/Note";
export default function NoteTitle({ title, setTitle, note, setNote }) {
return (
<>
<div className="note-maker__maincontainer">
<div className="note-maker__sub-container">
<div className="note-maker__input-container" ref={wrapperRef}>
<div className="note-maker__title">
<input
id="input_title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title..."
onClick={() => setIsNoteDisplayed(true)}
/>
</div>
<Note note={note} setNote={setNote} />
</div>
</div>
</div>
</>
);
}
// Note Component
import React from "react";
export default function Note({ note, setNote }) {
return (
<>
<div className="note__container">
<div className="note-maker__note">
<input
id="input_note"
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Take a note..."
/>
</div>
<div className="note-maker__buttons-container">
<button className="note-maker__submit-button" type="submit">
Submit
</button>
<button className="note-maker__close-button">Close</button>
</div>
</div>
</>
);
}
How would I go about doing this? I have tried this but its causing "error: To many renders"
setCompletedNote((prevState) =>({
title:{
...prevState.title,
[title]: title,
note:{
...prevState.note,
[note]: note
}
}
}))
Thanks in advance!
If you just want to add a new Completed Note then
Note: Use some library like uuid to generate id and don't do it like below :)
// You have to initiate just an empty array
const [completedNote, setCompletedNote] = useState([]);
// Call this function on submit
const addCompletedNote = () => {
// TODO: validate note and title are not empty
// Add new object to state
setCompletedNote((prevState) => [
...prevState,
{ id: Date.now(), note: note, title: title }
]);
// Clean up existing state
setTitle("");
setNote("");
// Note: this above clean-up triggers state updates 2 times which is not that good but works :) .
// TODO: so try to solve it by combining title, note and completedNote to be a single state object
// like this => { title: "", note: "", completedNote: [] }
// This above change requires a lot of extra changes to work (Try figuring them out :))
}
If you want to update title and note of an existing Completed Note, you need id, newTitle, and newNote values. You update the value of the object that matches the input id.
const updateCompletedNote = (id, newTitle, newNote) => {
setCompletedNote((prevState) => prevState.map(n) => {
if (n.id === id) { // match the id here
return {...n, title: newTitle, note: newNote}; // return new object
}
return n; // objects that do not match id are returned as it is
});
}
You can also update just note or just title But you always need id of the object.
Lets say if you want to just update title of the object you need both id and newTitle and return
return {...n, title: newTitle };
instead of
return {...n, title: newTitle, note: newNote};
Your setCompletedNote function is missing the last closing parenthesis )
You should pass in the arrow function enclosed in curly braces to prevent an infinite loop:
setCompletedNote({
(prevState) => ({
title: {
...prevState.title,
[title]: title,
note: {
...prevState.note,
[note]: note
}
}
})
})
Related
I'm working on a todo app and I have added the functionality to add a task. I am having trouble clearing out the input box and be ready for the next input.
Currently, you can add a todo, it clears the input box, I add another todo, it gets added but the text is missing.
const handleOnClick = (e) => {
e.preventDefault();
console.log("ref.current.value - ", ref.current.value);
tasks.addTasks((prev) => [
...prev,
{
id: uuidv4(),
todo: ref.current.value,
done: false,
},
]);
ref.current.value = ""; // clears it out but cant anything new in
};
In the console log, I can see the text for each todo but it is not getting entered into the array. using useState for the object and merging it with the previous.
Link to code sandbox: https://codesandbox.io/s/cold-darkness-v80pwr?file=/src/Components/AddItem.js
This happens because you are using the ref and changing the value of the element, but you dont have an onChange function that handles it's value, and using the ref in this case just to clear out the value and using it to create a task it's a wrong usage, and you should use a simple useState and set the onChange and value of the input.
Here is the edited sandbox - https://codesandbox.io/s/condescending-bush-gkvs93?file=/src/Components/AddItem.js
The function inside tasks.addTasks(...) called after ref.current.value = "". So you got an empty todo.
You don't need refs in this case. Here is working example:
https://codesandbox.io/s/nameless-frog-sqtldd?file=/src/Components/AddItem.js
import { v4 as uuidv4 } from "uuid";
import React, { useState } from "react";
const AddItem = (tasks, addTasks) => {
const [value, setValue] = useState("");
const handleOnClick = (e) => {
e.preventDefault();
tasks.addTasks((prev) => [
...prev,
{
id: uuidv4(),
todo: value,
done: false
}
]);
setValue("");
};
return (
<div>
<div>
<h2 className="">What needs to be done?</h2>
{<p>{tasks.tasks[0].todo}</p>}
<div className="task-input">
<input
type="text"
className="d-inline mx-2"
placeholder="Add a task"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button className="d-inline mx-2" onClick={handleOnClick}>
Add
</button>
</div>
</div>
</div>
);
};
export default AddItem;
I'm writing a program where you can add reviews to a list in React. I also added a feature to delete the reviews. Each review is a component stored in a State array. I wrote a function, removeItem, that updates the state by creating a duplicate of the array and popping the passed index". Each review is given a handleFeature property where this removeItem function is passed, and an id which corresponds to it's index in the array.
Inside the review component, it has an onclick event which calls the removeItem function through handleFeature, passing it's own id in the array. I thought this would cause the array to update and remove the item; however, It causes multiple items to get deleted for no apparent reason. Does anyone know the fix to this issue
Data
export default[
{
id: 0,
name: "Mindustry",
score: "4.5"
},
{
id: 1,
name: "Minecraft",
score: "4"
},
{
id: 2,
name: "Plants vs Zombies",
score: "4"
},
]
App
import './App.css';
import jSData from './components/jSData.js'
import Card from './components/Card.js'
import React from 'react';
function App() {
//we are mapping the review data to an array of cards using an lamba expression
//this is a state object. Once it changes, the webpage is updated
//it returns the object and a function to change it
//the object is immutable; however, you can reference it to make updates
const [reviews, changeState] = React.useState(jSData.map((item) => {
return (<Card
//key is necessary for list items
key = {item.id}
handleEvent = {removeItem}
//name = {item.name}
//score = {item.score}
//the above can be simplified to
{...item}
/>);
}
));
function submit(e)
{
//prevent reloading
e.preventDefault();
//spreading the original array + card into a new array
/*changeState(
[...reviews,
<Card
id = {reviews.length}
name = {document.getElementById('form_name').value}
score = {document.getElementById('form_score').value}
/>]
);*/
//best practice to use the higher order version of state change
//this should contain a function which returns the new state
changeState(oldValue =>
[...oldValue,
<Card
id = {reviews.length}
key = {reviews.length}
handleEvent = {removeItem}
name = {document.getElementById('form_name').value}
score = {document.getElementById('form_score').value}
/>]
);
}
function removeItem(id)
{
changeState(reviews.map(x => x).pop(id))
}
return (
<div id = "wrapper">
<form id = "review-form">
<h1>Review</h1>
<input className = "review-text" placeholder="Name" id = "form_name"/>
<input className = "review-text" placeholder="Score" id = "form_score"/>
<input id = "review-button" type = "Submit" onClick = {submit}/>
</form>
<ul id = "card-holder">
{reviews}
</ul>
</div>
);
}
export default App;
Review Component
import React from "react";
export default function Card(item)
{
function handle()
{
console.log(item.handleEvent);
item.handleEvent(item.id)
}
//conditional rendering with and statements
return(
<div className = "card-wrapper">
<div className = "card">
<h2>{item.name}</h2>
<h4>{item.score} / 5</h4>
</div>
<span class="material-symbols-outlined" onClick = {handle}>close</span>
</div>
);
}
Here's a simplified and fixed example. As I said in the comment, don't put elements in state; just map your data into elements when returning your markup.
The initial data is returned by a function, so we dont accidentally mutate "static data" from another module
The Card component now accepts the item as a prop, not "spread out"
Removal actually works (we filter the state array so there's only items without the ID-to-remove left)
import React from "react";
function getInitialData() {
return [
{
id: 0,
name: "Mindustry",
score: "4.5",
},
{
id: 1,
name: "Minecraft",
score: "4",
},
{
id: 2,
name: "Plants vs Zombies",
score: "4",
},
];
}
function Card({ item, onRemove }) {
return (
<div className="card-wrapper">
<div className="card">
<h2>{item.name}</h2>
<h4>{item.score} / 5</h4>
</div>
<span
className="material-symbols-outlined"
onClick={() => onRemove(item.id)}
>
close
</span>
</div>
);
}
function App() {
const [data, setData] = React.useState(getInitialData);
function removeItem(id) {
setData((reviews) => reviews.filter((item) => item.id !== id));
}
function submit(e) {
e.preventDefault();
setData((reviews) => {
// TODO: do these with refs or state instead of getElementById
const name = document.getElementById("form_name").value;
const value = document.getElementById("form_score").value;
const newReview = {
id: (+new Date()).toString(36),
name,
value,
};
return [...reviews, newReview];
});
}
return (
<div id="wrapper">
<form id="review-form">
<h1>Review</h1>
<input
className="review-text"
placeholder="Name"
id="form_name"
/>
<input
className="review-text"
placeholder="Score"
id="form_score"
/>
<input id="review-button" type="Submit" onClick={submit} />
</form>
<ul id="card-holder">
{data.map((item) => (
<Card key={item.id} item={item} onRemove={removeItem} />
))}
</ul>
</div>
);
}
I'm still beginner to ReactJS and need to build a dynamic table for my work.
In that table, the user can add new lines and can also remove any existing lines.
The problem is, I don't know how to save the values that are typed into the new fields. My onChange function isn't working, I've done several tests, but I'm not able to save the entered values.
Here's my code I put into codesandbox.
Could you tell me what I'm doing wrong to save the entered values? Thank you in advance.
import React from "react";
import "./styles.css";
import List from "./List/List";
const App = () => {
const [data, setData] = React.useState([
[
{
label: "Property Name",
field: "propertyName",
value: ""
},
{
label: "Value",
field: "value",
value: ""
}
]
]);
const handleOnChange = (field) => (e) => {
setData((prev) => ({
...prev,
[field]: e.target.value
}));
};
const addRow = () => {
setData([
...data,
[
{
label: "Property Name",
field: "propertyName",
value: ""
},
{
label: "Value",
field: "value",
value: ""
}
]
]);
};
const removeRow = (index) => {
const _data = [...data];
_data.splice(index, 1);
setData(_data);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<List
data={data}
addRow={addRow}
removeRow={removeRow}
handleOnChange={handleOnChange}
/>
</div>
);
};
export default App;
import React from "react";
import AddCircleIcon from "#material-ui/icons/AddCircle";
import RemoveCircleIcon from "#material-ui/icons/RemoveCircle";
import TextField from "#material-ui/core/TextField";
import "./styles.scss";
const List = ({ data, handleOnChange, addRow, removeRow }) => {
return (
<div className="container">
{data.map((items, index) => (
<div key={index} className="content">
<div className="content-row">
{items.map((item, index) => (
<TextField
key={index}
label={item.label}
value={item.value}
onChange={handleOnChange(index)}
variant="outlined"
/>
))}
</div>
<div>
<AddCircleIcon onClick={addRow} />
{data.length > 1 && (
<RemoveCircleIcon onClick={() => removeRow(index)} />
)}
</div>
</div>
))}
</div>
);
};
export default List;
Good that you shared the code. There are several issues with your code. I have an updated code placed under this URL,
https://codesandbox.io/s/reverent-mclean-hfyzs
Below are the problems,
Your data structure is an array of arrays and your onchange event doesn't respect that.
You have no property available [name/id] to identify a textbox when you change the value.
I had to add a name property to each textbox and design it like a 2D array so any textbox will have a unique name.
I had to map through the data array and find the node where I have to update the value and set the new value as the new state when any textbox changes.
I have added a console.log while adding a row so you can see the current state.
I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.
I am pretty new to react. So I have one parent component which has two child components. These 2 children are the lists that should be displayed. So far I figured out how to transfer the data between two lists by checking the status property of the data. I am not able to understand how to add data into the separate lists and edit them since the parent component renders the 2 lists. Can anyone explain how to add and edit new data that the user will enter? Should I create new states and props on the Items page or should I create them on the child component page? I am pretty confused.
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
<form onSubmit={this.addItem}>
<input placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p> </>
})
}
</div>
)
}
I have attached the 3 components which are Items, Pending and Completed above.
It's almost always better to have the state in the parent and pass down props to the children. So you want to keep your items state where it is. You can create an addItem function and pass it down as a prop to any child.
I don't think it makes sense to be able to add items from both lists since new items should be 'Pending'. So I would recommend that you put your add form in a new component AddItem which would be a third child of Items. Once AddItem calls the addItem function from props, that item will get saved to the state in items and it will show up in the Pending list automatically.
If all new items have status 'Pending' then the only information that we should need to add an item is the title of the task.
This function goes in Items:
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
Your AddItem component uses a controlled input to create the text for the new item.
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
Inside the return of Items, include your form:
<AddItem addItem={addItem} />
Unrelated to the question at hand, there are a few other improvements that you can make to your code.
Your updateStatus function actually mutates the current item. You should instead create a new object for the changed item by copying everything except the status.
You are getting warnings about unique keys because the key must be on the outermost component inside the .map(). You put a fragment <> outside the <p> which has the key, so remove the fragment.
In my opinion the filtering of which item goes in each list should be done by the parent. Your Completed and Pending components are extremely similar. You should combine them into one component. Everything that is different between the two, such as texts and class names, can be controlled by the props that you pass in.
import React, { useState } from "react";
export const ItemsList = ({
items,
title,
className,
buttonText,
onClickButton
}) => {
return (
<div className={className}>
<h1>{title}</h1>
{items.map((item) => (
<p className="item" key={item.id}>
<span className="item_title">{item.title}</span>
<button
className="move_item"
key={item.id}
onClick={() => {
onClickButton(item.id);
}}
>
{buttonText}
</button>
</p>
))}
</div>
);
};
// example of how to compose components
// this keeps the same setup that you had before, but without repeated code
export const Completed = ({ items, updateStatus }) => {
return (
<ItemsList
title="RIGHT"
buttonText="Move Left"
className="completed"
items={items.filter((item) => item.status === "Completed")}
onClickButton={(id) => updateStatus(id, "Pending")}
/>
);
};
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
export const Items = () => {
const [items, setItems] = useState([
{
id: 1,
title: "Workout",
status: "Pending"
},
{
id: 2,
title: "Read Books",
status: "Pending"
},
{
id: 3,
title: "Cook Pizza",
status: "Pending"
},
{
id: 4,
title: "Pay Bills",
status: "Completed"
},
{
id: 5,
title: " Watch Big Short",
status: "Completed"
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending"
}
]);
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
const updateStatus = (id, newStatus) => {
setItems((current) =>
// arrow function without braces is an implicit return
current.map((item) =>
item.id === id
? // copy to new item if id matches
{
...item,
status: newStatus
}
: // otherwise return the existing item
item
)
);
};
return (
<div className="items">
<AddItem addItem={addItem} />
{/* can set the props on ItemsList here */}
<ItemsList
title="LEFT"
buttonText="Move Right"
className="pending"
items={items.filter((item) => item.status === "Pending")}
// create a function that just takes the `id` and sets the status to "Completed"
onClickButton={(id) => updateStatus(id, "Completed")}
/>
{/* or do it in a separate component */}
<Completed items={items} updateStatus={updateStatus} />
</div>
);
};
export default Items;
Code Sandbox Link