Update only a specific item on button click - reactjs

I need to know what I must do to ensure that the texts of all the items(except for the one that needs to) don't have the line-through effect upon clicking the purchase button. Right now, every single item on the list tends to have this effect when I click either one of them. Also, another behavior of this component should be to change the name of the button to Purchased from Purchase and vice-versa on clicking upon it. This is working as it should except for the fact that all the buttons show this behavior rather than only the one that was clicked. I have used Context API to pass data around. The screenshot to show what I mean is attached as well.
The code is as follows:
context.js
import React, {useState, useEffect, createContext} from "react";
export const FoodContext = createContext();
export const FoodProvider = (props) => {
const[food, setFood] = useState([]);
const getData = () => {
const request = {
method : 'GET',
}
fetch("http://localhost:3008/api/get", request)
.then(res => res.json())
.then(data => {
setFood(data)
})
};
useEffect(() => {
getData()
}, []);
return(
<FoodContext.Provider value={[food, setFood]}>
{props.children}
</FoodContext.Provider>
);
}
content_screen.js - This is where the ListItems component is getting rendered.
import React, {Component} from 'react';
import Caption from '../components/caption/caption';
import InputBar from '../components/input_bar/input_bar';
import ListItems from '../components/list_items/list_items';
import "./content_screen.css";
//This is where the ListItems component is getting rendered
export default class ContentScreen extends React.Component {
render() {
return(
<div className="content_screen">
<div className="caption">
<Caption></Caption>
<div className="search_box">
<InputBar></InputBar>
</div>
<div className="box">
<ListItems></ListItems>
</div>
</div>
</div>
);
}
}
list_items.jsx - The component in question
import React,{useState, useContext} from "react";
import { FoodContext } from "../../context";
import "./list_items.css";
const ListItems = () => {
const [food, setFood] = useContext(FoodContext);
const [purchase, setPurchase] = useState(false);
const deleteItem = (id) => {
const request = {
method : 'DELETE'
};
fetch(`http://localhost:3008/api/delete/${id}`, request)
.then(res => res.json())
.then(data => console.log(data));
};
const clicked = () => {
setPurchase(!purchase);
}
return(
<div>
{!food.length ? <p>No Items Added</p> : food.map((key, value) => <div className="list_items" key={key._id}>
{purchase ? <span>{food[value].name}</span> : (food[value].name)}
<button className="x_button" onClick={() => deleteItem(food[value]._id)}>X</button>
<button className="purchase_button" onClick={clicked}>{purchase ? 'Purchased' : 'Purchase'}</button>
</div>)}
</div>
);
}
export default ListItems;

The problem is that you are using the same state for all of them.
You need to have each item that has its own state of being purchased or not.
Make a NEW component ListItem
import React, { useState } from 'react';
export const ListItem = ({ item, deleteItem }) => {
const [purchased, setPurchased] = useState(false);
const purchase = () => {
setPurchased(!purchased)
}
return (
<div className="list_items">
{purchased ? <span>{item.name}</span> : (item.name)}
<button className="x_button" onClick={() => deleteItem(item._id)}>X</button>
<button className="purchase_button" onClick={purchase}>{purchased ? 'Purchased' : 'Purchase'}</button>
</div>
)
}
and then in your ListItems component
import { ListItem } from "./ListItem";
const ListItems = () => {
const [food, setFood] = useContext(FoodContext);
const deleteItem = (id) => {
const request = {
method : 'DELETE'
};
fetch(`http://localhost:3008/api/delete/${id}`, request)
.then(res => res.json())
.then(data => console.log(data));
};
return (
<div>
{food.length
? food.map((item, key) => <ListItem item={item} key={key} deleteItem={deleteItem} />)
: <div> No food available </div>
}
</div>
)
}

for the second part: you have 1 single state (purchase). All your items are conditioned to that same state, meaning if it is true, the will show purchased and if it's false they will show purchase. You need to have different states for them or you can have one state containing all of them ( I'm talking about an array). This is how you should go about it:
const [purchsed, setPurchased] = useState([])
//let's say this is the list of your items ( items)
for ( let i in items.length()){
setPurchased((prevState)=>[...prevState,false]
}
// Up to here we have an array containing false's. The length of the array is
// the same as your items
// give your items a name ( or another attribute that could be read from event)
<button name="item.id" className="purchase_button" onClick={clicked}>{purchase ? 'Purchased' : 'Purchase'}</button>
// This was happened when mapping
//now you can target the name attribute when you call your function
const clicked = (e) => {
let copy = [...purchased]
//make a copy of your state
copy[e.target.name] = !purchased[e.target.name]
setPurchased([...copy])
}
and your first problem is caused by the same mistake. You can follow the same pattern to fix that too. Bear in mind that this was not a quality fix, since there are some mistakes in setting up the foundation of this project.

Related

useState set to string not working in Reactjs

I have this code that controls the behavior of what to map from an array onClick. The useState is set to a string const [activeFilter, setActiveFilter] = useState('All'); that is supposed to automatically filter all products containing the string as tag but it doesn't do this automatically and I can't figure out why. Please help with code below.
index.js
import React, { useEffect, useState } from 'react'
import {client} from '../lib/client'
import { Product, FooterBanner, HeroBanner } from '../components'
const Home = ({products, bannerData}) => {
const [productItems, setProductItems] = useState([])
const [filterWork, setFilterWork] = useState([]);
const [activeFilter, setActiveFilter] = useState('All');
useEffect(() => {
setProductItems(products)
}, [])
const handleProductFilter = (item) => {
setActiveFilter(item)
setTimeout(() => {
if (item == 'All'){
setFilterWork(productItems)
}else{
setFilterWork(productItems.filter((productItem)=> productItem.tags.includes(item)))
}
}, 500)
}
return (
<>
<HeroBanner heroBanner={bannerData.length && bannerData[0]} />
<div className='products-heading'>
<h2>Best Selling Products</h2>
<p>Smoke accessories of many variations</p>
</div>
<div className='product_filter'>
{['Lighter', 'Pipe', 'Roller', 'Hookah', 'All'].map((item, index) => (
<div
key={index}
className={`product_filter-item app__flex p-text ${activeFilter === item ? 'item-active' : ''}`}
onClick={() => handleProductFilter(item)}
>
{item}
</div>
))}
</div>
<div className='products-container'>
{
filterWork.map((product) => <Product key={product._id} product={product} />)
}
</div>
<FooterBanner footerBanner={bannerData && bannerData[0]} />
</>
)
};
export const getServerSideProps = async () => {
const query = '*[_type == "product"]'
const products = await client.fetch(query)
const bannerQuery = '*[_type == "banner"]'
const bannerData = await client.fetch(bannerQuery)
return {
props: {products, bannerData}
}
}
export default Home
The image below is what it looks like on load and the only time All products containing 'All' tags are visible is when the All button is clicked on again, regardless of it being active initially
No products are being displayed initially when the component renders because the displayed products are loaded from the filterWork state that is only set once an onClick event is triggered. To fix this you can simply set the initial products in the useEffect because you are starting with all the products being displayed.
useEffect(() => {
setProductItems(products);
setFilterWork(products);
}, [])

useEffect fails on page refresh

I am an infant programmer and I am trying to fetch an api and style the results using React. My page works fine on the initial load and subsequent saves on VScode,but when I actually refresh the page from the browser I get the error thats posted on imageenter image description here:
Here is my code: App.js
```import React, { useEffect, useState } from 'react';
import './App.css';
import Students from './components/Students';
import styled from 'styled-components';
function App() {
const [studentInfo, setStudentInfo] = useState({});
const [searchResult, setSearchResult] = useState({});
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
getStudents();
}, []);
useEffect(() => {
getStudents();
console.log('useEffect');
}, [searchTerm]);
const getStudents = async () => {
const url = 'https://api.hatchways.io/assessment/students';
console.log(url);
fetch(url)
.then((res) => res.json())
.then((data) => {
console.log(data);
searchTerm != ''
? setStudentInfo(filterStudents(data.students))
: setStudentInfo(data.students);
});
};
const filterStudents = (studentsArray) => {
return studentsArray.filter((info) => {
return (
info.firstName.toLowerCase().includes(searchTerm) ||
info.lastName.toLowerCase().includes(searchTerm)
);
});
};
console.log(searchTerm);
return (
<div className="App">
<Students
studentInfo={studentInfo}
setSearchTerm={setSearchTerm}
searchTerm={searchTerm}
/>
</div>
);
}
export default App;```
here is my component Students.js:
```import React, { useState } from 'react';
import styled from 'styled-components';
import GradeDetails from './GradeDetails';
const Students = ({ studentInfo, searchTerm, setSearchTerm }) => {
console.log(typeof studentInfo);
console.log(studentInfo[0]);
const [isCollapsed, setIsCollapsed] = useState(false);
const handleDetails = () => {
setIsCollapsed(!isCollapsed);
};
const average = (arr) => {
let sum = 0;
arr.map((num) => {
sum = sum + parseInt(num);
});
return sum / arr.length.toFixed(3);
};
console.log(isCollapsed);
return (
<Container>
<Input
type="text"
value={searchTerm}
placeholder="Search by name"
onChange={(e) => setSearchTerm(e.target.value.toLowerCase())}
/>
{studentInfo?.map((student) => (
<Wrapper key={student.id}>
<ImageContainer>
<Image src={student.pic}></Image>
</ImageContainer>
<ContentContainer>
<Name>
{student.firstName} {student.lastName}{' '}
</Name>
<Email>Email: {student.email}</Email>
<Company>Company: {student.company}</Company>
<Skills>Skill: {student.skill}</Skills>
<Average>Average:{average(student.grades)}%</Average>
</ContentContainer>
<ButtonContainer>
<Button onClick={handleDetails}>+</Button>
</ButtonContainer>
{isCollapsed && <GradeDetails studentInfo={studentInfo} />}
</Wrapper>
))}
</Container>
);
};```
Every time I have the error, I comment out the codes in Students.js starting from studentInfo.map until the and save and then uncomment it and save and everything works fine again.
I am hoping someone can help me make this work every time so that I don't have to sit at the edge of my seat all the time. Thank you and I apologize for the long question.
You are using an empty object as the initial state for studentInfo (the value passed to useState hook will be used as the default value - docs):
const [studentInfo, setStudentInfo] = useState({});
.map is only supported on Arrays. So this is failing when the component is rendering before the useEffect has completed and updated the value of studentInfo from an object, to an array. Try swapping your initial state to be an array instead:
const [studentInfo, setStudentInfo] = useState([]);

Ternary condition on onclick event

I am trying to create a to-do application in React. Code I have so far adds to-do items to the to-do list. When I click on the edit icon I had put turnery condition for the done icon but it's not working. Can someone explain what is wrong with my code?
App.js
import './App.css';
import React, { useState } from 'react';
import TodoList from './TodoList';
import { v4 as uuidv4 } from 'uuid';
function App() {
// const [input, setInput] = useState('');
const [todos, setTodo] = useState([]);
const input = React.useRef();
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([...todos, { id: id, text: input.current.value }])
input.current.value='';
}
const deleteTodo = (id) => {
setTodo(todos.filter(todo => todo.id !== id));
}
const editTodo = (id) => {
}
return (
<div className="App">
<form>
<input type="text" ref={input}/>
<button type="submit" onClick={addTodo}>Enter</button>
</form>
<TodoList todos={todos} deleteTodo={deleteTodo} editTodo={editTodo}/>
</div>
);
}
export default App;
TodoItem.js
import React from 'react'
import DeleteIcon from '#material-ui/icons/Delete';
import EditIcon from '#material-ui/icons/Edit';
import CheckBoxOutlineBlankIcon from '#material-ui/icons/CheckBoxOutlineBlank';
import DoneIcon from '#material-ui/icons/Done';
const TodoItem = ({todo, deleteTodo, editTodo}) => {
return (
<>
<div>
<CheckBoxOutlineBlankIcon/>
<input type="text" value={todo.text} readOnly={true}/>
</div>
<div>
{ <EditIcon/> ? <EditIcon onClick={editTodo}/> : <DoneIcon/> }
<DeleteIcon onClick={deleteTodo}/>
</div>
</>
)
}
export default TodoItem
There are a few problems with your code. One, also pointed out by Abu Sufian is that your ternary operator will always trigger whatever is immediately after ?, because <EditIcon/> is just a component and will always be true.
But more fundamentally, to do what you want, you will need to add another properly to your todo list, say, status. So when a task goes in for the first time, it will be in pending status, then once you click your Edit icon, it will change to done. And that's how we will toggle that icon with a ternary operator.
So I would change your addTodo function to
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([
...todos,
{ id: id, text: input.current.value, status: "pending" }
]);
input.current.value = "";
};
Then I would change your editTodo to:
const editTodo = (id) => {
console.log(id);
setTodo(
todos.map((todo) => {
if (todo.id === id) todo.status = "done";
return todo;
})
);
};
And finally, I would change your ternary part to:
{todo.status === "pending" ? (
<EditIcon onClick={() => editTodo(todo.id)} />
) : (
<DoneIcon />
)}
Here is a complete Sandbox for you. Sorry I don't have your CSS so I can't make it look super pretty.
Maybe you are looking for something like this.
{ !todo.done ? <EditIcon onClick={editTodo}/> : <DoneIcon/> }
I believe checking whether a todo item is done or not should happen with a property of todo object itself.
In a ternary you need to start with a condition.
condition ? do something when true : do something when false
So you have to have a condition in the first place. In your case EditIcon is not a condition.
If you are looking for a way to mark a todo as completed so you need to do more things.
const markAsCompleted = id => {
const todo = todos.find(todo => todo.id !== id);
setTodo([...todos, {...todo, done: true }]);
}
Then you can decide based on whether a todo is done or not.

Filter useContext state in a component without changing global state

I have a component that uses useContext to retrieve the global state, which is a list of movies/TV shows. I'm trying to save this global state into local state so I can filter the list by media type (movie or TV show) on a button click, but the filter should be reflected only locally, without dispatching an action to change the global state (I don't want the complete list to be lost).
However, after storing the global state to the local state, the local state is empty. Here is the component's code:
import React, { useContext, useState, useEffect } from "react";
import { WatchListContext } from "../../contexts/WatchListProvider";
import WatchItem from "../WatchItem";
const WatchList = () => {
const { state } = useContext(WatchListContext);
const [watchList, setWatchList] = useState(state);
const [filter, setFilter] = useState("");
useEffect(() => {
setWatchList((previousWatchlist) => {
const filteredWatchList = previousWatchlist.filter(
(watchItem) => watchItem.media_type === filter
);
return filteredWatchList;
});
}, [filter]);
return (
<div>
<div>
<button onClick={() => setFilter("tv")}>TV</button>
<button onClick={() => setFilter("movie")}>MOVIES</button>
</div>
{watchList
.filter((watchItem) => watchItem.media_type === filter)
.map(({ id, title, overview, poster_url, release_date, genres }) => (
<WatchItem
key={id}
title={title}
overview={overview}
poster_url={poster_url}
release_date={release_date}
genres={genres}
/>
))}
</div>
);
};
export default WatchList;
I can't understand why this isn't working. Any help will be much appreciated!
You can't straightaway ties your global state with your local state like the way you did (const [watchList, setWatchList] = useState(state);):-
by doing this it will automatically ties with your default value of your state declared in context which always gonna be an empty array or [] . So to go around this, why don't you track the incoming changes of state with useEffect, like so:-
WatchList.js:-
import React, { useContext, useState, useEffect } from "react";
import { WatchListContext } from "../../contexts/WatchListProvider";
import WatchItem from "../WatchItem";
const WatchList = () => {
const { state } = useContext(WatchListContext);
const [watchList, setWatchList] = useState([]);
const [filter, setFilter] = useState("");
// handle fetching current changes of state received by Context
useEffect(() => {
(() => setWatchList(state))()
}, [state])
// handle filtering of data
useEffect(() => {
setWatchList((previousWatchlist) => {
const filteredWatchList = previousWatchlist.filter(
(watchItem) => watchItem.media_type === filter
);
return filteredWatchList;
});
}, [filter]);
return (
<div>
<div>
<button onClick={() => setFilter("tv")}>TV</button>
<button onClick={() => setFilter("movie")}>MOVIES</button>
</div>
{watchList
.filter((watchItem) => watchItem.media_type === filter)
.map(({ id, title, overview, poster_url, release_date, genres }) => (
<WatchItem
key={id}
title={title}
overview={overview}
poster_url={poster_url}
release_date={release_date}
genres={genres}
/>
))}
</div>
);
};
export default WatchList;

Fetch firestore document from document id in array field and display in React

I have 2 collections on firestore, boxes contains a field shoesthat is an array of reference id to the shoes collections:
My Boxes component is fetching all boxes and displaying their number. I also want to display properties of the shoes in it, like a photo. So I go about like that:
Boxes.jsx
// DEPENDENCIES
import React, { useEffect, useContext } from 'react';
// COMPONENTS
import BoxCard from '../Components/BoxCard';
// CONTEXT
import ShoesContext from '../Contexts/ShoesContext';
// HELPERS
import db from '../config/firebase';
let initialBoxes = [];
const Boxes = () => {
const { boxes, setBoxes } = useContext(ShoesContext);
useEffect(() => {
initialBoxes = [];
db.collection('boxes')
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
initialBoxes.push(doc);
});
setBoxes(initialBoxes);
});
}, []);
return (
<div>
<h3>You have {boxes.length} boxes:</h3>
{!boxes ? (
<div>Loading..</div>
) : (
boxes.map(box => {
return <BoxCard box={box} key={box.id} />;
})
)}
</div>
);
};
export default Boxes;
Boxes.jsx
import React from 'react';
import TestComponent from './TestComponent';
const BoxCard = ({ box }) => {
const theBox = box.data();
return (
<div>
<h5>
Box number {theBox.number} has {theBox.shoes.length} shoes:{' '}
</h5>
{theBox.shoes.map(shoe => {
return <TestComponent shoe={shoe} />;
})}
</div>
);
};
export default BoxCard;
and this is where it all breaks:
import React from 'react';
const TestComponent = ({ shoe }) => {
useEffect(() => {
let hell;
shoe.get().then(doc => {
hell = doc.data();
});
}, []);
return <div>{hell ? hell.season : 'loading...'}</div>;
};
export default TestComponent;
hell is undefined. I have not found a way to render the nested docs so I can use them in my TestComponent component. My extensive research online has not been succesful so far, hence my question today.
Thanks!
Update:
I seem to have found the answer, answer from Josh below put me on the right track. See below code for TestComponent.jsx:
import React, { useEffect, useState } from 'react';
// HELPERS
import db from '../config/firebase';
const TestComponent = ({ shoe }) => {
const [hell, setHell] = useState();
useEffect(() => {
db.collection('shoes')
.doc(shoe.id)
.get()
.then(doc => {
setHell(doc.data());
});
}, []);
return <div>{hell ? hell.season : 'loading...'}</div>;
};
export default TestComponent;
What is shoe in shoe.get()... in the TestComponent? Shouldn't it be db.doc(shoe).get()....

Resources