So, The problem is that whenever I press the delete button, it just removes the deletes the last input and the button regardless of the index. I've tried to change the key to key={${index}${name}}`, this won't work that well, because the inputs are changeable (in main code) and that makes thing messed up.
How can I make delete properly according to indexes??
function App() {
const [names, setNames] = React.useState(["First", "Second",'third','fourth']);
const onNameDelete = useCallback((index: number) => {
setNames((prev) => {
const name = names[index];
return prev.filter(x => x != name);
});
}, [names]);
return (
<div>
{names.map((name, index) => (
<Child
key={index}
name={name}
index={index}
onDelete={onNameDelete}
/>
))}
</div>
);
}
interface ChildProps {
name: string;
index: number;
onDelete: (index: number) => void;
}
export const Child: React.FC<ChildProps> = React.memo(
({ name, index, onDelete }) => {
return (
<div>
<input
defaultValue={name}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
}
Here goes:
function App() {
const [names, setNames] = React.useState(
["First", "Second",'third','fourth'].map((name, index) => ({ id: index, name }))
);
const onNameDelete = useCallback((index: number) => {
setNames((prev) => prev.filter({ id } => id !== index));
}, [names]);
return (
<div>
{names.map(({ id, name }) => (
<Child
key={id}
name={name}
index={id}
onDelete={onNameDelete}
/>
))}
</div>
);
}
interface ChildProps {
name: string;
index: number;
onDelete: (index: number) => void;
}
export const Child: React.FC<ChildProps> = React.memo(
({ name, index, onDelete }) => {
return (
<div>
<input
defaultValue={name}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
}
Instead of an array for storing names, please use object array:
const [names, setNames] = React.useState(
["First", "Second",'third','fourth'].map((name, index) => ({ id: index, name }))
);
Related
I'm making a todo list in react js. Each time a new todo item is created, some buttons are appended next to it along with a edit input text box. I'm trying to avoid using refs but purely usestate for my case, however I can't figure out how to do it. At its current state, all edit text inputs are using the same state and that brings focus loss along with other issues. I'd highly appreciate any suggetsions.
import "./theme.css"
import * as appStyles from "./styles/App.module.css"
import * as todoStyles from "./styles/Todo.module.css"
import { useState } from "react"
const initialState = [
{
id: "1",
name: "My first ToDo",
status: "new",
},
]
export function App() {
const [numofItems, setNumofItems] = useState(2)
const [newToDo, setnewToDo] = useState('');
const [todos, setTodos] = useState(initialState);
const [editTodo, setEditTodo] = useState({name: ""});
const onAddTodo = () => {
setnewToDo("");
setTodos((old) => [
...old,
{ id: numofItems.toString(), name: newToDo, status: "new" },
])
setNumofItems(numofItems + 1);
}
deleteList = () =>{
setTodos([]);
}
const handleEdit = (id, description) =>{
let el = todos.map((item) => {if(item.id === id) {item.name = description} return item});
setTodos(el);
setEditTodo('');
}
const handleMove = (id, position) =>{
const search = obj => obj.id === id;
const todoIndex = todos.findIndex(search);
if(position === "up"){
if (todos[todoIndex - 1] === undefined) {
} else {
const newTodo1 = [...todos];
const temp1 = newTodo1[todoIndex - 1];
const temp2 = newTodo1[todoIndex]
newTodo1.splice(todoIndex - 1, 1, temp2);
newTodo1.splice(todoIndex, 1, temp1);
setTodos([...newTodo1]);
}
}
else if(position === "down"){
if (todos[todoIndex + 1] === undefined) {
} else {
const newTodo1 = [...todos];
const temp1 = newTodo1[todoIndex + 1];
const temp2 = newTodo1[todoIndex]
newTodo1.splice(todoIndex + 1, 1, temp2);
newTodo1.splice(todoIndex, 1, temp1);
setTodos([...newTodo1]);
}
}
}
const Todo = ({ record }) => {
return <li className={todoStyles.item}>{record.name}
<button className={appStyles.editButtons} onClick={() => deleteListItem(record.id)} >Delete</button>
<button className={appStyles.editButtons} onClick={() => handleEdit(record.id, editTodo.name)}>Edit</button>
<button className={appStyles.editButtons} onClick={() => handleMove(record.id, "down")}>Move down</button>
<button className={appStyles.editButtons} onClick={() => handleMove(record.id, "up")}>Move up</button>
<input className={appStyles.input}
type = "text"
name={`editTodo_${record.id}`}
value = {editTodo.name}
onChange={event => {event.persist();
setEditTodo({name: event.target.value});}}
/></li>
}
const deleteListItem = (todoid) => {
setTodos(todos.filter(({id}) => id !== todoid))
}
return (
<>
<h3 className={appStyles.title}>React ToDo App</h3>
<ul className={appStyles.list}>
{todos.map((t, idx) => (
<Todo key={`todo_${idx}`} record={t} />
))}
</ul>
<div className={appStyles.actions}>
<form>
<label>
Enter new item:
<input className={appStyles.input} type="text" name="newToDo" value={newToDo} onChange={event => setnewToDo(event.target.value)}/>
</label>
</form>
<button
className={appStyles.button}
onClick={onAddTodo}
>
Add
</button>
<br></br>
<button className={appStyles.button} onClick={this.deleteList}>
Delete List
</button>
</div>
</>
)
}
Never define components in the body of another component. It will result in unmount/mount of that element every time it's rendered.
Here is how you can split up the Todo component from you App:
const Todo = ({ record, onDelete, onEdit, onMove }) => {
const [inputValue, setInputValue] = useState(record.name);
return (
<li className={todoStyles.item}>
{record.name}
<button className={appStyles.editButtons} onClick={() => onDelete()}>
Delete
</button>
<button
className={appStyles.editButtons}
onClick={() => onEdit(inputValue)}
>
Edit
</button>
<button className={appStyles.editButtons} onClick={() => onMove("down")}>
Move down
</button>
<button className={appStyles.editButtons} onClick={() => onMove("up")}>
Move up
</button>
<input
className={appStyles.input}
type="text"
value={inputValue}
onChange={(event) => {
setInputValue(event.target.value);
}}
/>
</li>
);
};
function App() {
return (
<>
<ul className={appStyles.list}>
{todos.map((t, idx) => (
<Todo
key={`todo_${idx}`}
record={t}
onDelete={() => deleteListItem(t.id)}
onEdit={(description) => handleEdit(t.id, description)}
onMove={(position) => handleMove(t.id, position)}
/>
))}
</ul>
</>
);
}
Note: I've shown only the interesting bits, not your entire code.
If you're going to do it this way I would suggest using useReducer instead of useState.
const initialState = [
{
id: "1",
name: "My first ToDo",
status: "new",
},
]
export const types = {
INIT: 'init',
NEW: 'new'
}
export default function (state, action) {
switch (action.type) {
case types.INIT:
return initialState;
case types.NEW:
return { ...state, { ...action.item } };
default:
return state;
}
}
Now in your component you can use it like this:
import {useReducer} from 'react';
import reducer, { initialState, types } from './wherever';
const [state, dispatch] = useReducer(reducer, initialState);
const handleSubmit = (event) => {
event.preventDefault();
dispatch({ type: types.NEW, item: event.target.value });
}
When the user adds a value to an object that is inside an array the status is updated to added.
I'm trying to do the same thing when that value is deleted, to update the status to deleted.
const initialName = [
{
name: "",
status: "",
},
];
export default function changeNames(){
const [name, setName] = useState([initialName ]);
const handleAdd = () => {
setName([...name, ""]);
};
const handleItemChanged = (event, index) => {
const value = event.target.value;
const list = [...name];
list[index] = { name: value + "-" + id, status: "added" };
setName(list);
};
...
}
So when I add an input field and type the name, the array looks like this:
[{…}]
0:
name: "John-id"
status: "added"
When I remove John from the array, I want smth like this:
[{…}]
0:
name: "John-id"
status: "deleted"
This is the remove function
const handleRemoveClick = (index) => {
const list = [...name];
list.splice(index, 1);
setName(list);
};
<div>
{name.map((o, i) => {
return (
<tr key={"item-" + i}>
<td>
<div>
<input
type="text"
value={o.item}
onChange={(event) => handleItemChanged(event, i)}
placeholder="Name it"
/>
</div>
</td>
<td>
<button type="button" onClick={handleRemoveClick}>
Delete
</button>
</td>
</tr>
);
})}
<div>
<button Click={handleAddClick}>
Add Language
</button>
</div>
</div>
);
How can I make it work?
I think this might help you.thank you
import { useState } from "react";
import "./styles.css";
export default function App() {
const [Name, setName] = useState([]);
const [input, setInput] = useState({ name: "", status: "" });
const handleItemChanged = (event, index) => {
const { value } = event.target;
setInput({ name: value + "-id", status: "added" });
};
const addHandler = () => {
setName((prev) => {
return [...prev, input];
});
};
const removeHandler = (i) => {
const arr = [...Name];
arr[i].status = "deleted";
setName(arr);
};
return (
<div className="App">
<input type="text" name="name" onChange={(e) =>
handleItemChanged(e)} />
<button onClick={addHandler} style={{ margin: "1rem" }}>
Add
</button>
<div>
{Name &&
Name.map((data, i) => {
return (
<div key={i}>
<h3>
{data.name} {data.status}
<button
style={{ margin: "1rem" }}
onClick={() => removeHandler(i)}
>
Delete
</button>
</h3>
</div>
);
})}
</div>
</div>
);
}
You need to change the "status" property, because there is no way of removing item and setting its property.
const handleRemoveClick = (event, index) => {
const list = [...name];
list[index].status = "deleted";
setName(list);
};
And later while rendering you have to either perform a check in map function (not really elegant), or first filter your array and then map it:
// first, not elegant in my opinion
...
{name.map(item => item.status !== "deleted" ? <div>item.name</div> : null)}
// second approach
...
{name.filter(item => item.status !== "deleted").map(item => <div>item.name</div>)}
I am building a clone of the Google Keep app with react js. I added all the basic functionality (expand the create area, add a note, delete it) but I can't seem to manage the edit part. Currently I am able to edit the inputs and store the values in the state, but how can I replace the initial input values for the new values that I type on the input?
This is Note component
export default function Note(props) {
const [editNote, setEditNote] = useState(false);
const [currentNote, setCurrentNote] = useState({
id: props.id,
editTitle: props.title,
editContent: props.content,
});
const handleDelete = () => {
props.deleteNote(props.id);
};
const handleEdit = () => {
setEditNote(true);
setCurrentNote((prevValue) => ({ ...prevValue }));
};
const handleInputEdit = (event) => {
const { name, value } = event.target;
setCurrentNote((prevValue) => ({
...prevValue,
[name]: value,
}));
};
const updateNote = () => {
setCurrentNote((prevValue, id) => {
if (currentNote.id === id) {
props.title = currentNote.editTitle;
props.content = currentNote.editContent;
} else {
return { ...prevValue };
}
});
setEditNote(false);
};
return (
<div>
{editNote ? (
<div className='note'>
<input
type='text'
name='edittitle'
defaultValue={currentNote.editTitle}
onChange={handleInputEdit}
className='edit-input'
/>
<textarea
name='editcontent'
defaultValue={currentNote.editContent}
row='1'
onChange={handleInputEdit}
className='edit-input'
/>
<button onClick={() => setEditNote(false)}>Cancel</button>
<button onClick={updateNote}>Save</button>
</div>
) : (
<div className='note' onDoubleClick={handleEdit}>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleDelete}>DELETE</button>
</div>
)}
</div>
);
}
And this is the Container component where I am renderind the CreateArea and mapping the notes I create. I tried to map the notes again with the new values but it wasn't working.
export default function Container() {
const [notes, setNotes] = useState([]);
const addNote = (newNote) => {
setNotes((prevNotes) => {
return [...prevNotes, newNote];
});
};
const deleteNote = (id) => {
setNotes((prevNotes) => {
return prevNotes.filter((note, index) => {
return index !== id;
});
});
};
// const handleUpdateNote = (id, updatedNote) => {
// const updatedItem = notes.map((note, index) => {
// return index === id ? updatedNote : note;
// });
// setNotes(updatedItem);
// };
return (
<div>
<CreateArea addNote={addNote} />
{notes.map((note, index) => {
return (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={deleteNote}
//handleUpdateNote={handleUpdateNote}
/>
);
})}
</div>
);
}
There are a couple of mistakes in your code.
The state properties are in the camel case
const [currentNote, setCurrentNote] = useState({
...
editTitle: props.title,
editContent: props.content,
});
But the names of the input are in lowercase.
<input
name='edittitle'
...
/>
<textarea
name='editcontent'
...
/>
Thus in handleInputEdit you don't update the state but add new properties: edittitle and editcontent. Change the names to the camel case.
In React you cant assign to the component prop values, they are read-only.
const updateNote = () => {
...
props.title = currentNote.editTitle;
props.content = currentNote.editContent;
You need to use the handleUpdateNote function passed by the parent component instead. You have it commented for some reason.
<Note
...
//handleUpdateNote={handleUpdateNote}
/>
Check the code below. I think it does what you need.
function Note({ id, title, content, handleUpdateNote, deleteNote }) {
const [editNote, setEditNote] = React.useState(false);
const [currentNote, setCurrentNote] = React.useState({
id,
editTitle: title,
editContent: content,
});
const handleDelete = () => {
deleteNote(id);
};
const handleEdit = () => {
setEditNote(true);
setCurrentNote((prevValue) => ({ ...prevValue }));
};
const handleInputEdit = (event) => {
const { name, value } = event.target;
setCurrentNote((prevValue) => ({
...prevValue,
[name]: value,
}));
};
const updateNote = () => {
handleUpdateNote({
id: currentNote.id,
title: currentNote.editTitle,
content: currentNote.editContent
});
setEditNote(false);
};
return (
<div>
{editNote ? (
<div className='note'>
<input
type='text'
name='editTitle'
defaultValue={currentNote.editTitle}
onChange={handleInputEdit}
className='edit-input'
/>
<textarea
name='editContent'
defaultValue={currentNote.editContent}
row='1'
onChange={handleInputEdit}
className='edit-input'
/>
<button onClick={() => setEditNote(false)}>Cancel</button>
<button onClick={updateNote}>Save</button>
</div>
) : (
<div className='note' onDoubleClick={handleEdit}>
<h1>{title}</h1>
<p>{content}</p>
<button onClick={handleDelete}>DELETE</button>
</div>
)}
</div>
);
}
function CreateArea() {
return null;
}
function Container() {
const [notes, setNotes] = React.useState([
{ title: 'Words', content: 'hello, bye' },
{ title: 'Food', content: 'milk, cheese' }
]);
const addNote = (newNote) => {
setNotes((prevNotes) => {
return [...prevNotes, newNote];
});
};
const deleteNote = (id) => {
setNotes((prevNotes) => {
return prevNotes.filter((note, index) => {
return index !== id;
});
});
};
const handleUpdateNote = ({ id, title, content }) => {
const _notes = [];
for (let i = 0; i < notes.length; i++) {
if (i === id) {
_notes.push({ id, title, content });
} else {
_notes.push(notes[i]);
}
}
setNotes(_notes);
};
return (
<div>
<CreateArea addNote={addNote} />
{notes.map((note, index) => {
return (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={deleteNote}
handleUpdateNote={handleUpdateNote}
/>
);
})}
</div>
);
}
function App() {
return (
<div>
<Container />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Also, you can store the notes in an object or hash map instead of an array. For example
const [notes, setNotes] = React.useState({
'unique_id': { title: 'Words', content: 'hello, bye' }
});
Then in handleUpdateNote you have
setNotes((prev) => ({ ...prev, unique_id: { title, content } }))
I find myself struggling to change the form input value from the button onClick handler. My problem is escalated by the fact that I have the line items on a form and things become even more complex that way. I tried to bring in a ref to set value,,, the value is displayed but at the form state after submission it remains empty which means the value is not being set. Below are all my code files. May someone kindly help me on where I may be missing the point.
class VehicleTemplate extends React.Component {
constructor(props){
super(props);
this.ws = new WebSocket('ws://localhost:8000/ws/weightData/');
this.socketRef = null;
this.state = {
data: 0,
name: '',
model: '',
plate_number: '',
type: 'truck',
wagons: [{ index: uuid(), label: '', type: 'once', weight: '' }],
total: 0,
};
this.onSubmit = this.onSubmit.bind(this);
this.handleChange= this.handleChange.bind(this);
this.handleChangeTable = this.handleChangeTable.bind(this);
this.addNewRow = this.addNewRow.bind(this);
this.deleteRow = this.deleteRow.bind(this);
this.handleLineItemChange = this.handleLineItemChange.bind(this);
}
componentDidMount() {
this.ws.onopen = () => {
// on connecting, do nothing but log it to the console
console.log('connected')
}
this.ws.onmessage = evt => {
// listen to data sent from the websocket server
// const message = JSON.parse(evt.data)
const {data} = this.state;
this.setState({data: evt.data})
// this.setState({this.state.lines.weight: data})
}
this.ws.onclose = () => {
console.log('disconnected')
// automatically try to reconnect on connection loss
}
};
onSubmit = (e) => {
e.preventDefault();
const {
name,
model,
plate_number,
type,
wagons,
} = this.state;
const vehicle = {
name,
model,
plate_number,
type,
wagons,
};
this.props.addVehicle(vehicle, this.props.token);
console.log(vehicle);
this.setState({
name: '',
model: '',
plate_number: '',
wagons: [],
});
};
handleLineItemChange = (elementIndex) => (event) => {
let wagons = this.state.wagons.map((item, i) => {
if (elementIndex !== i) return item
return {...item, [event.target.name]: event.target.value}
})
this.setState({wagons})
}
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
handleChangeTable = (name, id) => event => {
this.updateItem(id, { [name]: event.target.value });
};
addNewRow = (event) => {
this.setState({
wagons: this.state.wagons.concat(
[{index: uuid(), label: '', type: 'once', weight: '' }]
)
})
}
deleteRow = (index) => (event) => {
this.setState({
wagons: this.state.wagons.filter((item, i) => {
return index !== i
})
})
}
handleReorderLineItems = (newLineItems) => {
this.setState({
wagons: newLineItems,
})
}
clickOnDelete(record) {
this.setState({
wagons: this.state.lines.filter(r => r !== record)
});
}
render() {
const { classes } = this.props;
const {
data,
name,
model,
plate_number,
wagons,
} = this.state;
return (
<InformationTechnologyLayout>
<PapperBlock title="ADD VEHICLE" icon="ios-document-outline" desc="VEHICLE">
<Form>
<div style={{ clear: 'both' }} />
<h1>WEIGHT: {data}KG</h1>
<Grid container>
<Grid item xs={6}>
<Controls.Input
name="name"
label="Name"
value={name}
onChange={this.handleChange('name')}
/>
<Controls.Input
name="model"
label="MODEL"
value={model}
onChange={this.handleChange('model')}
/>
</Grid>
<Grid item xs={6}>
<Controls.Input
name="plate_number"
label="PLATE NUMBER"
value={plate_number}
onChange={this.handleChange('plate_number')}
/>
</Grid>
</Grid>
<Extensions
items={wagons}
addHandler={this.addNewRow}
changeHandler={this.handleLineItemChange}
focusHandler={this.handleFocusSelect}
deleteHandler={this.deleteRow}
reorderHandler={this.handleReorderLineItems}
data ={data}
/>
<div className='valueTable'>
<div className='row'>
<div className='label'>Subtotal</div>
<div className='value'>{this.calcLineItemsTotal()}KG</div>
</div>
<div className='row'>
<div className='label'>Total Due</div>
<div className='value'>{this.calcGrandTotal()}KG</div>
</div>
</div>
<Button variant="contained" onClick={this.onSubmit}>SUBMIT</Button>
</Form>
</PapperBlock>
</InformationTechnologyLayout>
);
}
}
On top is the main form component which has got Vehicle exetnsions and I am getting my weight from a websocket connection streaming scale outputs.
class Extensions extends Component {
handleDragEnd = (result) => {
if (!result.destination) return
// helper function to reorder result (src: react-beautiful-dnd docs)
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
// perform reorder
const lineItems = reorder(
this.props.items,
result.source.index,
result.destination.index
)
// call parent handler with new state representation
this.props.reorderHandler(lineItems)
}
render = () => {
const {
items,
addHandler,
changeHandler,
focusHandler,
deleteHandler,
reorderHandler,
products,
data,
readOnly,
} = this.props
return (
<form>
<div className='lineItems'>
<div className='gridTable'>
<DragDropContext onDragEnd={this.handleDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className= 'listDraggingOver'
>
{this.props.items.map((item, i) => (
<Draggable key={item.index} draggableId={item.index} index={i}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={provided.draggableProps.style}
className='listItemDragging'
>
<Extension
addHandler={addHandler}
changeHandler={changeHandler}
focusHandler={focusHandler}
deleteHandler={deleteHandler}
reorderHandler={reorderHandler}
data={data}
style={{color: 'red'}}
key={i + item.index}
index={i}
name={item.index}
label={item.label}
weight={item.weight}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
<div className='addItem'>
<button type="button" onClick={addHandler}><AddIcon size="1.25em" className='addIcon' /> Add Item</button>
</div>
</div>
</form>
)
}
}
This are the extensions and the last file is where I would like to capture weight from
class LineItem extends Component {
constructor(props){
super(props);
this.ref = React.createRef();
this.state = {
buttonState: false,
};
}
requestWeight = () => {
const { weight } = this.props;
const {buttonState} =this.state;
this.ref.current.value = this.props.data;
this.setState({
buttonState: true
})
};
render = () => {
const { index, label, weight } = this.props;
const {buttonState} = this.state;
return (
<div className='lineItem'>
<div>{index + 1}</div>
<Controls.Input
name="label"
label="LABEL"
value={label}
onChange={this.props.changeHandler(index)}
/>
<input
className='currency'
type='number'
readOnly={true}
ref={this.ref}
name='weight'
onChange={this.props.changeHandler(index)}
/>
<div>
<button type="button"
className='requestItems'
onClick={this.requestWeight}
disabled={buttonState}
>REQUEST WEIGHT</button>
</div>
<div>
<button type="button"
className='deleteItem'
onClick={this.props.deleteHandler(index)}
><DeleteIcon size="1.25em" /></button>
</div>
</div>
)
}
}
export default LineItem
If I add the value and put weight on the input then the ref stops working. I assume I might be missing something between my handleChange function and the requestWeight function but I really dont know what exaclty
Currently I have implemented it using plain javascript array. But there seems to be few problems.
onChange, onDelete has to iterate all over the array which could be better like O(1)
When some item gets deleted key property of other items get changed. I'm not sure if it's bad or not.
Using immutable.js can get rid of problem 1. Is number 2 really a problem? Is there a better alternative than immutable.js?
Maybe I'm not asking the right questions, what's the best way?
import React, { useState } from "react";
export default function() {
const [rows, setRows] = useState([""]);
const onChange = (e, i) => {
setRows(rows.map((row, index) => (index !== i ? row : e.target.value)));
};
const onDelete = i => {
setRows(rows.filter((_, index) => i !== index));
};
return (
<>
{rows.map((row, index) => {
return (
<div key={index}>
<input value={row} onChange={e => onChange(e, index)} />
{index !== 0 && (
<button onClick={() => onDelete(index)}>
- delete row
</button>
)}
</div>
);
})}
<button onClick={() => setRows(rows.concat([""]))}>
+ add row
</button>
</>
);
}
UPDATE
I tried using immutable-js OrderedMap. Now key property of elements won't change and onChange and onDelete won't iterate over everything. Is this better than before?
import React, { useState } from "react";
import { OrderedMap } from "immutable";
export default function() {
const [inputState, setInputState] = useState({
rows: OrderedMap(),
nextKey: Number.MIN_VALUE,
});
function onChange(k, v) {
setInputState({
...inputState,
rows: inputState.rows.update(k, () => v),
});
}
function addRow() {
const { rows, nextKey } = inputState;
setInputState({
rows: rows.set(nextKey, ""),
nextKey: nextKey + 1,
});
}
function deleteItem(k) {
setInputState({
...inputState,
rows: inputState.rows.delete(k),
});
}
return (
<>
{inputState.rows.entrySeq().map(([k, v]) => {
return (
<div key={k}>
<input
value={v}
onChange={({ target: { value } }) => {
onChange(k, value);
}}
/>
<button onClick={() => deleteItem(k)}>-</button>
</div>
);
})}
<button onClick={addRow}>+ add row</button>
</>
);
}
UPDATE: 2
Also tried using plain javascript Map.
import React, { useState } from "react";
export default function() {
const [inputState, setInputState] = useState({
rows: new Map(),
nextKey: Number.MIN_VALUE,
});
function onChange(k, v) {
const { rows, nextKey } = inputState;
rows.set(k, v);
setInputState({
nextKey,
rows,
});
}
function addRow() {
const { rows, nextKey } = inputState;
rows.set(nextKey, "");
setInputState({
rows,
nextKey: nextKey + 1,
});
}
function deleteItem(k) {
const { rows, nextKey } = inputState;
rows.delete(k);
setInputState({
nextKey,
rows,
});
}
const uiList = [];
for (const [k, v] of inputState.rows.entries()) {
uiList.push(
<div key={k}>
<input
value={v}
onChange={({ target: { value } }) => {
onChange(k, value);
}}
/>
<button onClick={() => deleteItem(k)}>-</button>
</div>
);
}
return (
<>
{uiList}
<button onClick={addRow}>+ add row</button>
</>
);
}
UPDATE 3
Using plain array but with key property.
import React, { useState } from "react";
export default function() {
const [inputState, setInputState] = useState({
rows: [],
nextKey: Number.MIN_VALUE,
});
function onChange(i, v) {
const { rows, nextKey } = inputState;
rows[i].value = v;
setInputState({
nextKey,
rows,
});
}
function addRow() {
const { rows, nextKey } = inputState;
rows.push({ key: nextKey, value: "" });
setInputState({
rows,
nextKey: nextKey + 1,
});
}
function deleteItem(i) {
const { rows, nextKey } = inputState;
rows.splice(i, 1);
setInputState({
nextKey,
rows,
});
}
return (
<>
{inputState.rows.map(({ key, value }, index) => {
return (
<div key={key}>
<input
value={value}
onChange={({ target: { value } }) => {
onChange(index, value);
}}
/>
<button onClick={() => deleteItem(index)}>-</button>
</div>
);
})}
<button onClick={addRow}>+ add row</button>
</>
);
}
Instead of using external js, i just do the little modification of the code, instead of doing the map or filter, you can use directly with splice,
the below code will help to do the deletion fast and it will keep the same index after deletion also.
import React, { useState } from "react";
export default function() {
const [rows, setRows] = useState([""]);
const onChange = (e, i) => {
const {value} = e.target;
setRows(prev => {
prev[i] = value;
return [...prev];
});
};
const onDelete = i => {
setRows(prev => {
prev.splice(i,1, undefined);
return [...prev];
});
};
return (
<>
{rows.map((row, index) => (typeof row !== "undefined") && (
<div key={index}>
<input value={row} onChange={e => onChange(e, index)} />
{index !== 0 && (
<button onClick={() => onDelete(index)}>
- delete row
</button>
)}
</div>
))}
<button onClick={() => setRows(rows.concat([""]))}>
+ add row
</button>
</>
);
}
So far, best choices are
Generate key to use as key.
Use map to have efficient update and delete.
Don't mutate the state.
The immutable library fits the point 2 and 3 perfectly. Since it can update and delete without mutation efficiently it seems like a good choice.
import React, { useState, useEffect } from "react";
import { OrderedMap } from "immutable";
function UsingImmutableJS({ onChange }) {
const [inputState, setInputState] = useState({
rows: OrderedMap(),
nextKey: 0,
});
useEffect(() => {
if (onChange) onChange([...inputState.rows.values()]);
}, [inputState, onChange]);
function onInputChange(k, v) {
setInputState(prev => ({
...prev,
rows: prev.rows.update(k, () => v),
}));
}
function addRow() {
setInputState(prev => ({
nextKey: prev.nextKey + 1,
rows: prev.rows.set(prev.nextKey, ""),
}));
}
function deleteItem(k) {
setInputState(prev => ({
...prev,
rows: prev.rows.delete(k),
}));
}
return (
<>
{inputState.rows.entrySeq().map(([k, v]) => {
return (
<div key={k}>
<input
value={v}
onChange={({ target: { value } }) => {
onInputChange(k, value);
}}
/>
<button onClick={() => deleteItem(k)}>-</button>
</div>
);
})}
<button onClick={addRow}>+ add row</button>
<button onClick={() => console.log([...inputState.rows.values()])}>
print
</button>
</>
);
}
Without index we can do using the Map and Map retrieval is faster, below code you can try.
import React, { useState } from "react";
export default function() {
const [rows, setRows] = useState(new Map());
const onUpdate = (key, value) => {
setRows(prev => new Map(prev).set(key, { key, value}));
}
const onDelete = (key) => {
setRows(prev => new Map(prev).delete(key));
}
return (
<>
{[...rows].map((row, index) => (typeof row[1].value !== "undefined") && (
<div key={index}>
<input value={row[1].value} onChange={e => onUpdate(row[1].key, e.target.value)} />
<button onClick={() => onDelete(row[1].key)}>- delete row</button>
</div>
))}
<button onClick={() => onUpdate(rows.size, "")}>
+ add row
</button>
</>
);
}