React todo list. addItem function not working - javascript

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()}]);
};

Related

Issue with React state-management

I am using the Google Books API to retrieve book details in my application. I have two routes: one with a form and the search results, and the other with the full details of each search result. Currently,
I have a link on the details page that takes me back to the form page, but I want to retain the state of the form page and display the search results when I click the link instead of just the form. How can I do this?
You can check my code below.
BookDetails.jsx
import { useParams } from "react-router-dom";
import fetchBookDetails from "../hooks/fetchBookDetails";
import { useQuery } from "#tanstack/react-query";
import { Link } from "react-router-dom";
import notFoundImage from "../assets/react.svg";
import Header from "./Header";
const BookDetails = () => {
const { id } = useParams();
const results = useQuery(["book", id], fetchBookDetails);
if (results.isLoading) {
return <div>Loading...</div>;
}
if (results.isError) {
return <div>Book with id {id} not found.</div>;
}
if (results.data) {
return (
<div>
<Header />
<Link to="/">
<button className="back">Back</button>
</Link>
<div className="book-details">
<img
src={
results.data.volumeInfo.imageLinks
? results.data.volumeInfo.imageLinks.thumbnail
: notFoundImage
}
alt={results.data.volumeInfo.title}
/>
<h1>{results.data.volumeInfo.title}</h1>
<p>{results.data.volumeInfo.authors}</p>
</div>
</div>
);
}
};
export default BookDetails;
SearchForm.jsx
const SearchForm = ({ onSubmit }) => {
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(e.target.elements.book.value);
};
return (
<>
{" "}
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="book">
Book Name:
<input type="text" name="book" />
</label>
<button type="submit">Submit</button>
</form>
</div>
</>
);
};
export default SearchForm;
Seachresult.jsx
import React from "react";
import { Link } from "react-router-dom";
import notFoundImage from "../assets/react.svg";
const SearchResults = ({ results }) => {
return results.isLoading ? (
<div>Loading...</div>
) : results.isError ? (
<div>{results.error.message}</div>
) : (
<div className="books">
{results.data.items.map((item, i) => {
return (
<Link to={`/book/${item.id}`} key={item.id}>
<div key={item.id} className="book border-2-red-500">
<p>{i + 1}</p>
<img
src={
item.volumeInfo.imageLinks
? item.volumeInfo.imageLinks.thumbnail
: notFoundImage
}
alt={item.volumeInfo.title}
/>
<h3>{item.volumeInfo.title}</h3>
<p>{item.volumeInfo.authors}</p>
</div>
</Link>
);
})}
</div>
);
};
export default SearchResults;
Home.jsx
import { useState } from "react";
import { useQuery } from "#tanstack/react-query";
import fetchBooks from "../hooks/fetchBooks";
import SearchResults from "./SearchResults";
import SearchForm from "./SearchForm";
import Header from "./Header";
const Home = () => {
const [book, setBook] = useState(null);
const results = useQuery(["search", book], fetchBooks);
return (
<main>
<Header />
<SearchForm onSubmit={setBook} />
<SearchResults results={results} />
</main>
);
};
export default Home;
And lastly, App.jsx
import "./App.css";
import { QueryClient, QueryClientProvider } from "#tanstack/react-query";
import Home from "./components/Home";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import BookDetails from "./components/BookDetails";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
cacheTime: Infinity,
},
},
});
function App() {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/book/:id" element={<BookDetails />} />
<Route path="/" element={<Home />} />
</Routes>
</QueryClientProvider>
</BrowserRouter>
);
}
export default App;
You can use React Context API, create a Context that wraps the components you want to grant access to that states, and then you can manage the state in a "global" way in order to keep it intact when going back and forth.
You can check the example bellow:
import React, { useState, createContext, useCallback, useContext } from 'react';
import { uuid } from 'uuidv4';
import ToastContainer from '../components/ToastContainer';
interface ToastContextData {
addToast(messages: Omit<ToastMessage, 'id'>): void;
}
export interface ToastMessage {
id: string;
type?: 'success' | 'error' | 'info';
title: string;
description: string;
}
const ToastContext = createContext<ToastContextData>({} as ToastContextData);
const ToastProvider: React.FC = ({ children }) => {
const [messages, setMessages] = useState<ToastMessage[]>([]);
const addToast = useCallback(
({ type, title, description }: Omit<ToastMessage, 'id'>) => {
const id = uuid();
const toast = {
id,
type,
title,
description,
};
setMessages((state) => [...state, toast]);
},
[]
);
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
<ToastContainer messages={messages} />
</ToastContext.Provider>
);
};
function useToast(): ToastContextData {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
export { ToastProvider, useToast };

How do I delete a list item that has been given a unique id when created, in React + Firebase?

I am creating a Todo list using React and Firebase. So far, I have already created the AddToDo functionality, however, now I am having trouble with the delete functionality. I believe this is where my problem lies. For example, when I try and click the delete icon that I set up, I get an error:
Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'id')
This is the code if it helps. AddLink.js
import { useState, useEffect } from "react";
import classes from "./addlink.module.css";
import firebase from "firebase/app";
import initFirebase from "../../config";
import "firebase/firestore";
import Todo from "../Todo/Todo";
import { v4 as uuidv4 } from "uuid";
initFirebase();
const db = firebase.firestore();
function AddLink(props) {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
useEffect(() => {
db.collection("links")
.orderBy("timestamp", "desc")
.onSnapshot((snapshot) => {
// this gives back an array
setTodos(
snapshot.docs.map((doc) => ({
id: doc.id,
todo: doc.data().todo,
}))
);
});
}, []);
const addTodo = (event) => {
event.preventDefault();
console.log("clicked");
db.collection("links").add({
id: uuidv4(),
todo: input,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
setInput("");
};
return (
<div className={classes.addlink}>
<form>
<div className={classes.adminlink}>
<input
type="text"
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<button
className={classes.adminbutton}
type="submit"
onClick={addTodo}
>
Add new link
</button>
</div>
</form>
{todos.map((todo, id) => (
<Todo value={todo} key={id} />
))}
{/* {modalIsOpen && (
<Modal onCancel={closeModalHandler} onConfirm={closeModalHandler} />
)}
{modalIsOpen && <Backdrop onCancel={closeModalHandler} />} */}
</div>
);
}
export default AddLink;
And Todo.js
import React from "react";
import { AiOutlinePicture } from "react-icons/ai";
import { AiOutlineStar } from "react-icons/ai";
import { GoGraph } from "react-icons/go";
import DeleteForeverIcon from "#material-ui/icons/DeleteForever";
import classes from "./todo.module.css";
import firebase from "firebase/app";
import initFirebase from "../../config";
import "firebase/firestore";
initFirebase();
const db = firebase.firestore();
function Todo(props) {
const deleteHandler = () => {
db.collection("todos").doc(props.todo.id).delete();
};
return (
<li className={classes.adminsection}>
<div className={classes.linkCards}>
<h3>{props.text}</h3>
<p>This is a new link</p>
<div>
<AiOutlinePicture />
<AiOutlineStar />
<GoGraph />
<DeleteForeverIcon onClick={deleteHandler} />
</div>
</div>
</li>
);
}
export default Todo;
Any help would be greatly appreciated.
const deleteHandler = () => {
db.collection("todos").doc(props.todo.id).delete();
};
You should replace props.todo.id with props.value.id.
const deleteHandler = () => {
db.collection("todos").doc(props.value.id).delete();
};
Alternatively you can change:
<Todo value={todo} key={id} />
To
<Todo todo={todo} key={id} />
The key you use to access props.value should be the same as the one declared in the jsx template. Using proptypes can help you avoid those mistakes.
After deleting from the database you should update the state, UI with
Todos.filter(d=>d.id !== id of deleted list item)

Reducer/Context Api

So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example

React-router not rendering dynamic component- When clicked nothing happens

I am doing a small project and have a list of components that display information about countries. Now I have added react router so that when I click on a card it displays more information about that country. Now when I click on the card nothing happens! Below is the code for the Countries.
import React, { Component } from 'react';
import { CountryList } from './Components/Card-List/CountryList';
import { SearchBox } from './Components/Search-box/Search-Box';
import './Countries.styles.css';
import { DetailCountryCard } from './Components/DetailCountryCard/DetailCountryCard';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
class Countries extends Component {
constructor() {
super();
this.state = {
countries:[],
searchField:"",
regionField:"",
darkMode: false
}
this.setDarkMode = this.setDarkMode.bind(this);
};
componentDidMount() {
fetch("https://restcountries.eu/rest/v2/all")
.then(response => response.json())
.then(all => this.setState({ countries: all,
regions: all}))
.catch(error => console.log("I have errored" + error));
}
setDarkMode(e){
this.setState((prevState) => ({ darkMode: !prevState.darkMode }));
}
render() {
const { countries, searchField, regionField, darkMode } = this.state;
const filterCountries = countries.filter((country) => country.name.toLowerCase().includes(searchField.toLowerCase()) &&
country.region.toLowerCase().includes(regionField.toLowerCase()));
return(
<Router>
<div className={darkMode ? "dark-mode" : "light-mode" }>
<nav className="navbar-items">
<h1 className="header">Where in the World</h1>
<div className="moon-end">
<button onClick={this.setDarkMode}>
<i className={darkMode ? "moon fas fa-moon" : "moon far fa-moon" }></i>
</button>
<h2>{darkMode ? "Dark Mode" : "Light Mode" }</h2>
</div>
</nav>
<div className="Input">
< SearchBox type="search" placeholder="Search a Country" handlechange={e=> this.setState({
searchField: e.target.value })}
/>
< SearchBox type="regions" placeholder="Filter by Regions" handlechange={e=> this.setState({
regionField: e.target.value })}
/>
</div>
<CountryList countries={filterCountries} />
{/* <Route path="/" exact component={Countries} /> */}
<Switch>
<Route path="/card-detail/:name" component={ DetailCountryCard } exact/>
</Switch>
</div>
</Router>
);
}
}
export default Countries
The link for each card is in the following component:
import React from 'react';
import './CountryList.styles.css';
import {Link} from 'react-router-dom'
import { CountryCard } from '../Card/CountryCard';
export const CountryList = (props) => (
<div className='card-list'>
{props.countries.map(country => (
<Link to={`/card-detail/${country.name}`} >
<CountryCard key={country.alpha2Code} country={country} />
</Link>
))}
</div>
);
This should go to the following component:
import React from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
export const DetailCountryCard = ({match}) => {
useEffect(() => {
fetchItem();
console.log(match);
},[])
const [country, setCountry] = useState([])
const fetchItem = async ()=> {
const fetchCountry = await fetch(`https://restcountries.eu/rest/v2/name/${match.params.name}`);
const countries = await fetchCountry.json();
setCountry(countries);
console.log(country);
}
return (
<div>
{country.map(town => (
<div>
<h1 key={town.alpha2Code}>{town.name}</h1>
<p>Native Name{town.nativeName}</p>
<p>Region: {town.region}</p>
<p>Languages: {town.languages[0].name}</p>
</div>
))}
</div>
);
}
Not sure what I am missing. I don't think I have done a typo on the component. So not sure why it is not rendering? Any help would be appreciated.
You just need add dependency of match in useEffect in DetailCountryCard. Because [] its similar in Class ComponentcomponentDidMount()` and you need to listen when match it's changed.
This is final code to DetailCountryCard:
import React from "react";
import { useEffect } from "react";
import { useState } from "react";
export const DetailCountryCard = ({ match }) => {
useEffect(() => {
fetchItem();
console.log(match);
}, [match]);
const [country, setCountry] = useState([]);
const fetchItem = async () => {
const fetchCountry = await fetch(
`https://restcountries.eu/rest/v2/name/${match.params.name}`
);
const countries = await fetchCountry.json();
setCountry(countries);
console.log(country);
};
return (
<div>
{country.map(town => (
<div>
<h1 key={town.alpha2Code}>{town.name}</h1>
<p>Native Name{town.nativeName}</p>
<p>Region: {town.region}</p>
<p>Languages: {town.languages[0].name}</p>
</div>
))}
</div>
);
};
I tested in CodeSandBox and it works!
Link

Update a react state (array of objects) from a child component

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

Categories