When I press delete button, it only deletes the last element every time, regardless to index.
How can I do this proper way? Without changing <input defaultValue={name} /> to <input value={name} /> in Child component?, I've done it with <input defaultValue={name} />, but how could I do this with value property to an input?
export const App = () => {
const [names, setNames] = React.useState([
"First",
"Second",
"third",
"fourth",
]);
const onChange = (params) => {
doing(params); // etc.
};
function onDelete(index: number) {
const nameArr = [...names];
nameArr.splice(index, 1);
setNames(nameArr);
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={index}
name={name}
index={index}
onChange={onChange}
onDelete={handleDelete}
/>
))}
</div>
);
};
const ChildComponent = React.memo(({ name, index, onChange, onDelete }) => {
return (
<div>
<input
defaultValue={name}
onChange={(event) => onChange(index, event.target.value)}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
});
You have a typo in your code (onDelete and handleDelete) but this is not the main issue. Your problem is using index as your key. Since you are removing some elements attached to the state, React can't decide how to sort the array again, because index here is not trustable.
You should use some unique values for your keys, if there is not, you should create them somehow. In the below example I try to mix index with the value:
key={`${index}${name}`}
Also, do not use methods like splice since they mutate the state. splice change elements in place, so mutation. I used filter but you can use any other method which doesn't mutate the state.
function App() {
const [names, setNames] = React.useState([
"First",
"Second",
"third",
"fourth"
]);
function onDelete(index) {
setNames((prev) => prev.filter((_, i) => i !== index));
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={`${index}${name}`}
name={name}
index={index}
onDelete={onDelete}
/>
))}
</div>
);
}
const ChildComponent = React.memo(({ name, index, onChange, onDelete }) => {
return (
<div>
<input
defaultValue={name}
onChange={(event) => onChange(index, event.target.value)}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
});
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
Related
I am working on a react where I am dealing with dynamic number of input fields as I am rendering them using arr.map function. But How can I handle the input onChange method with so many input fields?
Here's my component:
this.props.setsList.map((codeset, index) => (
<Table.Row key={codeset.code_system_id}>
<Table.Cell>{index + 1}</Table.Cell>
<Table.Cell>{codeset.name}</Table.Cell>
<Table.Cell>
<Input
type='text'
className='form-control'
value={codeset.code}
placeholder={translateText('Code')}
onChange={() => this.handleCodeChange(event, index)}
style={{ height: '70%' }}
/>
</Table.Cell>
<Table.Cell>
<Input
type='text'
className='form-control'
value={codeset.description}
placeholder={translateText('Code Description')}
onChange={() => this.handleCodeDescriptionChange(event, index)}
style={{ height: '70%' }}
/>
</Table.Cell>
</Table.Row>
All the input fields may have existing fields or may be empty and one can edit it them semd the edit fields to the API. How can I handle such a case with just 1 handleFunction. Is there a way? Any leads will be appreciated.
In the above code, 2 input boxes are there with initial values from the api.
Data passed into your component should go directly into state. Then each field sends the array index, field name and new value to the handleChange callback.
import { useState } from "react";
// passed in as a prop from the parent component
const data = [
{
name: "foo",
code: "gdfgsd"
},
{
name: "bar",
code: "gfdsgsdfgfd"
}
];
const App = ({ setsList = data }) => {
const [state, setState] = useState(setsList);
const handleChange = (e, i) => {
const { value, name } = e.target;
const newState = [...state];
newState[i] = {
...newState[i],
[name]: value
};
console.log(newState);
setState(newState);
};
return (
<div className="App">
{state.map(({ name, code }, index) => {
return (
<div key={index}>
<label>
name
{": "}
<input
name="name"
value={name}
onChange={(e) => handleChange(e, index)}
/>
</label>
<label>
code
{": "}
<input
name="code"
value={code}
onChange={(e) => handleChange(e, index)}
/>
</label>
</div>
);
})}
</div>
);
};
export default App;
There's also a way that you can render each field dynamically by mapping over the array, and then inner mapping over that array item's keys.
import { useState } from "react";
import { data } from "./data";
const App = ({ setsList = data }) => {
const [state, setState] = useState(setsList);
const handleChange = (e, i) => {
const { value, name } = e.target;
const newState = [...state];
newState[i] = {
...newState[i],
[name]: value
};
console.log(newState);
setState(newState);
};
return (
<table className="App">
<tbody>
{state.map((item, index) => (
<tr key={index}>
{Object.keys(item).map((key) => (
<td key={`${index}-${key}`}>
<label>
{key}
{": "}
<input
name={key}
value={item[key]}
onChange={(e) => handleChange(e, index)}
/>
</label>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default App;
I have a dynamic array of form fields, whose values are fetched via REST API. On the page, there is also a dropdown, that, when changed, shows a different array of fields. I fetch all of these fields/values during the componentDidMount life cycle hook and filter the list to show the relevant data.
The Formik docs mention FieldArrays as a means to handle an array of fields. However, their example shows a static list of objects as its initialValues -- but I don't see how dynamically generated lists. In fact, since I'm fetching initialValues via AJAX, it's initially an empty array -- so nothing is rendered even after getting the data.
This is simplified version of my code:
const MyComponent = class extends Component {
componentDidMount() {
// data structure: [{Name: '', Id: '', Foo: '', Bar: ''}, ...]
axios
.get('/user')
.then((res) => {
this.setState({
userData: res.data
});
});
}
render() {
return (
<div>
<Formik
initialValues={{
users: this.state.userData
}}
render={({values}) => (
<Form>
<FieldArray
name="users"
render={arrayHelpers => (
<ul>
{
values.users.map((user, index) => {
return (
<li key={user.Id}>
<div>{user.Name}</div>
<Field name={`user[${index}].Foo`} type="text" defaultValue={user.Foo} />
<Field name={`user[${index}].Bar`} type="text" defaultValue={user.Bar} />
</li>);
}
}
</ul>
)}
/>
</Form>
)}
/>
</div>
);
}
}
You can do this via setting enableReinitialize true. According to doc it will do this:
Default is false. Control whether Formik should reset the form if initialValues changes (using deep equality).
I created complete codesanbox where your incoming data is async and when you push the data its also async. check this:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const InviteFriends = () => {
const [initialValues, setInitialValues] = React.useState({
friends: []
});
useEffect(() => {
const initialValues = {
friends: [
{
name: "",
email: ""
}
]
};
const timer = setTimeout(() => {
setInitialValues(initialValues);
}, 1000);
return () => {
timer && clearTimeout(timer);
};
}, []);
return (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{({ values }) => (
<Form>
<FieldArray name="friends">
{({ insert, remove, push }) => (
<div>
{console.log("Values", values, initialValues)}
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>
Email
</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={async () => {
await new Promise((r) =>
setTimeout(() => {
push({ name: "", email: "" });
r();
}, 500)
);
}}
>
Add Friend
</button>
</div>
)}
</FieldArray>
<button type="submit">Invite</button>
</Form>
)}
</Formik>
</div>
);
};
ReactDOM.render(<InviteFriends />, document.getElementById("root"));
Here is the demo: https://codesandbox.io/s/react-formik-async-l2cc5?file=/index.js
The component loads with 3 todos. If you check the middle one it should get a line through it. Then if you click the [x] button on it, it goes away, but for some reason the todo below it gets checked.
Anyone see the reason for this?
const Todo = props => {
const markCompleted = (checked, index) => {
const newTodos = [...props.todos];
newTodos[index].isCompleted = checked;
props.setTodos(newTodos);
};
const deleteTodo = index => {
const newTodos = [...props.todos];
newTodos.splice(index, 1);
props.setTodos(newTodos);
};
return (
<div
style={{ textDecoration: props.todo.isCompleted ? 'line-through' : '' }}
className="todo"
>
<input
type="checkbox"
onChange={e => markCompleted(e.target.checked, props.index)}
/>
{props.todo.text}
<button onClick={() => deleteTodo(props.index)}>x</button>
</div>
);
};
const TodoForm = props => {
const [value, setValue] = React.useState('');
const addTodo = e => {
e.preventDefault();
if (!value) return;
const newTodos = [...props.todos, { text: value }];
props.setTodos(newTodos);
setValue('');
};
return (
<form onSubmit={addTodo}>
<input
type="text"
className="input"
value={value}
onChange={e => setValue(e.target.value)}
/>
</form>
);
};
const App = () => {
const [todos, setTodos] = React.useState([
{ text: 'Learn about React', isCompleted: false },
{ text: 'Meet friend for lunch', isCompleted: false },
{ text: 'Build really cool todo app', isCompleted: false }
]);
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo {...{ key: index, todo, index, todos, setTodos }} />
))}
<TodoForm {...{ todos, setTodos }} />
</div>
</div>
);
};
ReactDOM.render(
<App />
, document.querySelector('#react'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
The previously checked checkbox remains rendered. You should explicitly set its checked status instead, so that it's taken from the props every time, rather than possibly from user input:
checked={props.todo.isCompleted}
You also should use functional methods like filter instead of using mutating methods like splice, and newTodos[index].isCompleted = checked; is mutating the todo object - [...props.todos] only shallow clones the array of objects. Spread the todos around the changed object into the array passed to setTodos instead.
props.setTodos([
...todos.slice(0, index),
{ ...todos[index], isCompleted: checked },
...todos.slice(index + 1),
]);
const Todo = props => {
const markCompleted = (checked, index) => {
const { todos } = props;
props.setTodos([
...todos.slice(0, index),
{ ...todos[index], isCompleted: checked },
...todos.slice(index + 1),
]);
};
const deleteTodo = index => {
props.setTodos(props.todos.filter((todo, i) => i !== index));
};
return (
<div
style={{ textDecoration: props.todo.isCompleted ? 'line-through' : '' }}
className="todo"
>
<input
type="checkbox"
onChange={e => markCompleted(e.target.checked, props.index)}
checked={props.todo.isCompleted}
/>
{props.todo.text}
<button onClick={() => deleteTodo(props.index)}>x</button>
</div>
);
};
const TodoForm = props => {
const [value, setValue] = React.useState('');
const addTodo = e => {
e.preventDefault();
if (!value) return;
const newTodos = [...props.todos, { text: value }];
props.setTodos(newTodos);
setValue('');
};
return (
<form onSubmit={addTodo}>
<input
type="text"
className="input"
value={value}
onChange={e => setValue(e.target.value)}
/>
</form>
);
};
const App = () => {
const [todos, setTodos] = React.useState([
{ text: 'Learn about React', isCompleted: false },
{ text: 'Meet friend for lunch', isCompleted: false },
{ text: 'Build really cool todo app', isCompleted: false }
]);
return (
<div className="app">
<div className="todo-list">
{todos.map((todo, index) => (
<Todo {...{ key: index, todo, index, todos, setTodos }} />
))}
<TodoForm {...{ todos, setTodos }} />
</div>
</div>
);
};
ReactDOM.render(
<App />
, document.querySelector('#react'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I have trouble debugging this code.
I have an App component:
function App() {
const [state, setState] = useState(0);
const onSelectItem = () => {
console.log("🐞: onSelectItem -> currentState", state);
};
// items is an array of ReactNode: button, when click on it. It will log the currentState.
const items = ["FirstItem", "SecondItem"].map(item => (
<button key={item} onClick={() => onSelectItem()}>
{item}
</button>
);
);
return (
<div className="App">
<Menu items={items} />
<hr />
<button onClick={() => setState(prevState => prevState + 1)}>Change State</button>
</div>
);
}
My Menu components will receive items prop, and render it. It also has ability to set the active item. For simplicity's sake, I render a button to set activeItem to the first one. The active item will also be rendered.
function Menu({ items }) {
const [activeItem, setActiveItem] = useState(items[0]);
return (
<div>
{items}
<hr />
{activeItem}
</div>
);
}
Now, come to the main part:
I press the button (before hr) => it shows currentState (OK)
I press the active button (after hr) => it shows currentState (OK)
I press change state button => the state now changes to 1 (OK)
Now, if I press the button (before hr ) => It shows currentState is 1 (OK)
But, if I press the active button (after hr ) => It still shows 0 (which is the last state) (???)
My guess is React keeps remembering everything when using useState. But I'm not sure. Could anyone explain this for me!
I also include the snippets for you to easily understand my problem.
const {useState} = React;
function Menu({ items }) {
const [activeItem, setActiveItem] = useState(items[0]);
return (
<div>
{items}
<hr />
<span>Active Item:</span>
{activeItem}
</div>
);
}
function App() {
const [state, setState] = useState(0);
console.log(state);
const onSelectItem = () => {
console.log("🐞: onSelectItem -> currentState", state);
};
const items = ["FirstItem", "SecondItem"].map(item => {
return (
<button key={item} onClick={() => onSelectItem()}>
{item}
</button>
);
});
return (
<div className="App">
<Menu items={items} />
<hr />
<button onClick={() => setState(prevState => prevState + 1)}>Change State</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You are trying to access the state from App component in your Menu component.
State is local to that component and can't be accessed outside, if you would like to access the state outside the component you can refer to the useContext hook implementation.
https://reactjs.org/docs/hooks-reference.html#usecontext
Reason you are seeing 0 in the Active state is that is the default value of useState.
You need to pass key to your menu component.
Whenever there is change in props, the component has to re-render with new props.
Refer this artcile from their official docs - https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
Change I made is passing state as key to Menu component
const {useState} = React;
function Menu({ items }) {
const [activeItem, setActiveItem] = useState(items[0]);
return (
<div>
{items}
<hr />
<span>Active Item:</span>
{activeItem}
</div>
);
}
function App() {
const [state, setState] = useState(0);
console.log(state);
const onSelectItem = () => {
console.log("🐞: onSelectItem -> currentState", state);
};
const items = ["FirstItem", "SecondItem"].map(item => {
return (
<button key={item} onClick={() => onSelectItem()}>
{item}
</button>
);
});
return (
<div className="App">
<Menu items={items} key={state}/>
<hr />
<button onClick={() => setState(prevState => prevState + 1)}>Change State</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Here is my Todolist component, which contains a List, and all list items with checkboxes and with material list and checkboxes. Two props are passed: todos and deleteTodo.
const TodoList = ({ todos, deleteTodo}) => {
return (
<List>
{todos.map((todo, index) => (
<ListItem key={index.toString()} dense button>
<Checkbox disableRipple/>
<ListItemText key={index} primary={todo} />
<ListItemSecondaryAction>
<IconButton
aria-label="Delete"
onClick={() => {
deleteTodo(index);
}}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
);
};
I figured out how to use local storage for storing the todos as an array, but have no idea how to store the checkbox values. Can somebody explain, what would be the strategy for that?
And here is the main app:
const initialValue = () => {
const initialArray = localStorage.getItem("todos");
return JSON.parse(initialArray);
};
const [todos, setTodos] = useState(initialValue);
useEffect(() => {
const json = JSON.stringify(todos);
localStorage.setItem("todos", json);
});
return (
<div className="App">
<Typography component="h1" variant="h2">
Todos
</Typography>
<TodoForm
saveTodo={todoText => {
const trimmedText = todoText.trim();
if (trimmedText.length > 0) {
setTodos([...todos, trimmedText]);
}
}}
/>
<TodoList
todos={todos}
deleteTodo={todoIndex => {
const newTodos = todos.filter((_, index) => index !== todoIndex);
setTodos(newTodos);
}}
/>
</div>
);
};
I would appreciate any suggestions or directions, how to tackle this problem. Thx
One approach would be to use the onChange callback of the Checkbox component
e.g. <Checkbox disableRipple onChange={(e)=> onCheckboxChange(e.event.target) /> (and whatever params you need)
and pass it up to your parent component through a prop, e.g.
const TodoList = ({ todos, deleteTodo, onCheckboxChange}) => {
You can then store the value in local storage the parent component.
There may be a more elegant approach