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.
Related
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
}
}
})
})
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
I'm trying to create a function that renders an array of links and i want to create a text input and a button that adds value from input in the array. I got the links saved in the state in the object that looks like this:
sourceLinks: {
0: "https://www.w3schools.com/html/"
1: "https://www.apachefriends.org/docs/"
2: "https://docs.moodle.org/38/en/Windows_installation_using_XAMPP"
}
I've managed to render the links like this:
renderLinks() {
let sessionLinks = this.state.sessionLinks;
let links = [];
Object.values(sessionLinks).map((link) => {
links.push(<div className="column">
<span>
<InputPreview inputValue={link} classes="w-300" />
</span>
</div>)
})
return links;
}
InputPreview is the component i use for displaying links. I'm tryin to add a text input and a button bellow the rendered links that adds the value to the array, and an icon next to every link that removes it from an array. I'm trying to do it all in one function renderLinks() and then call it in render. I know i have to push and slice items from an array and update the state but i'm strugling cause i just started learning react. Please help :)
You can add and render links with below code.
import React from "react";
class ItemList extends React.Component {
state = {
links: ["item1"],
newItem: ""
};
submit(e, newLink) {
e.preventDefault();
let updatedLinks = this.state.links;
updatedLinks.push(newLink);
this.setState({ links: updatedLinks });
}
render() {
return (
<React.Fragment>
<ul>
{this.state.links?.map((link, i) => (
<li key={i}>
<p>{link}</p>
</li>
))}
</ul>
<form onSubmit={(e) => this.submit(e, this.state.newItem)}>
<input
type="text"
value={this.state.newItem}
onChange={(e) => this.setState({ newItem: e.target.value })}
/>
<button type="submit">ADD</button>
</form>
</React.Fragment>
);
}
}
export default ItemList;
Let me know for further clarificaton.
This is a example with functional components and hooks
import React, { useState } from 'react';
const sourceLinks = [
'https://www.w3schools.com/html/',
'https://www.apachefriends.org/docs/',
'https://docs.moodle.org/38/en/Windows_installation_using_XAMPP',
];
export const ListLinks = () => {
const [links, setLinks] = useState(sourceLinks);
const [newLink, setNewLink] = useState('');
const handleAdd = () => {
setLinks(links => [...links, newLink]);
};
const handleChangeNewLink = e => {
const { value } = e.target;
setNewLink(value);
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<input type='text' value={newLink} onChange={handleChangeNewLink} />
<button onClick={handleAdd}>Add</button>
</div>
<br />
{links.map((link, index) => (
<p key={index}>{link}</p>
))}
</div>
);
};
This is the result:
Lastly, read the documentation, managing the state is essential.
I have been converting all my classes to functions to update my react projects. I am getting a:
'ESLint has encountered a parsing error' in my code. If I remove ONE line, the error disappears. Here's the code
import React, { useState } from "react";
import orderBy from "lodash/orderBy";
import SelectField from "#material-ui/core";
import MenuItem from "#material-ui/core";
import TextField from "#material-ui/core";
import Table from "./Table";
const invertDirection = {
asc: "desc",
desc: "asc"
};
const Patients = props =>
{
var handleRemove = (i) => {
setPatientsData(patientsData.filter((row, j) => j !== i));
};
var handleSelect = (i) =>
{
setEditIdx(i);
}
const [editIdx, setEditIdx] = useState(-1);
const [columnToSort, setColumnToSort] = useState('');
const [sortDirection, setSortDirection] = useState('asc');
const [patientsData, setPatientsData] = useState([]);
const [columnToQuery, setColumnToQuery] = useState('');
const [columnQueryValue, setColumnQueryValue] = useState('');
var handleSort = columnName =>
{
if (columnName === columnToSort) {
setSortDirection(invertDirection[sortDirection]);
}
else {
setSortDirection("asc");
}
setColumnToSort(columnName);
};
var handleQueryChange = (value) =>
{
setColumnQueryValue({ value });
}
const lowerCaseQuery = this.state.query.toLowerCase();
return (
<div>
<div style={{ display: "flex" }}>
<div style={{ display: "flex", margin: "auto" }}>
<TextField
hintText="Query"
floatingLabelText="Query"
value={columnQueryValue}
onChange={(event) => setColumnQueryValue({event.target.value })} --->>>ERROR
floatingLabelFixed
/>
<SelectField
style={{ marginLeft: "1em" }}
floatingLabelText="Select a column"
value={columnToQuery}
onChange={(event, index, value) => setColumnToQuery({value }) }
>
<MenuItem value="firstName" primaryText="First Name" />
<MenuItem value="lastName" primaryText="Last Name" />
<MenuItem value="username" primaryText="Username" />
<MenuItem value="email" primaryText="Email" />
</SelectField>
</div>
</div>
<Table
handleSort={handleSort}
handleRemove={handleRemove}
editIdx={editIdx}
handleSelect={handleSelect}
columnToSort={columnToSort}
sortDirection={sortDirection}
data={orderBy(
columnQueryValue
? patientsData.filter(x =>
x[columnToQuery]
.toLowerCase()
.includes(lowerCaseQuery)
)
: patientsData,
columnToSort,
sortDirection
)}
header={[
{
name: "First name",
prop: "firstName"
},
{
name: "Last name",
prop: "lastName"
},
{
name: "Username",
prop: "username"
},
{
name: "Email",
prop: "email"
}
]}
/>
</div>
);
}
export default Patients;
Specifically, this line:
onChange={(event) => setColumnQueryValue({event.target.value })}
I've spent 24 hours trying to fix it, tried everything, but still get the error. Any help would be greatly appreciated!
ADDED: I fixed a typo that was not causing the problem. Specifically, the interpreter complains about the 'event.target.value' I am passing to the setColumnQueryValue which is the state setter for ColumnQueryValue, which is a string. Which is SUPPOSED to be what a text field returns. (event.target.value). That is the code that is the issue and that I cannot figure out why it's complaining.
Firstly, You don't have any setQueryValue function in your code, I think you misinterpreted with setColumnQueryValue. Secondly, you're passing an object and didn't define a key for the event.target.value. As I see that you initialized columnQueryValue with empty string so you should pass a string value in function.
onChange={(event) => setColumnQueryValue(event.target.value)}
Ok, well separating the onChange event into a called function made the error go away, but I still don't understand what I did wrong.
Added:
var handleOnChange = event =>
{
setColumnQueryValue(event.target.value);
};
Changed:
onChange={(event) => setColumnQueryValue({event.target.value })}
to
onChange={handleOnChange}
No difference I can detect in the actual code, but one worked, the other didn't. I hate javascript. ;-)