Let's say I have a todo list array of objects:
const todos = [
{
id: 1,
task: "Take out Trash",
completed: false,
},
{
id: 2,
task: "Make Dinner",
completed: false,
},
export default todos
How would you go about updating completed property to true in a functional component? I understand the code I have below is merely changing checked to true. I just need to figure out how to interact with the data source.
import ToDoData from "./data/tododata";
const myComp = () => {
const [isChecked, setIsChecked] = useState(false);
const handleChange = () => {
setIsChecked(true)
};
return (
<input
type="checkbox"
checked={isChecked}
onChange={props.handleChange}
/>
)
}
Your question is a bit unclear. if you want to keep the array of todos as state & toggle the completed property of one of its members, then you need to pass an id with you onChange function which would look like this:
const handleChange = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
but the better approach would be mapping over TodoData & returning a Todo component for each like this:
TodoData.map(todo => <Todo todo={todo}/>)
then inside you Todo component you can have:
const Todo = () => {
const [todo, setTodo] = useState(todo)
const handleChange = () => {
setTodos((todo) => ({ ...todo, completed: !todo.completed }));
};
return (
<input type="checkbox" checked={isChecked} onChange={props.handleChange} />
);
};
this way you don't need to map over the whole array everytime one checkbox gets toggled
Related
I'm making it so that every component is one element (button, the whole list, a single element...) I'm having trouble figuring out how to make my list print below the form. Tasks are shown in console.log() but I can't seem to get the right data transferred.
Thanks in advance for any help
This is items.jsx code
import React, { useState} from 'react'
import './todo.css'
import List from './list'
import Button from './button';
function Items () {
const [tasks, setTasks] = useState([]);
const [value, setvalue] = useState("");
/* const onChange = (e) => {
setvalue(e.target.value)
// console.log('type')
} */
const onAddTask = (e) =>{
e.preventDefault();
console.log('submit')
const obj = {
name: value ,
id: Date.now(),
};
if (value !== "") {
setTasks(tasks.concat(obj));
setvalue("")
console.log(obj)
}
};
return(
<div className="form">
<header>Your todo list</header>
<input
placeholder="type your task"
value={value}
onChange={(e) => setvalue(e.target.value)}/>
<input type="date" placeholder='Set your date!'/>
<button onClick={onAddTask}>Submit task</button>
<List data = {List}/>
</div>
)
}
export default Items
This is list.jsx code
import React , { useState } from "react";
import "./Items"
import Button from "./button"
const List = (tasks) => {
return(
<div>
{tasks.map}
</div>
)
console.log(task.map)
}
export default List
step 1
Here's a fully functioning demo to get you started -
function Todo() {
const [items, setItems] = React.useState([])
const [value, setValue] = React.useState("")
const addItem = event =>
setItems([...items, { id: Date.now(), value, done: false }])
return <div>
<List items={items} />
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="button" onClick={addItem}>Add</button>
</div>
}
function List({ items = [] }) {
return <ul>
{items.map(item =>
<ListItem key={item.id} item={item} />
)}
</ul>
}
function ListItem({ item = {} }) {
return <li>{item.value}</li>
}
ReactDOM.render(<Todo />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
start with good state
Note using an Array to store the items is somewhat inefficient for the kinds of operations you will need to perform. Consider using a Map instead. Run the demo again and click on a list item to toggle its state -
const update = (m, key, func) =>
new Map(m).set(key, func(m.get(key)))
function Todo() {
const [items, setItems] = React.useState(new Map)
const [value, setValue] = React.useState("")
const addItem = event => {
const id = Date.now()
setItems(update(items, id, _ => ({ id, value, done: false })))
}
const toggleItem = id => event =>
setItems(update(items, id, item => ({ ...item, done: !item.done })))
return <div>
<List items={items} onClick={toggleItem} />
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="button" onClick={addItem}>Add</button>
</div>
}
function List({ items = new Map, onClick }) {
return <ul>
{Array.from(items.values(), item =>
<ListItem key={item.id} item={item} onClick={onClick(item.id)} />
)}
</ul>
}
function ListItem({ item = {}, onClick }) {
return <li onClick={onClick}>
{ item.done
? <s>{item.value}</s>
: item.value
}
</li>
}
ReactDOM.render(<Todo />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
do more with less
Functional programming goes a long way in React. Using a curried update function we can take advantage of React's functional updates -
const update = (key, func) => m => // <-
new Map(m).set(key, func(m.get(key)))
function Todo() {
// ...
const addItem = event => {
const id = Date.now()
setItems(update(id, _ => ({ id, value, done: false }))) // <-
}
const toggleItem = id => event =>
setItems(update(id, item => ({ ...item, done: !item.done }))) // <-
// ...
}
but don't stop there
Avoid creating the todo item data by hand { id: ..., value: ..., done: ... }. Instead let's make an immutable TodoItem class to represent our data. A class also gives us an appropriate container for functions that would operate on our new data type -
class TodoItem {
constructor(id = 0, value = "", done = false) {
this.id = id
this.value = value
this.done = done
}
toggle() {
return new TodoItem(id, value, !this.done) // <- *new* data
}
}
Now our Todo component is unmistakable with its intentions -
function Todo() {
// ...
const [items, setItems] = useState(new Map)
const addItem = event => {
const id = Date.now()
setItems(update(id, _ => new TodoItem(id, value))) // <- new TodoItem
}
const toggleItem = id => event =>
setItems(update(id, item => item.toggle())) // <- item.toggle
// ...
}
What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.
What I'm trying to do here is to build a dynamic component, that will be responsible to take an array of objects and the form will be built based on my formState. So I made my initial State and used .map to make the loop over the state and mapped the keys to making the label, value, and inputs appear based on the state. but my problem is at onChange. How to update the value key in every object and set the new state for it. any advice, please.
import { useState } from "react";
import InputText from "./components";
import useForm from "./hooks/useForm";
function App() {
interface formStateT {
id: number;
label: string;
value: any;
error: string;
}
const formState = [
{
id: 0,
label: "firstName",
value: "",
error: "",
},
{
id: 1,
label: "lastName",
value: "",
error: "",
},
];
const { form, validate, setForm, checkValidHandler } = useForm(formState);
const [error, setError] = useState("");
const submitFormHandler = (e: { preventDefault: () => void }) => {
e.preventDefault();
checkValidHandler();
// write form logic
// setError() will be used to take the error message
console.log(form);
};
return (
<form onSubmit={(e) => submitFormHandler(e)}>
{form.map((f: formStateT) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={(e) => {
// Im trying here to update the value key of every label key.
// setForm({ ...form, [f.label.valueOf()]: f.value })
}}
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>
);
}
export default App;
From your comment, f.value = e.target.value; is a state mutation and should be avoided, the setForm([...form]); is masking the mutation.
In App create an onChangeHandler function that takes the onChange event object and the index you want to update. Unpack the value from the onChange event and update the state. The handler should use a functional state update to update from the previous state, and create a shallow copy of the form array, using the index to shallow copy and update the correct array element.
Example:
// curried function to close over index in scope, and
// return handler function to consume event object
const onChangeHandler = index => e => {
const { value } = e.target;
setForm(form => form.map((el, i) =>
i === index
? { ...el, value }
: el
));
};
...
<form onSubmit={submitFormHandler}>
{form.map((f: formStateT, index: number) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={onChangeHandler(index)} // <-- invoke and pass mapped index
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>
I have an array of objects in todos state hook. And I want to change a single property if it is completed. Seems I can update it but without using setTodos. I am a React beginner.
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const addTodoHandler = (e) => {
e.preventDefault();
if (input.length < 2) return;
setTodos([...todos, { id: Date.now(), text: input, isComplete: false }]);
setInput("");
};
const removeHandler = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const completeHandler = (id) => {
// working without setTodo()
// errors when added setTodo()
todos.map((todo) =>
todo.id === id ? console.log((todo.isComplete = !todo.isComplete)) : ""
);
};
<div className="todolist">
{todos.map((todo) => (
<Todo
key={todo.id}
id={todo.id}
text={todo.text}
removeHandler={removeHandler}
completeHandler={completeHandler}
isComplete={todo.isComplete}
/>
))}
</div>
To fix this inside completeHandler() first create a new array using map() method and inside map() method update the isComplete for the current todo and simply return the updated value like:
var updatedTodos = todos.map((todo) => todo.id === id ? {
...todo,
isComplete: !todo.isComplete
} : todo);
Then inside setTodos() just return this new updatedTodos array like:
setTodos(updatedTodos);
You can also do this in one-line like:
setTodos(todos.map((todo) => todo.id === id ? { ...todo, isComplete: !todo.isComplete } : todo));
But the previous code provides more readability and also helps in better debugging if you want to check each variable line by line.
I am getting some unexpected results.
Looking at that
import React from 'react';
import PropTypes from 'prop-types';
const propTypes = {
fooList: PropTypes.array
};
const defaultProps = {
fooList: [
{ active: false },
{ active: false }
];
};
const FooBar = ({
fooList
}) => {
const [state, setState] = React.useState(fooList);
const onClick = (entry, index) => {
entry.active = !entry.active;
state[index] = entry;
console.log('#1', state) // <- That loggs the new State properly!
setState(state);
}
console.log('#2', state) // <- That does not log at after clicking on the text, only after the initial render
return state.map((entry, index) => {
return <p
onClick={() => onClick(entry, index)}
key={index}>
{`Entry is: ${entry.active ? 'active' : 'not active'}`}
</p>
})
}
FooBar.defaultProps = defaultProps;
FooBar.propTypes = propTypes;
export default FooBar;
I expect on every click the text in the <p /> Tag to change from Entry is: not active to Entry is: active.
Now, I am not sure if I can simply alter the state like this
state[index] = entry;
Using a class extending React.Component, this wouldn't work. But maybe with React Hooks? And then, I am not sure if I can use hooks in a map().
When you use state[index] = entry;, you are mutating the state but the state reference does not change, and so React will not be able to tell if the state changed, and will not re-render.
You can copy the state before mutating it:
const onClick = (entry, index) => {
entry.active = !entry.active;
const newState = [...state];
newState[index] = entry;
console.log(newState) // <- That loggs the new State properly!
setState(newState);
}
I would also consider maybe changing up your design a little https://stackblitz.com/edit/react-6enuud
rather than handling each individual click out side, if it is just for display purposes, then it can be easier to encapsulate in a new component:
const FooBarDisplay = (entry) => {
const [active, setActive] = useState(entry.active);
const onClick = () => {
setActive(!active);
}
return (<p onClick={() => onClick()}>
{`Entry is: ${active ? 'active' : 'not active'}`}
</p>
)
}
Here you can make handling state easier, and avoid mutating arrays.
Simpler parent:
const FooBar = ({
fooList = [
{ active: false },
{ active: false }
]
}) => fooList.map((entry, i) => <FooBarDisplay key={i} entry={entry} />);
I've just moved default props to actual default argument values.