LocalStorage includes an empty array on every render - javascript

I'm trying to update my localStorage with new items that are added to my shopping cart. However, every time an item is added, an empty array is added before it (see screenshot). Why is this?
I'm thinking I need to add a ternary operator to return an empty array if there are no existing items in the cart and to return the current items in the cart if there are items currently in localStorage. Is this correct, or do I have a syntax error?
Screenshot:
Code in question:
useEffect(() => {
const newData = JSON.parse(localStorage.getItem('product')) || [];
newData.push(cart);
localStorage.setItem('product', JSON.stringify(newData));
}, [cart])
Full code:
import React, { useState, useEffect } from 'react';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
function Cart(props) {
const [cart, setCart] = useState([]);
const [quantity, setQuantity] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(async () => {
fetchItems();
}, [])
const itemId = props.match.params.id;
const itemQuantity = parseInt(props.match.params.qty, 10);
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products/' + itemId);
const items = await data.json();
setCart(items)
setQuantity(itemQuantity)
setLoading(true)
}
function price(qty){
const newPrice = qty * cart.price;
return newPrice.toFixed(2)
}
useEffect(() => {
const newData = JSON.parse(localStorage.getItem('product')) || [];
newData.push(cart);
localStorage.setItem('product', JSON.stringify(newData));
}, [cart])
return (
<div>
{loading ? (
<div className="productStyle">
<img src={cart.image} className="productImage"></img>
<p>{cart.title}</p>
<div className="quantity">
<button className="btn minus-btn" type="button"
onClick={quantity > 1 ? () => setQuantity(quantity - 1) : null}>-</button>
<input type="text" id="quantity" placeholder={quantity}/>
<button className="btn plus-btn" type="button"
onClick={() => setQuantity(quantity + 1)}>+</button>
</div>
<p>${price(quantity)}</p>
</div>
) : (<ReactBootStrap.Spinner className="spinner" animation="border" />)}
</div>
);
}
export default Cart;

You are fetching the initial cart content asynchronously, but are setting product to newData right away, i.e., before cart had any content, which means that newData only contains [].
Perhaps just replace newData.push(cart); with cart.length > 0 && newData.push(cart);? Or rethink your logic. Not sure what you want product to contain.

Related

Array disappears after a few clicks with an undefined error - React

I'm trying to make a simulation of a blackjack hand - first user get two random cards, and with each 'hit' they get another one, however after a few 'hit' the app crashes and the 'undefined' error comes up in (array is undefined therefore can't get length). I've tried to save the deck again in the original shuffle, tried putting it all in one, however I can never get it to fully work.
I suspect it's something to do with useState being used incorrectly, however I'm not sure how to fix it.
Here's my code:
import {useState, useEffect} from 'react'
import Card from '../components/Card';
import Total from '../components/Total';
import {deckArray} from '../utils/data'
export default function Home(){
const initialHand = 2
const [dealCards, setDealCards] = useState(false)
const [isStarted, setIsStarted] = useState(false)
const [isReset, setIsReset] = useState(false)
const [hand, setHand] = useState(initialHand)
const [deck, setDeck] = useState(deckArray)
const [total, setTotal] = useState(0)
const [usersCards, setUsersCards] = useState([])
function shuffle(deck){
console.log("shuffle called")
setIsStarted(true)
let i = deck.length;
while (--i > 0) {
let randIndex = Math.floor(Math.random() * (i + 1));
[deck[randIndex], deck[i]] = [deck[i], deck[randIndex]];
}
setUsersCards(deck.slice(-initialHand))
console.log(usersCards)
console.log(deck)
}
useEffect(() => {
if(dealCards===true){
const randomCard = deck[Math.floor(Math.random()*deck.length)];
const newCardsArray = deck.filter(el => el.index !== randomCard.index)
const chosenCardArray = deck.filter(el => el.index === randomCard.index)
const chosenCard = chosenCardArray[0]
setDeck(newCardsArray)
setUsersCards(prevCards => [...prevCards, chosenCard])
console.log(newCardsArray.length)
setDealCards(false)
}
}, [usersCards, dealCards, deck])
useEffect(() => {
if(isReset){
setUsersCards([])
setDeck(shuffle(deckArray))
setDealCards(false)
setTotal(0)
setIsStarted(true)
}
setIsReset(false)
},[isReset, setIsReset])
useEffect(() => {
if(total>=22){
setIsStarted(true)
setIsReset(true)
setDeck(shuffle(deckArray))
}
}, [total, setTotal])
return (
<>
{isStarted ? <>
<Total usersCards={usersCards} total={total} setTotal={setTotal}/>
<Card usersCards={usersCards} />
<button onClick={() => setDealCards(true)}>HIT</button>
<button>STAND</button>
<button onClick={() => setIsReset(true)}>START OVER</button>
</> :
<>
<p>Game over!</p>
<button onClick={() => shuffle(deck)}>PLAY AGAIN</button></>}
</>
)
}
any help much appreciated!

make todo and store in localstorage but getting this error fetchdata.map is not a function at App (App.js:27:1)

import React, { useState, useEffect } from 'react';
import './style.css';
export default function App() {
const [state, setState] = useState([]);
const [inputData, setInputData] = useState();
const [fetchdata, setFetchData] = useState([])
const addHandler = () => {
setState((data) => {
return [...data, inputData];
});
localStorage.setItem('state', JSON.stringify(state));
setInputData('');
};
setFetchData(localStorage.getItem('state'))
return (
<div>
<input
onChange={(e) => setInputData(e.target.value)}
value={inputData || ''}
placeholder="add items"
/>
<button onClick={addHandler}>Add</button>
{fetchdata?.map((item) => {
return (
<div style={{ color: `#+${color}` }}>
<li key={item}>{item}</li>
</div>
);
}) || []}
</div>
);
}
This is the code I have tried also need dynamic colors for lists. Any help is appreciated with big thanks
even the key I have given unique but it says unique key required
Try to add a default value to your fetchData:
const [fetchdata, setFetchData] = useState(localStorage.getItem('state') ?? []);
and please don't begin to use useless useEffect like every begginer are doing, further documentation here !
try:
remove
setFetchData(localStorage.getItem('state'))
replace
const initData = () => {
try {
return JSON.parse(localStorage.getItem('state'));
} catch (e) {
return [];
}
}
const [fetchdata, setFetchData] = useState(initData())

How can I change the state of individual elements in a map function?

I want the content to display when the tab is clicked. The issue that I'm having is that once the tab is clicked, all the tabs open... and likewise close when clicked again. I've been trying for hours to figure out how to fix this. I thought I had an answer by having a state that I could set the index to and then write a condition for the tab to open when the index of the state is the same but I noticed that after clicking on another tab, the other one closes. I would appreciate it so much if someone could help me open an individual tab when it's clicked and always stay open until clicked again, meaning, I could have multiple tabs open at once.
Here's a demo:
https://codesandbox.io/s/orrigenda-react-question-5oxg47
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import LeaguesStyle from '../components/styles/LeaguesStyle.css';
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false)
const [isOpen, setOpen] = useState(false);
const getTeams = async () => {
try {
const res = await axios.get('https://api-football-standings.azharimm.site/leagues');
setTeams(res.data.data)
setLoading(true);
console.log(res.data)
} catch (err) {
alert(err.message)
}
}
useEffect(() => {
getTeams();
}, []);
return (
<div className="leagues">
{loading &&
teamz.map(item => (
<div className='teamCard' key={item.id}>
<div onClick={() => setOpen(!isOpen)} className="teamDiv">
<img src={item.logos.dark} className='teamLogo' />
<h1>{item.name}</h1>
</div>
{isOpen && <div className='card-content-active'>{item.abbr}</div>}
</div>
))}
</div>
);
}
You need to track the individual truthy values per item.id. This can be easily done by using an object to keep track of all the previous states via the spread operator. Once an initial state is set per tab, then it's just a matter of toggling that individual state between true and false. You delineate between tabs by dynamically assigning the id to the truthy value ([id]: !isOpen[id]). Here is the code in totality:
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [isOpen, setOpen] = useState({});
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const handleOpen = (id) => {
setOpen((prevTruthys) => ({ ...prevTruthys, [id]: !isOpen[id] }));
};
console.log(isOpen);
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => handleOpen(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isOpen[item.id] === true && (
<div className="card-content-active">{item.abbr}</div>
)}
</div>
))}
</div>
);
};
export default Leagues;
Here is the code sandbox: https://codesandbox.io/s/orrigenda-react-question-forked-42lbfo?file=/src/App.js
The solution is to store all clicked tabs in a list using the item ID, when the tab is open and you clicked again the ID is removed from the list
here is the code with the solution:
I created a function to update the state. setOpenById(tabId) and a function for checking if the tab is open isTabOpen(tabId)
the onClick now uses that function onClick={() => setOpenById(item.id)}
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [openTab, setOpenTab] = useState([])
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
//console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const setOpenById = (tabId) => {
if(!isTabOpen(tabId)){
setOpenTab([...openTab, tabId])
} else{
var array = [...openTab] // make a separate copy of the array
var index = array.indexOf(tabId)
if (index !== -1) {
array.splice(index, 1)
setOpenTab(array)
}
}
}
const isTabOpen = (tabId) => {
return openTab.indexOf(tabId) !== -1
}
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => setOpenById(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isTabOpen(item.id) && <div className="card-content-active">{item.abbr}</div>}
</div>
))}
</div>
);
};
export default Leagues;

New elements are not shown in the list after adding in React

When the user clicks the plus button, I want the task array to appear as a list. I am not able to push my tasks to an array
import logo from './logo.svg';
import './App.css';
import { BsPlusLg } from "react-icons/bs";
import { useState } from 'react';
const App = () => {
const items = ["Pizza", "Burger", "Shawarma", "Biryani", "Butter Naan", "Panner", "Chapathi"];
let tasks = [];
const [searchValue, setSearchValue] = useState("");
const changeValue = (event) => {
setSearchValue(event.target.value);
}
const searchedItems = tasks.filter((item) => {
if(item.toLowerCase().includes(searchValue.toLowerCase())){
return item;
}
})
const handleClick = () => {
if(items.length>0){
tasks.push(items.pop());
}
}
On clicking the plus button, after the items are added to the tasks array, they should appear as a list.
return (
<div className="App">
<div className='navbar'>
<input type="text" value={ searchValue } onChange={ changeValue } placeholder="Search"></input>
{/* <div className="verticalLine"></div> */}
<button onClick={ handleClick }><BsPlusLg /></button>
</div>
<hr/>
<div className='list'>
<ul>
{(searchValue.length>0)&&(searchedItems.map((item) => {
return <li key={item}>{item}</li>}))
}
{(searchValue.length===0)&&(tasks.map((item) => {
return <li key={item}>{item}</li>
}))
}
</ul>
</div>
</div>
);
}
export default App;
The elements in the tasks in the page are not being displayed when I log out of the tasks that are being pushed.
Clicking on your button does not update your component and will therefore rerender your list.
If if it would, it will run the whole function again and even call let tasks = []; again, which would reset your whole list. To update lists or whatever state you else need, you are required to use useState or similar hooks.
Because you also pop your items array, you might want to use two useState hooks and update them instead. Those will handle the internal state for you and isn't lost on a rerender. See https://reactjs.org/docs/hooks-state.html for more information. A solution might look like the following:
import "./styles.css";
import {useState} from 'react'
const BsPlusLg = () => <span >+</span>
const App = () => {
const [items, setItems] = useState( ["Pizza", "Burger", "Shawarma", "Biryani", "Butter Naan", "Panner", "Chapathi"]);
const [tasks, setTasks] = useState([]);
const [searchValue, setSearchValue] = useState("");
const changeValue = (event) => {
setSearchValue(event.target.value);
}
// searchedItems will be updated on each rerender
const searchedItems = tasks.filter((item) => {
if(item.toLowerCase().includes(searchValue.toLowerCase())){
return item;
}
})
const handleClick = () => {
if(items.length === 0){
// keep it simple and stop execution
return;
}
// enforce a new reference
let newItems = [...items];
let item = newItems.pop();
// enforce a new tasks reference
let newTasks = [...tasks, item];
setItems(newItems);
setTasks(newTasks);
}
return (
<div className="App">
<div className='navbar'>
<input type="text" value={ searchValue } onChange={ changeValue } placeholder="Search"></input>
{/* <div className="verticalLine"></div> */}
<button onClick={ handleClick }><BsPlusLg /></button>
</div>
<hr/>
<div className='list'>
<ul>
{(searchValue.length>0)&&(searchedItems.map((item) => {
return <li key={item}>{item}</li>}))
}
{(searchValue.length===0)&&(tasks.map((item) => {
return <li key={item}>{item}</li>
}))
}
</ul>
</div>
</div>
);
}
export default App;
Sandbox: https://codesandbox.io/s/confident-albattani-jf1z6f?file=/src/App.js:0-1655
It means that when React component be re-render, variables is not changed.
you must define 'item' variable as state.
const [tasks, setTasks] = useState([]);
For push it:
setTasks([...tasks, item.pop()])

Reactjs filter not returning correct users, when i delete the characters in the search filter

I am fetching the users from dummyapi and i am listing them. There is a search input, i want to filter the users on the page by name. When i type the characters, it filters correctly. When i start to delete the character, users are not listed correctly. It remains filtered. How can i fix this ? This is my code:
import { useEffect, useState } from "react";
import Header from "../components/Header";
import User from "./User";
import axios from "axios";
function App() {
const BASE_URL = "https://dummyapi.io/data/api";
const APP_ID = "your app id";
const [users, setUsers] = useState(null);
const handleChange = (e) => {
const keyword = e.target.value.toLowerCase();
const filteredUsers =
users &&
users.filter((user) => user.firstName.toLowerCase().includes(keyword));
setUsers(filteredUsers);
};
useEffect(() => {
async function fetchData() {
try {
const response = await axios.get(`${BASE_URL}/user?limit=1`, {
headers: { "app-id": APP_ID },
});
setUsers(response.data.data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
return (
<>
<Header />
<div className="container">
<div className="filter">
<h3 className="filter__title">USER LIST</h3>
<div>
<input
id="filter"
type="text"
placeholder="Search by name"
onChange={handleChange}
/>
</div>
</div>
<div className="user__grid">
{users &&
users.map((user, index) => {
const { id } = user;
return <User key={index} id={id} />;
})}
</div>
</div>
</>
);
}
export default App;
This is because you are manipulating the original array of users. So after each filter the original array has less values than previous hence after deleting it will search from the reduced number of elements.
To avoid this, keep original way as it is, apply filter on that and store the result in a separate array.
Something like this:
const [allUsers, setAllUsers] = useState(null); //will store original records
const [users, setUsers] = useState(null); // will store filtered results
then in useEffect hook:
useEffect(() => {
async function fetchData() {
try {
const response = await axios.get(`${BASE_URL}/user?limit=1`, {
headers: { "app-id": APP_ID },
});
setUsers(response.data.data);
setAllUsers(response.data.data); //add this line
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
and finally in handleChange event:
const handleChange = (e) => {
const keyword = e.target.value.toLowerCase();
// use allUsers array (with original unchanged data)
const filteredUsers =
allUsers &&
allUsers.filter((user) => user.firstName.toLowerCase().includes(keyword));
setUsers(filteredUsers);
};
Obviously, you can use some better approach, but this is just to give the idea of original issue.

Categories

Resources