I would like to update the parent state from child component, which renders each object of the array of objects. The main goal of the child component is to update the original value from the array of objects.
I've the following code
Parent:
import { useState } from 'react';
import ExpenseItem from './expenseItem';
function Update({ data }) {
const [ expenses, setExpenses ] = useState(data);
return (
<div>
{expenses.map((expense, index) => {
return <ExpenseItem key={index} {...expense} />;
})}
<button>Save</button>
</div>
);
}
export default Update;
child:
import { useState, useRef } from 'react';
function ExpenseItem({ description, date, credit, debit }) {
const [ edit, setEdit ] = useState(false);
const [ expenseDescription, setExpenseDescription ] = useState(description);
const textInput = useRef();
const renderDefaultView = () => {
return <h3 onDoubleClick={() => setEdit(true)}>{expenseDescription}</h3>;
};
const renderEditView = () => {
return (
<div>
<input
type="text"
ref={textInput}
defaultValue={expenseDescription}
onDoubleClick={() => setEdit(true)}
/>
<button onClick={() => setEdit(false)}>X</button>
<button onClick={() => updateValue()}>OK</button>
</div>
);
};
const updateValue = () => {
const value = textInput.current.value;
setExpenseDescription(value);
textInput.current.defaultValue = value;
setEdit(false);
};
return (
<div>
{edit ? renderEditView() : renderDefaultView()}
<span>{date}</span>
<p>{debit}</p>
<p>{credit}</p>
</div>
);
}
export default ExpenseItem;
Once way, is to pass the parent state property (expenses) and the function that updates it (setExpenses) to the child Component via the props:
Parent:
import React from 'react';
import ReactDOM from 'react-dom';
import { useState } from 'react';
import ExpenseItem from './ExpenseItem';
function Update({ data }) {
const [ expenses, setExpenses ] = useState(data);
return (
<div>
Checking: { expenses[0].description } | { expenses[1].description }
<hr/>
{expenses.map((expense, index) => {
return <ExpenseItem key={index} index={index} expenses={expenses} setExpenses={setExpenses} />;
})}
<button>Save</button>
</div>
);
}
export default Update;
Child:
import React from 'react';
import { useState, useRef } from 'react';
function ExpenseItem( props ) {
let { description, date, credit, debit } = props.expenses[props.index];
const setExpenses = props.setExpenses;
const [ edit, setEdit ] = useState(false);
const [ expenseDescription, setExpenseDescription ] = useState(description);
const textInput = useRef();
const renderDefaultView = () => {
return <h3 onDoubleClick={() => setEdit(true)}>{expenseDescription}</h3>;
};
const renderEditView = () => {
return (
<div>
<input
type="text"
ref={textInput}
defaultValue={expenseDescription}
onDoubleClick={() => setEdit(true)}
/>
<button onClick={() => setEdit(false)}>X</button>
<button onClick={() => updateValue()}>OK</button>
</div>
);
};
const updateValue = () => {
const value = textInput.current.value;
setExpenseDescription(value);
textInput.current.defaultValue = value;
setEdit(false);
const expenses = [ ...props.expenses ]; // Get a copy of the expenses array
// Replace the current expense item
expenses.splice( props.index, 1, {
description: value, date, credit, debit
});
// Update the parent state
setExpenses( expenses );
};
return (
<div>
{edit ? renderEditView() : renderDefaultView()}
<span>{date}</span>
<p>{debit}</p>
<p>{credit}</p>
</div>
);
}
export default ExpenseItem;
Working demo
This can get really complicated as you move along, so the best option is to look for some sort of State Management solution, like using the Context API.
Also, take a look at this interesting post that talks about using the map index value as a key value: Index as a key is an anti-pattern
Related
DonationList.js
import React, { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { auth, db } from "../firebase-config";
import Donation from "./Donation";
const DonationList = () => {
const [donations, setDonations] = useState([]);
//loadDonations
const loadDonations = async () => {
const donationsCollectionRef = collection(db, "donations");
const querySnapshot = await getDocs(donationsCollectionRef);
querySnapshot.forEach(async (donationSnap) => {
const items = [];
const itemsCollectionRef = collection(db, "donations", donationSnap.id, "items");
const itemquerySnapshot = await getDocs(itemsCollectionRef);
itemquerySnapshot.forEach((itemSnap) => {
items.push([
{
itemType: itemSnap.data().itemType,
quantity: itemSnap.data().quantity,
},
]);
});
setDonations((prevDonation) => {
return [
...prevDonation,
{
type: donationSnap.data().type,
donor: donationSnap.data().donor,
requestor: donationSnap.data().requestor,
items: items,
},
];
});
items = [];
});
};
useEffect(() => {
loadDonations();
}, []);
return (
<div className="grid">
<div className="col-12">
<div className="card">
<h5>Donation List</h5>
{donations.map((donation) => (
<Donation donation={donation} />
))}
</div>
</div>
</div>
);
};
const comparisonFn = function (prevProps, nextProps) {
return prevProps.location.pathname === nextProps.location.pathname;
};
export default React.memo(DonationList, comparisonFn);
Donation.js
import React, { useState } from "react";
const Donation = (props) => {
return (
<div className="card">
<h5>Donation Type</h5>
{props.donation.type}
<h5>Requestor</h5>
{props.donation.requestor}
<h5>Donor</h5>
{props.donation.donor}
<h5>Items</h5>
{props.donation.items.map((item) => (
<Item item={item} />
))}
</div>
);
};
const Item = (props) => {
console.log(props.item)
return (
<React.Fragment>
ItemType: {props.item.itemType}
Quantity: {props.item.quantity}
</React.Fragment>
);
};
export default Donation;
These two code snippets is me trying to take data from firebase and display it.
When I console.log(props.item) in the Item component function, the item seems to be an array.
screenshot of the log
Shouldn't it be an object like props.donation?
Its my first time using reactJS so I can't seem to find the problem
Check this block in your code.
const items = [];
itemquerySnapshot.forEach((itemSnap) => {
items.push([
{
itemType: itemSnap.data().itemType,
quantity: itemSnap.data().quantity,
},
]);
});
You have already defined an array and added the item as an array. That is why you are accessing item in Donations; it's returning as an array type.
{props.donation.items.map((item) => (
<Item item={item} />
))}
Change your loadDonations method like this, and it will work correctly.
itemquerySnapshot.forEach((itemSnap) => {
items.push(
{
itemType: itemSnap.data().itemType,
quantity: itemSnap.data().quantity,
},
);
})
My purpose is Making to-do list using React.
App.js
import React ,{useState,useRef} from "react"
import CreateTask from "./CreateTask.js"
import TaskList from "./TaskList.js"
function App() {
const [inputs, setInputs] = useState({ });
const { taskname } = inputs;
const onChange = (e) => { };
const [tasks, setTasks] = useState([ ]);
const nextId = useRef(); // nextId.current
const onCreate = () => { };
const onRemove = (id) => {
setTasks(tasks.filter((user) => user.id !== id));
};
return (
<>
<CreateTask task={taskname} onChange={onChange} onCreate={onCreate} />
<TaskList tasks={tasks} onRemove={onRemove} />
</>
);
}
export default App;
Without editing any code under 'onRemove'function, I'd like to get this function inside TakeList.js.
CreateTask.js
import React from "react";
function CreateTask({task,onChange,onCreate}){
return(
<div>
<input task = "task" value={task} placeholder="Todo"/>
<button onClick = {onCreate}>Register</button>
</div>
);
}
export default CreateTask;
I couldn't finish writing CreateTask.js.
But rather than writing CreateTask.js, I want to finish TaskList.js earlier.
TaskList.js
import React from "react";
function Task({task}){
return <div>
<b>{task.taskname}</b><button onClick = {onRemove}>Remove</button>
</div>
}
function TaskList(){
const tasks = [
{
id:1,
taskname: "Homework",
},
{
id:2,
taskname: "Workout",
},
{
id:3,
taskname: "Clean",
}
]
return (
<div>
{tasks.map((task, index) => (
<Task task = {task} key={index}/>
))}
</div>
);
}
export default TaskList;
You have to extract tasks and onRemove function from props in TaskList.js .
And then you can pass id of the task in onRemove function in TaskList.js .
import React from "react";
function Task({task, removeTask}){
return <div>
<b>{task.taskname}</b><button onClick = {() => {removeTask(task.id)}}>Remove</button>
</div>
}
function TaskList({tasks, onRemove}){
return (
<div>
{tasks.map((task, index) => (
<Task task = {task} removeTask={onRemove} key={index}/>
))}
</div>
);
}
export default TaskList;
I'm trying to edit an input value in a child component and send to the parent
:
https://codesandbox.io/s/sleepy-rain-skoss?file=/src/Editlabel.js:0-389
Parent:
import "./styles.css";
import EditLabel from "./Editlabel";
import { useEffect, useState } from "react";
export default function App() {
const [newName, setNewName] = useState();
useEffect(() => {
console.log("newName", newName);
}, [newName]);
return (
<div className="App">
<EditLabel
value={"hello"}
click={(changedName) => {
setNewName(changedName);
}}
/>
</div>
);
}
Child:
import React, { useState } from "react";
const EditLabel = ({ value, click }) => {
const [name, setName] = useState(value);
return (
<>
<input type={"text"} placeholder={name}></input>
<button
onClick={(e) => {
setName(e.target.value);
click(name);
}}
>
Edit
</button>
</>
);
};
export default EditLabel;
However, the console logs "hello" and then it just logs empty strings.
How can I make it work?
try this on your child's input box
<input type={"text"} placeholder={name} onChange={(e) => setName(e.target.value)}>
Change EditLabel to use a ref to capture the input value:
const EditLabel = ({ value, click }) => {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} type={"text"} placeholder={value}></input>
<button
onClick={() => {
click(inputRef.current.value);
}}
>
Edit
</button>
</>
);
};
Update App to use the values it gets via the click callback:
export default function App() {
const [newName, setNewName] = useState("hello");
useEffect(() => {
console.log("newName", newName);
}, [newName]);
return (
<div className="App">
<EditLabel
value={newName}
click={(changedName) => {
setNewName(changedName);
}}
/>
</div>
);
}
I'm following a tutorial to make a React todo app.
I have components and contexts files.
I have addItem function but when I clicked 'Add todo' button,
the item and date is not rendering into todo list.
Also, it shows an error as Warning: Each child in a list should have a unique "key" prop. even though
I have given an id.
Since I am following the tutorial, I don't know where I did wrong.
Would be appreciated if anyone could tell what is wrong.
App.js
import React from 'react';
import Navbar from './components/Navbar';
import Form from './components/Form';
import TodoList from './components/TodoList';
import TodoContextProvider from './contexts/TodoContexts';
function App() {
return (
<div className="App">
<TodoContextProvider>
<Navbar />
<TodoList />
<Form />
</TodoContextProvider>
</div>
);
}
export default App;
TodoContexts.js
import React, { createContext, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
export const TodoContext = createContext();
const TodoContextProvider = (props) => {
const [items, setItems] = useState([
{items: 'laundry', date: '2020-11-18', id: 1},
{items: 'lunch', date: '2020-11-20', id: 2}
]);
const addItems = (items, date) => {
setItems([...items, {items, date, id: uuidv4()}]);
};
const removeItems = (id) => {
setItems(items.filter(item => item.id !== id));
};
return (
<TodoContext.Provider value={{ items, addItems, removeItems }}>
{props.children}
</TodoContext.Provider>
)
}
export default TodoContextProvider
TodoList.js
import React, { useContext } from 'react';
import TodoDetails from './TodoDetails';
import { TodoContext } from '../contexts/TodoContexts';
const TodoList = () => {
const { items } = useContext(TodoContext);
return items.length ? (
<div className="todo-list">
<ul>
{items.map(item => {
return ( <TodoDetails item={item} key={item.id} /> )
})}
</ul>
</div>
) : (
<div className="empty">You have no todos at the moment.</div>
)
}
export default TodoList
TodoDetails.js
import React, { useContext } from 'react';
import { TodoContext } from '../contexts/TodoContexts';
const TodoDetails = ({ item }) => { //TodoList item is props
const { removeItems } = useContext(TodoContext);
return (
<li onClick={() => removeItems(item.id)}>
<div className="items">{item.items}</div>
<div className="date">{item.date}</div>
</li>
)
}
export default TodoDetails
Form.js
import React, { useState, useContext } from 'react';
import './Form.css';
import { TodoContext } from '../contexts/TodoContexts';
const Form = () => {
const {addItems} = useContext(TodoContext);
const [items, setItems] = useState('');
const [date, setDate] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log(items, date);
addItems(items, date);
setItems('');
setDate('');
}
return (
<form className="form" onSubmit={handleSubmit}>
<input
type="text"
value={items}
placeholder="Enter todo"
onChange={(e) => setItems(e.target.value)}
/>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<input type="submit" value="Add todo"/>
</form>
)
}
export default Form
Navbar.js
import React, { useContext } from 'react';
import { TodoContext } from '../contexts/TodoContexts';
const Navbar = () => {
const { items } = useContext(TodoContext);
return (
<div>
<h1>Todo List</h1>
<p>Currently you have {items.length} todos to get through...</p>
</div>
)
}
export default Navbar
Your error may be attributable to using same variable name of 'items' in addItems function:
Try changing the name of first argument to 'item' instead.
const addItems = (item, date) => {
setItems([...items, {item, date, id: uuidv4()}]);
};
I really like react context, but I think it's missing something (or maybe I don't know how to do it)
Say I have a list of todos and it's corresponding provider as
const Home = () => (
<div className="container">
<TodosProvider>
<TodosList />
</TodosProvider>
</div>
)
const TodosList = () => {
const { todos } = useTodos();
return (
<>
{todos.map((todo, idx) => (
<SingleTodo />
))}
</>
)
}
And in another file
import { createContext, useContext, useState } from "react";
const TodosContext = createContext({});
export const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([{ text: 'a' }, { text: 'b' }, { text: 'c' }])
return (
<TodosContext.Provider value={{ todos }}>
{children}
</TodosContext.Provider>
)
}
export const useTodos = () => {
const todos = useContext(TodosContext)
return todos
}
How can I update a single todo inside the SingleTodo without:
1) Passing the map idx as a property to the SingleTodo and then from SingleTodo call a method of the TodosList provider passing the idx as a parameter
2) Giving an artificial id property to the todo. And then in TodosProvider update the todo that matches with that id.
The reasons for those restrictions are that:
1) Passing down the position of the todo in the rendering as a prop, invalidates the benefits of using context, which is to not have to do prop drilling
2) I don't think it's good to pollute the model with an artificial id just for state management.
I'd like to be able to create a SingleTodoContext and instantiate a SingleTodoProvider in each iteration of the loop
const TodosList = () => {
const { todos } = useTodos();
return (
<>
{todos.map((todo, idx) => (
<SingleTodoProvider key={idx} loadFrom={todo}>
<SingleTodo />
</SingleTodoProvider>
))}
</>
)
}
But that doesn't work because the provider would then need to store the loadFrom property as a state, and that would break the sync between the list todo, and the single todo.
So, how do I update a single item inside a list without prop drilling the position of the item in the list? I don't want to use Redux
You can pass methods for updating the values in context as part of your context. Here is an example based on your code (sort of all crammed together):
import React from "react";
import "./styles.css";
import { createContext, useContext, useState } from "react";
const TodosContext = createContext({});
export const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([
{ text: "a" },
{ text: "b" },
{ text: "c" }
]);
const selectTodo = (todo, idx) => {
console.log(
"do something with the todo here and then call setTodos, or something else?",
todo.text,
idx
);
// setTodos(prev => /* Do something here to update the list */)
};
return (
<TodosContext.Provider value={{ selectTodo, todos }}>
{children}
</TodosContext.Provider>
);
};
export const useTodos = () => {
const todos = useContext(TodosContext);
return todos;
};
const Home = () => (
<div className="container">
<TodosProvider>
<TodosList />
</TodosProvider>
</div>
);
const SingleTodo = ({ todo, onClick }) => (
<div>
{todo.text} <button onClick={() => onClick(todo)}>Click Me!</button>
</div>
);
const TodosList = () => {
const { selectTodo, todos } = useTodos();
return todos.map((todo, idx) => (
<SingleTodo onClick={todo => selectTodo(todo, idx)} todo={todo} key={idx} />
));
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Home />
</div>
);
}
Hope that helps!