I'm working on a small todo app as an exercise using React. I have a mock service like this:
export default class TodoService {
constructor(todos) {
this.todos = new Map();
todos.forEach(todo => {
this.todos.set(todo.id, todo);
});
}
findAll() {
return Array.from(this.todos.values());
}
saveTodo(todo) {
this.todos[todo.id] = todo
}
completeTodo(todo) {
this.todos.delete(todo.id)
}
}
and in my React app I have some state which contains the todos:
const [todos, setTodos] = useState([]);
const [flip, flipper] = useState(true);
const completeTodo = (todo) => {
todoService.completeTodo(todo);
flipper(!flip);
}
useEffect(() => {
setTodos(todoService.findAll());
}, [flip])
completeTodo is a function which I pass into my Todo component to be used when I want to complete a todo like this:
import React from "react";
const Todo = ({ todo, completeFn }) => {
return (
<form className="todo">
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value=""
name={todo.id}
id={todo.id}
onClick={() => {
console.log(`completing todo...`)
completeFn(todo)
}} />
<label className="form-check-label" htmlFor={todo.id}>
{todo.description}
</label>
</div>
</form>
)
}
export default Todo
So what happens is that whenever the user clicks the checkbox completeFn is called with the todo, it gets deleted from the service object and the state is supposed to update, but the weirdest thing happens.
When TodoService.completeTodo() is called the todo gets deleted properly, but when findAll() is called the old todo is still there! If I write the contents to the console I can see the item being deleted then somehow teleported back when I call findAll. Why does this happen? I this because of some React magic I don't understand?
Edit: What's even more insane is that if I modify this to only use effects for the initial loading like this:
const [todos, setTodos] = useState([]);
const completeTodo = (todo) => {
todoService.completeTodo(todo);
setTodos(todoService.findAll());
}
useEffect(() => {
setTodos(todoService.findAll());
}, [])
I get a super weird result:
Can someone explain this to me?
Edit2: This is a complete reproducible example (without the index.html with a <div id="root"></div> in it).
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Todo = ({ todo, completeFn }) => {
return (
<div>
<input
type="checkbox"
name={todo.id}
id={todo.id}
onClick={() => {
console.log(`completing todo...`)
completeFn(todo)
}} />
<label className="form-check-label" htmlFor={todo.id}>
{todo.description}
</label>
</div>
)
}
class TodoService {
constructor(todos) {
this.todos = new Map();
todos.forEach(todo => {
this.todos.set(todo.id, todo);
});
}
findAll() {
return Array.from(this.todos.values());
}
saveTodo(todo) {
this.todos[todo.id] = todo
}
completeTodo(todo) {
this.todos.delete(todo.id)
}
}
const App = () => {
let todoService = new TodoService([{
id: 1,
description: "Let's go home."
}, {
id: 2,
description: "Take down the trash"
}, {
id: 3,
description: "Play games"
}]);
const [todos, setTodos] = useState([]);
const [flip, flipper] = useState(true);
const completeTodo = (todo) => {
todoService.completeTodo(todo);
flipper(!flip);
}
useEffect(() => {
setTodos(todoService.findAll());
}, [flip])
return (
<div>
{todos.map(todo => <Todo key={todo.id} todo={todo} completeFn={completeTodo} />)}
</div>
)
};
ReactDOM.render(<App />, document.getElementById("root"));
You don't need to call useEffectin this scenario. You've put a dependency in the useEffect which is fine to use it to stop infinite loop. but it's unnecessary here. You're not really doing any fetch
You can update your code to be like this.
import React, { useState, useCallback, useEffect } from "react";
const Todo = ({ todo, completeFn }) => {
const handleOnclick = useCallback(() => {
// useCallback since function is passed down from parent
console.log(`completing todo...`);
completeFn(todo);
}, [completeFn, todo]);
return (
<div>
<input
type="checkbox"
name={todo.id}
id={todo.id}
onClick={handleOnclick}
/>
<label className="form-check-label" htmlFor={todo.id}>
{todo.description}
</label>
</div>
);
};
class TodoService {
constructor(todos) {
this.todos = new Map();
todos.forEach(todo => {
this.todos.set(todo.id, todo);
});
}
findAll() {
console.log(Array.from(this.todos.values()));
return Array.from(this.todos.values());
}
saveTodo(todo) {
this.todos[todo.id] = todo;
}
completeTodo(todo) {
this.todos.delete(todo.id);
}
}
const todoService = new TodoService([
{
id: 1,
description: "Let's go home."
},
{
id: 2,
description: "Take down the trash"
},
{
id: 3,
description: "Play games"
}
]);
export default function App() {
const [todos, setTodos] = useState([]); // Set initial state
const completeTodo = todo => {
todoService.completeTodo(todo);
setTodos(todoService.findAll()); // Update state
};
useEffect(() => {
setTodos(todoService.findAll());
}, []); // Get and update from service on first render only
return (
<div>
{todos.map(todo => (
<Todo key={todo.id} todo={todo} completeFn={completeTodo} />
))}
</div>
);
}
working example
https://codesandbox.io/s/cranky-hertz-sewc5?file=/src/App.js
Related
I have an app that allows user take and delete notes and I'm trying to implement the edit function. The problem is that I don't want to take the user to a different page, I was the note they click on to change into a form or some kind of editable space (prepopulated with the current content of the note) that they can then save so that it renders again but with the updated values (Google Keep style).
The things that I'm struggling with is how to change just one note since the notes are mapped to components through an array - I've tried using the filter() method and playing with ternary operators, but I either copy the entire array or nothing happens. I came across this question: Google Keep edit functionality, but I'm struggling to understand what's happening and how to adapt it in my code.
So the main question is: how can I change one component without disrupting the other elements in the array or their positions on the page?
Here's my Note component so far:
import React, { useState, useEffect} from "react";
import axios from "axios";
function Note(props) {
const [noteToEdit, setNoteToEdit] = useState({
title: "",
content: "",
category: ''
})
const [isEditNote, setEditNote] = useState(false)
const [idToEdit, setIdToEdit] = useState('')
function deleteNote(id) {
axios.delete(`http://localhost:5000/notes/${id}`)
.then(() => { console.log("Note successfully deleted")
props.setFetch(true)
});
}
function editNote(id, title, content, category){
setEditNote(true)
setNoteToEdit(prevNote => {
return {
title : title,
content : content,
category : category
};
});
console.log("Current note to edit after useState:")
setIdToEdit(id)
console.log(noteToEdit)
}
return (
<div>
{isEditNote && <h1>want to edit: {idToEdit}</h1>}
{!isEditNote &&
<div>
{props.notes.map((noteItem) => {
return (
<div className="note">
<h1>{noteItem.title}</h1>
<p>{noteItem.content}</p>
<button onClick={() => {editNote(noteItem._id, noteItem.title, noteItem.category, noteItem.content)}}>
Edit
</button>
<button onClick={() => {deleteNote(noteItem._id)}}>
Delete
</button>
<p>{noteItem.category}</p>
</div>
);
})}
</div>
}
</div>
)
}
export default Note
and my CreateArea component:
import React, { useState, useEffect } from "react";
import Header from "./Header";
import Footer from "./Footer";
import ListCategories from "./ListCategories";
import CreateCategory from "./CreateCategory";
import Note from "./Note";
import axios from "axios"
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
function CreateArea() {
const [isExpanded, setExpanded] = useState(false);
const [categories, setCategories] = useState([])
const [notes, setNotes] = useState([])
const [fetchB, setFetch] = useState(true)
const [fetchCategories, setFetchCategories] = useState(true)
const [noteToEdit, setNoteToEdit] = useState({
title: "",
content: "",
category: ''
})
const [ieditNote, setEditNote] = useState(false)
const [note, setNote] = useState({
title: "",
content: "",
category: ''
});
useEffect(() => {
if(fetchCategories){
fetch('http://localhost:5000/categories')
.then(res => res.json())
.then(json => {
setCategories(json)
setFetchCategories(false)
})
}
}, [fetchCategories])
useEffect(() => {
if(fetchB) {
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
console.log(json)
setNotes(json)
setFetch(false)
})
}
}, [fetchB])
function handleChange(event) {
const { name, value } = event.target;
setNote(prevNote => {
return {
...prevNote,
[name]: value
};
});
}
function submitNote(e){
e.preventDefault();
axios.post("http://localhost:5000/notes/add-note", note)
.then((res) => {
setNote({
category: '',
title: "",
content: ""
})
setFetch(true)
console.log("Note added successfully");
console.log(note)
})
.catch((err) => {
console.log("Error couldn't create Note");
console.log(err.message);
});
}
function expand() {
setExpanded(true);
}
function filterNotes(category){
fetch('http://localhost:5000/notes')
.then(res => res.json())
.then(json => {
const filtered = json.filter((noteItem) => (noteItem.category === category));
setNotes(filtered);
})
}
function editNote(id, title, content, category){
setNoteToEdit(prevNote => {
return { ...prevNote };
});
console.log("Current note to edit after useState:")
console.log(noteToEdit)
}
return (
<div>
<Header/>
<div className="categories">
<CreateCategory setFetchCategories={setFetchCategories}/>
<button className="all-button" onClick={()=>{setFetch(true)}}>All</button>
<ListCategories categories={categories} notes={notes} filterNotes={filterNotes} setFetch={setFetch}/>
</div>
<div className="notes-container">
<form className="create-note">
{isExpanded && (
<input
name="title"
onChange={handleChange}
value={note.title}
placeholder="Title"
/>
)}
<textarea
name="content"
onClick={expand}
onChange={handleChange}
value={note.content}
placeholder="Take a note..."
rows={isExpanded ? 3 : 1}
/>
<select
name="category"
onChange={handleChange}
value={note.category}>
{
categories.map(function(cat) {
return <option
key={cat.category} value={cat.value} > {cat.category} </option>;
})
}
</select>
<button onClick={submitNote}>Add</button>
</form>
<div className="notes-group">
<Note notes={notes} setFetch={setFetch} editNote={editNote} setEditNote={setEditNote}/>
</div>
</div>
<Footer/>
</div>
);
}
export default CreateArea;
Would appreciate any guidance on this, thanks!
Hi I am creating an app where a user can search for a book and put it on a shelf depending on which shelf the user clicks on. Currently the user can type a query and many results can get displayed. The user can open a dropdown on a book and click on a shelf (in the dropdown) to select a shelf for that book.
I want to call a method that will update the shelf of a book. It works only if the shelfType is hardcoded however (shelfTypes are 'wantToRead', 'read', 'currentlyReading'). What I want to happen is that the user clicks on a shelf and that shelf is set as the local state variable shelfType in SearchPage. Then once the shelfType changes, the method to update the shelf of a book will run (it makes an API call to a backend).
But for some strange reason I can only update the shelf if I hardcode the shelf type into the update method, not when I use the value of the state shelfType. What am I doing wrong? I hope this question makes sense.
SearchPage.js
import React, { useEffect, useState } from 'react';
import { BsArrowLeftShort } from 'react-icons/bs';
import SearchBar from '../components/SearchBar';
import { search, update, getAll } from '../api/BooksAPI';
import Book from '../components/Book';
const SearchPage = () => {
const [query, setQuery] = useState('');
const [data, setData] = useState([]);
const handleChange = (e) => {
setQuery(e.target.value);
};
useEffect(() => {
const bookSearch = setTimeout(() => {
if (query.length > 0) {
search(query).then((res) => {
if (res.length > 0) {
setData(res);
} else setData([]);
});
} else {
setData([]); // make sure data is not undefined
}
}, 1000);
return () => clearTimeout(bookSearch);
}, [query]);
const [shelfType, setShelfType] = useState('None');
const [currentBook, setCurrentBook] = useState({});
const doSomethingWithBookAndShelf = (book, shelf) => {
setShelfType(shelf);
setCurrentBook(book);
};
useEffect(() => {
//following line doesn't update like this, but I want it to work like this
update(currentBook, shelfType).then((res) => console.log(res));
// update works if I run update(currentBook, 'wantToRead').then((res) => console.log(res));
getAll().then((res) => console.log(res));
}, [shelfType]);
return (
<div>
<SearchBar
type="text"
searchValue={query}
placeholder="Search for a book"
icon={<BsArrowLeftShort />}
handleChange={handleChange}
/>
<div className="book-list">
{data !== []
? data.map((book) => (
<Book
book={book}
key={book.id}
doSomethingWithBookAndShelf={doSomethingWithBookAndShelf}
/>
))
: 'ok'}
</div>
</div>
);
};
export default SearchPage;
Book.js
import React from 'react';
import PropTypes from 'prop-types';
import ButtonDropDown from './ButtonDropDown';
const Book = ({ book, doSomethingWithBookAndShelf }) => {
return (
<div className="book">
<img
src={book.imageLinks.thumbnail}
alt={book.title}
className="book-thumbnail"
/>
<ButtonDropDown
choices={['Currently Reading', 'Want to Read', 'Read', 'None']}
onSelectChoice={(choice) => {
// book came from the component props
doSomethingWithBookAndShelf(book, choice);
}}
/>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
);
};
Book.propTypes = {
doSomethingWithBookAndShelf: PropTypes.func.isRequired,
book: PropTypes.shape({
imageLinks: PropTypes.shape({
thumbnail: PropTypes.string.isRequired,
}),
title: PropTypes.string.isRequired,
authors: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
};
export default Book;
ButtonDropDown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { BsFillCaretDownFill } from 'react-icons/bs';
const ButtonDropDown = ({ choices, label, onSelectChoice }) => {
const [active, setActive] = useState(false);
const toggleClass = () => {
setActive(!active);
};
return (
<div className="dropdown">
<button
type="button"
className="dropbtn"
onFocus={toggleClass}
onBlur={toggleClass}
>
<BsFillCaretDownFill />
</button>
<div
id="myDropdown"
className={`dropdown-content ${active ? `show` : `hide`}`}
>
<div className="dropdown-label">{label}</div>
{choices.map((choice, index) => (
<button
// eslint-disable-next-line react/no-array-index-key
key={index}
className="dropdown-choice"
onClick={() => {
// we create an specific callback for each item
onSelectChoice(choice);
}}
type="button"
value={choice}
>
{choice}
</button>
))}
</div>
</div>
);
};
ButtonDropDown.propTypes = {
choices: PropTypes.arrayOf(PropTypes.string).isRequired,
label: PropTypes.string,
onSelectChoice: PropTypes.func.isRequired,
};
ButtonDropDown.defaultProps = {
label: 'Move to...',
};
export default ButtonDropDown;
Cause you're "Want to Read" text in choices is different
choices={['Currently Reading', *'Want to Read'*, 'Read', 'None']}
Based on this // update works if I run update(currentBook, 'wantToRead').then((res) => console.log(res));
"wanToRead" is not equal to "Want to Read"
I am trying to create my first Todo list with React.js. I am trying to change the state from
const [todos, setTodos] = useState([])
To:
const [todos, setTodos] = useState({
todo: [],
isCompleted: false,
})
Just to try and add in a isCompleted state. However, when I change it, I get an error when running my application from a previously working map. The error is in the title.
Could somebody tell me what is wrong?
Code:
TodosApp.js
import React, { useState } from "react"
import Todos from "./Todos"
const TodoApp = () => {
const [todos, setTodos] = useState({
todo: [],
isCompleted: false,
})
const [input, setInput] = useState("")
const handleCurrentInput = (e) => {
setInput(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(input)
setInput("")
setTodos({
...todos,
task: input,
isCompleted: false,
})
}
const handleDelete = ({ index }) => {
const newTodos = [...todos]
newTodos.splice(index, 1)
setTodos(newTodos)
}
return (
<div id="todoForm">
<div class="container">
<div class="todo_form">
<div class="todo_input">
<form onSubmit={handleSubmit}>
<input
type="text"
id="input_todo"
onChange={handleCurrentInput}
value={input}
/>
</form>
<Todos todos={todos} handleDelete={handleDelete} />
</div>
</div>
</div>
</div>
)
}
export default TodoApp
Todos.js
import React, { useState } from "react"
const Todos = (props) => {
return (
<ul>
{props.todos.map((todo, index) => {
return (
<li key={todo}>
{todo}
<button onClick={() => props.handleDelete({ index })}>
Delete
</button>
</li>
)
})}
</ul>
)
}
export default Todos
You need to focus on each todo item including 2 props task, isCompleted instead of isCompleted of todos.
const [todos, setTodos] = useState([]);
var newTodo = {
task: 'React JS',
isCompleted: false
};
setTodos([...todos, newTodo]);
Then your todos's structure like below:
[
{
task: 'Study React JS',
isCompleted: false
},
{
task: 'Study React Redux',
isCompleted: false
},
];
Your state is an object containing an array of todos. This is what you're passing to your Todos component.
So you have two options:
Either pass todos.todos as a prop or
(Better way) Rethink your state. isCompleted seems that it should be part of each todo, because each todo should be completed not the list itself. A list is completed if every todo isCompleted
So your state would be const [todos, setTodos] = useState([])
I hope it's clear what I mean. Typing this from the phone is not so easy :-)
It's because you don't set to state the right way, todos got overwritten with the wrong value. You should write:
// handleSubmit
setTodos(s => {
...s,
task: input,
isCompleted: false,
});
and
// handleDelete
const newTodos = [...todos]
newTodos.splice(index, 1)
setTodos(s => ({ ...s, todos: newTodos }))
Working App: Stackblitz
import React, { useState, useEffect } from "react";
const TodoApp = () => {
/* initialize todos with array
instead of an object 👇*/
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const handleCurrentInput = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
console.log(input);
/* update the state by appending an object having
key todo and isCompleted to copy of our main state,
todos.👇
*/
setTodos([...todos, { todo: input, isCompleted: false }]);
setInput("");
};
const handleDelete = ({ index }) => {
const newTodos = [...todos];
newTodos.splice(index, 1);
setTodos(newTodos);
};
useEffect(() => {
console.log(JSON.stringify(todos));
}, [todos]);
return (
<div id="todoForm">
<div class="container">
<div class="todo_form">
<div class="todo_input">
<form onSubmit={handleSubmit}>
<input
type="text"
id="input_todo"
onChange={handleCurrentInput}
value={input}
/>
</form>
<Todos todos={todos} handleDelete={handleDelete} />
</div>
</div>
</div>
</div>
);
};
export default TodoApp;
const Todos = props => {
return (
<>
<ul>
{props.todos.map((todo, index) => {
return (
<li key={index}>
{/**use null propogation to avoid accessing the null todo value which will not exist in first render. */}
{todo?.todo}
<button onClick={() => props.handleDelete({ index })}>
Delete
</button>
</li>
);
})}
</ul>
</>
);
};
isCompleted should be associated with each todo item.
So, you should use todos as array and store objects within that array. Each object will have isCompleted and a task property along with a unique Id as well.
const [todos, setTodos] = useState([]);
And your submit input would look like:
const handleSubmit = (e) => {
e.preventDefault();
const todo = {
task: input,
id: new Date().getTime().toString(),
isCompleted: false
};
const updatedTodos = [...todos, todo];
setTodos(updatedTodos);
console.log(updatedTodos);
setInput("");
};
Note: To generate unique Ids you can use uuid library. I have generated unique ids here using id: new Date().getTime().toString().
FULL WORKING CODE SANDBOX LINK: https://codesandbox.io/s/todosissue-2mc26?file=/src/TodoApp.js
Have modified handleDelete function as well :)
I noticed this strange behavior of my App, that when I do anything on it (write something in the search field, create a new list, etc) my page gets rerendererd. Of course, I cannot find the source of it.
Below is the the look of my page, when it is loaded the first time, with default (blank) search results.
And now, the result in profiler, when I type something in the searchBar (or create a new list, or anything):
Here is my code of the App.js
import React, { useState, createContext, useEffect } from "react";
import NavBar from "../NavBar/NavBar";
import youtube from "../../apis/youtube";
import VideoList from "../VideoList/VideoList";
import VideoDetail from "../VideoDetail/VideoDetail";
import SideBar from "../SideBar/SideBar";
import "./App.css";
export const VideoContext = createContext();
export const FavoriteContext = createContext();
const API_KEY = process.env.REACT_APP_API_KEY;
const App = () => {
const [ videos, setVideos ] = useState([]);
const [ searchedValue, setSearchedValue ] = useState({
selectedVideo: null
});
const handleSelectedVideo = (singleRenderedVideo) => {
setSearchedValue((previous) => ({
...previous,
selectedVideo: singleRenderedVideo
}));
};
const handleSearch = async (inputText) => {
const response = await youtube.get("/search", {
params: {
q: inputText,
part: "snippet",
type: "video",
maxResults: 16,
key: API_KEY
}
});
setVideos(response.data.items);
setSearchedValue({
selectedVideo: response.data.items[0] //take the first search result and make it appear as a playable one
});
};
useEffect(() => {
handleSearch();
}, []);
//By the user newly created lists
const [ lists, setLists ] = useState([]);
const addList = (newList) => {
setLists((prevLists) => {
return [ ...prevLists, newList ];
});
};
const onDeleteList = (id) => {
setLists((prevLists) => {
return prevLists.filter((listItem, index) => {
return index !== id;
});
});
};
//Render(Play) Favorited Video
const [ favoritedItem, setFavoritedItem ] = useState({
clickedFavoritedVideo: null
});
const handleSelectedFavorite = (renderFavorite) => {
setFavoritedItem((previous) => ({
...previous,
clickedFavoritedVideo: renderFavorite
}));
};
//Add a newly favorited video to a, by user created, list (BUG: for now the favorited video is added to EVERY, by the user, created list)
const [ favoritedList, setFavoritedList ] = useState([]);
const handleFavoritedVideo = (favoritedElement, selectedList) => {
setFavoritedList((previousFavorited) => {
return [ { favoritedElement, selectedList }, ...previousFavorited ];
});
};
const deleteFavorited = (id) => {
setFavoritedList((prevLists) => {
return prevLists.filter((listItem, index) => {
return index !== id;
});
});
};
return (
<div className="container">
<NavBar handleSearch={handleSearch} />
<div className="content">
<SideBar
addList={addList}
lists={lists}
handleSelectedFavorite={handleSelectedFavorite}
favoritedList={favoritedList}
onDeleteList={onDeleteList}
onDeleteFavorited={deleteFavorited}
/>
<main className="video">
<VideoContext.Provider value={handleSelectedVideo}>
<FavoriteContext.Provider value={handleFavoritedVideo}>
<VideoDetail
selectedVideo={searchedValue.selectedVideo}
clickedFavoritedVideo={
favoritedItem.clickedFavoritedVideo
}
/>
<VideoList listOfVideos={videos} lists={lists} />
</FavoriteContext.Provider>
</VideoContext.Provider>
</main>
</div>
</div>
);
};
export default App;
I will not post my whole app here, because it is a lot of files. I just give a link to my gitHub:
GitHub LINK
I was trying to find a solution, as stated here:
Link to SO page
which is like my case, but it didn't help (maybe because I was not using memo):
import React, { useState, createContext, useEffect, useCallback } from "react";
import NavBar from "../NavBar/NavBar";
import youtube from "../../apis/youtube";
import VideoList from "../VideoList/VideoList";
import VideoDetail from "../VideoDetail/VideoDetail";
import SideBar from "../SideBar/SideBar";
import "./App.css";
export const VideoContext = createContext();
export const FavoriteContext = createContext();
const API_KEY = process.env.REACT_APP_API_KEY;
const App = () => {
const [ videos, setVideos ] = useState([]);
const [ searchedValue, setSearchedValue ] = useState({
selectedVideo: null
});
const handleSelectedVideo = useCallback((singleRenderedVideo) => {
setSearchedValue((previous) => ({
...previous,
selectedVideo: singleRenderedVideo
}));
}, []);
const handleSearch = async (inputText) => {
const response = await youtube.get("/search", {
params: {
q: inputText,
part: "snippet",
type: "video",
maxResults: 16,
key: API_KEY
}
});
setVideos(response.data.items);
setSearchedValue({
selectedVideo: response.data.items[0] //take the first search result and make it appear as a playable one
});
};
useEffect(() => {
handleSearch();
}, []);
//By the user newly created lists
const [ lists, setLists ] = useState([]);
const addList = useCallback((newList) => {
setLists((prevLists) => {
return [ ...prevLists, newList ];
});
}, []);
const onDeleteList = useCallback((id) => {
setLists((prevLists) => {
return prevLists.filter((listItem, index) => {
return index !== id;
});
});
}, []);
//Render(Play) Favorited Video
const [ favoritedItem, setFavoritedItem ] = useState({
clickedFavoritedVideo: null
});
const handleSelectedFavorite = useCallback((renderFavorite) => {
setFavoritedItem((previous) => ({
...previous,
clickedFavoritedVideo: renderFavorite
}));
}, []);
//Add a newly favorited video to a, by user created, list (BUG: for now the favorited video is added to EVERY, by the user, created list)
const [ favoritedList, setFavoritedList ] = useState([]);
const handleFavoritedVideo = useCallback((favoritedElement, selectedList) => {
setFavoritedList((previousFavorited) => {
return [ { favoritedElement, selectedList }, ...previousFavorited ];
});
}, []);
const deleteFavorited = useCallback((id) => {
setFavoritedList((prevLists) => {
return prevLists.filter((listItem, index) => {
return index !== id;
});
});
}, []);
return (
<div className="container">
<NavBar handleSearch={handleSearch} />
<div className="content">
<SideBar
addList={addList}
lists={lists}
handleSelectedFavorite={handleSelectedFavorite}
favoritedList={favoritedList}
onDeleteList={onDeleteList}
onDeleteFavorited={deleteFavorited}
/>
<main className="video">
<VideoContext.Provider value={handleSelectedVideo}>
<FavoriteContext.Provider value={handleFavoritedVideo}>
<VideoDetail
selectedVideo={searchedValue.selectedVideo}
clickedFavoritedVideo={
favoritedItem.clickedFavoritedVideo
}
/>
<VideoList listOfVideos={videos} lists={lists} />
</FavoriteContext.Provider>
</VideoContext.Provider>
</main>
</div>
</div>
);
};
export default App;
I also tried to give a type for my buttons (type="button"), which currently have no type, like as in:
CreateNewList.js
import React, { useState } from "react";
import iconSprites from "../../images/sprite.svg";
import shortid from "shortid";
const CreateNewList = ({ onAdd }) => {
const [ list, setList ] = useState({
id: shortid.generate(),
title: ""
});
const handleChange = (event) => {
const { value } = event.target;
setList((prevList) => {
return {
...prevList,
title: value
};
});
event.preventDefault();
};
const submitNewList = (event) => {
onAdd({ ...list });
setList({ id: shortid.generate(), title: "" });
event.preventDefault();
};
return (
<React.Fragment>
<li className="new-list__item">
<form>
<div className="new-list__link">
<button
onClick={submitNewList}
className="new-list__btn-plus btn"
>
<svg className="new-list__icon">
<use href={iconSprites + "#icon-circle-with-plus"} />
</svg>
</button>
<input
className="new-list__input"
name="title"
value={list.title}
onChange={handleChange}
placeholder="New List"
/>
</div>
</form>
</li>
</React.Fragment>
);
};
export default CreateNewList;
but it also didn't help. Maybe because they are not in <form>?
So that is it. Maybe someone can help me with my issue?
To have all of the questions in one place:
Why my app keeps rerendering?
Should I use memo with useCallback?
Should I put my buttons in a <form> and give them a type?
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