As I am learning React, I've decided to try to create my own online store. Just after learning and implementing createContext functionality and handling, I've come across the problem of it not keeping data on page refresh/reload.
The easiest solution would probably be to store the data into the localStorage, but I can't figure out how to implement and handle the localStorage item as the whole createContext (contextValue) array and keep it updated on each context's function change.
Is there any simple solution for this in React? Or what is the best JS solution in react overall to handle such context?
import { createContext, useState } from "react";
import { productsArray, getProductData } from "./productsConfig";
export const CartContext = createContext({
items: [],
getProductQuantity: () => {},
addOneToCart: () => {},
removeOneFromCart: () => {},
deleteFromCart: () => {},
getTotalCost: () => {}
});
export function CartProvider({children}) {
const [cartProducts, setCartProducts] = useState([]);
function getProductQuantity(id) {
const quantity = cartProducts.find(product => product.id === id)?.quantity;
if (quantity === undefined) {
return 0;
}
return quantity;
}
function addOneToCart(id) {
const quantity = getProductQuantity(id);
if (quantity === 0) {
setCartProducts(
[
...cartProducts,
{
id: id,
quantity: 1
}
]
)
} else { // is in cart
setCartProducts(
cartProducts.map(
product =>
product.id === id
? { ...product, quantity: product.quantity + 1 }
: product
)
)
}
}
function removeOneFromCart(id) {
const quantity = getProductQuantity(id);
if (quantity === 1) {
deleteFromCart(id);
} else {
setCartProducts(
cartProducts.map(
product =>
product.id === id
? { ...product, quantity: product.quantity - 1 }
: product
)
)
}
}
function deleteFromCart(id) {
setCartProducts(
cartProducts =>
cartProducts.filter(currentProduct => {
return currentProduct.id !== id;
})
)
}
function getTotalCost() {
let totalCost = 0;
cartProducts.map((cartItem) => {
const productData = getProductData(cartItem.id);
totalCost += (productData.price * cartItem.quantity);
});
return totalCost;
}
const contextValue = {
items: cartProducts,
getProductQuantity,
addOneToCart,
removeOneFromCart,
deleteFromCart,
getTotalCost
}
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
)
}
export default CartProvider;
You should use context to manage cart internal state and use local storage to keep your cart state persistent (or you can use an session storage approach). So, use local storage to save your cartProducts array as an object
localStorage.setItem('cart', JSON.stringify(newCartProductsArray))
and update it on add/remove functions, the update function should overwrite the same key value.
To restore your state after an refresh use useEffect hook inside CartProvider.
useEffect(() => {
const savedCartState = JSON.parse(localStorage.getItem('cart'));
setCartProducts(savedCartState);
}, []);
Related
I have a form where I put a float value (1.1, 1.2, 1.9 and so on) and I want to store a bunch of them inside an array on an atom:
import { atom } from 'recoil';
export const valueState = atom({
key: 'values',
default: []
});
Whenever I write a value and it's checked that it's a double, the value gets added to valueState, however, I want to make it so if that the value I write on the form gets deleted, it also deletes the value from the valueState array. I tried by using pop, however, if I do so the program crashes. How can I do it then?
import { valueState as valueStateAtom } from '../../atoms/Atoms';
import { useSetRecoilState } from 'recoil';
const setValue = useSetRecoilState(valueStateAtom);
// The function that handles the onChange event of the form
const setNewValue = (v) => {
if (v !== '') {
const valueNumber = parseFloat(v);
if (!isNaN(valueNumber)) {
setPageValueChanged(true);
pageValue = valueNumber;
// The value gets added to valueState
setValue((prev) => prev.concat({ valueNumber, cardID }));
} else {
setPageValueChanged(false);
}
} else {
setPageValueChanged(false);
// Delete v from the atom array here
}
};
pop did not work for you because it does not return a new array (state immutability)
I think you can do a trick with filter. For example
setValue((prev) => prev.filter((value, index) => index !== prev.length - 1));
Full code
import { valueState as valueStateAtom } from '../../atoms/Atoms';
import { useSetRecoilState } from 'recoil';
const setValue = useSetRecoilState(valueStateAtom);
// The function that handles the onChange event of the form
const setNewValue = (v) => {
if (v !== '') {
const valueNumber = parseFloat(v);
if (!isNaN(valueNumber)) {
setPageValueChanged(true);
pageValue = valueNumber;
// The value gets added to valueState
setValue((prev) => prev.concat({ valueNumber, cardID }));
} else {
setPageValueChanged(false);
}
} else {
setPageValueChanged(false);
setValue((prev) => prev.filter((value, index) => index !== prev.length - 1));
}
};
One more feedback, your concat is seemingly incorrect. It's expecting to have an array param but you passed an object. The modification can be
setValue((prev) => prev.concat([{ valueNumber, cardID }]));
I'm trying to fetch all objects from the favorites array and set the checkbox to checked
I've checked online and tried using the localStorage for that yet nothing works and the values aren't saved after refreshing.
Would appreciate any help!
Selected Book Component :
import React, { useEffect, useState } from 'react';
import { bookService } from '../service/book.service';
export const SelectedBook = ({ selectedBook, setFavorites, favorites, removeFavorite }) => {
const onHandleFavorite = (book, e) => {
if (e.currentTarget.checked) {
setFavorites([...favorites, book]);
bookService.addFavorite(book);
} else {
removeFavorite(book);
}
};
const isFavorite = () => {
if (!favorites.includes(selectedBook)) {
return false;
} else {
return true;
}
};
return (
<div className='selected-book-container'>
<input type='checkbox' checked={isFavorite()} onChange={(e) => onHandleFavorite(selectedBook, e)} />
<div className='title'>{selectedBook?.title}</div>
</div>
);
};
Book Page component :
import React, { useEffect, useState } from 'react';
import { bookService } from '../service/book.service.js';
import { BookList } from '../cmps/BookList';
import { SelectedBook } from '../cmps/SelectedBook.jsx';
import { utilService } from '../service/util.service';
export const BookPage = () => {
const [books, setBooks] = useState([]);
const [favorites, setFavorites] = useState([]);
const [index, setIndex] = useState(0);
const [selectedBook, setSelectedBook] = useState();
useEffect(() => {
bookService.favoriteQuery().then((res) => {
setFavorites(res);
});
}, []);
useEffect(() => {
bookService.query().then((res) => {
setBooks(res);
setSelectedBook(res[0]);
});
}, []);
document.onkeydown = checkKey;
function checkKey(e) {
e = e || window.event;
if (e.keyCode == '37') {
if (index === 0) return;
setIndex(index - 1);
} else if (e.keyCode == '39') {
if (index >= books.length - 1) return;
setIndex(index + 1);
}
}
useEffect(() => {
setSelectedBook(books[index]);
}, [index]);
const removeFavorite = (book) => {
setFavorites(favorites.filter((favorite) => favorite.id !== book.id));
bookService.removeFavorite(selectedBook);
};
return (
<div>
<div className='main-container main-layout'>
<div className='second'>
<SelectedBook
selectedBook={selectedBook}
setFavorites={setFavorites}
favorites={favorites}
removeFavorite={removeFavorite}
/>
<BookList books={favorites} removeFavorite={removeFavorite} />
</div>
</div>
<div className='footer-container'>
<section className='footer'>Footer</section>
</div>
</div>
);
};
Service :
async function favoriteQuery() {
try {
let favorites = await _loadeFavoriteFromStorage();
if (!favorites) return (favorites = []);
return favorites;
} catch (err) {
console.log('cannot load favorites', err);
}
}
function _loadeFavoriteFromStorage() {
return storageService.loadFromStorage(STORAGE_FAVORITE_KEY);
}
Storage Service :
export const storageService = {
loadFromStorage,
saveToStorage
}
function saveToStorage(key, val) {
localStorage.setItem(key, JSON.stringify(val))
}
function loadFromStorage(key) {
var val = localStorage.getItem(key)
return JSON.parse(val)
}
thanks for any kind of help
You're not updating localstorage each time that checked is being changed. You're calling setFavorites with a new set of favorites but this is just changing state. I would suggest creating a function within the book page component which does
function changeFavorite(book, checked){
saveToStorage(book?, checked)
rerender()
}
and having rerender set the state of favorites to whatever is in localstorage to ensure that you have a single source of truth which is found in localstorage and that you change that and not anything else
I'll just add
if (!favorites.includes(selectedBook)) {
return false;
} else {
return true;
}
};
Could really look like
const isFavorite () => favorites.includes(selectedBook)
I also didn't quite understand how you're doing about storing the books in object storage. You should probably have an id of some sorts which you use to save favorite information with
I am making a simple e-commerce website but I've ran into an issue where useEffect() won't fire after making a state change. This code snippet I'll include is for the "shopping cart" of the website and uses localStorage to store all items in the cart. My state will change when quantity changes in the QuantChange() function but will not trigger useEffect(). When I refresh the page after changing an item's quantity, the new quantity won't persist and the old quantity is shown instead. What am I doing wrong? Thanks in advance.
import React, { useState, useEffect } from 'react';
import { SetQuantity } from '../utils/Variables';
import { CartItem } from './CartItem';
const CartView = () => {
const [state, setState] = useState(
JSON.parse(localStorage.getItem('cart-items'))
? JSON.parse(localStorage.getItem('cart-items'))
: []
);
useEffect(() => {
console.log('Updating!');
updateLocalStorage();
});
const updateLocalStorage = () => {
localStorage.setItem('cart-items', JSON.stringify(state));
};
const quantChange = (event) => {
setState((prevState) => {
prevState.forEach((item, index) => {
if (item._id === event.target.id) {
item.quantity = SetQuantity(parseInt(event.target.value), 0);
prevState[index] = item;
}
});
return prevState;
});
};
const removeItem = (id) => {
setState((prevState) => prevState.filter((item) => item._id != id));
};
// Fragments need keys too when they are nested.
return (
<>
{state.length > 0 ? (
state.map((item) => (
<CartItem
key={item._id}
ID={item._id}
name={item.name}
quantity={item.quantity}
changeQuant={quantChange}
delete={removeItem}
/>
))
) : (
<h1 className="text-center">Cart is Empty</h1>
)}
</>
);
};
export default CartView;
import React, { Fragment } from 'react';
import { MAX_QUANTITY, MIN_QUANTITY } from '../utils/Variables';
export const CartItem = (props) => {
return (
<>
<h1>{props.name}</h1>
<input
id={props.ID}
type="number"
max={MAX_QUANTITY}
min={MIN_QUANTITY}
defaultValue={props.quantity}
onChange={props.changeQuant}
/>
<button onClick={() => props.delete(props.ID)} value="Remove">
Remove
</button>
</>
);
};
export const MIN_QUANTITY = 1;
export const MAX_QUANTITY = 99;
// Makes sure the quantity is between MIN and MAX
export function SetQuantity(currQuant, Increment) {
if (Increment >= 0) {
if (currQuant >= MAX_QUANTITY || (currQuant + Increment) > MAX_QUANTITY) {
return MAX_QUANTITY;
} else {
return currQuant + Increment;
}
} else {
if (currQuant <= MIN_QUANTITY || (currQuant + Increment) < MIN_QUANTITY) {
return MIN_QUANTITY;
} else {
return currQuant + Increment;
}
}
}
You are not returning new state, you are forEach'ing over it and mutating the existing state and returning the current state. Map the previous state to the next state, and for the matching item by id create and return a new item object reference.
const quantChange = (event) => {
const { id, value } = event.target;
setState((prevState) => {
return prevState.map((item) => {
if (item._id === id) {
return {
...item,
quantity: SetQuantity(parseInt(value), 0)
};
}
return item;
});
});
};
Then for any useEffect hook callbacks you want triggered by this updated state need to have the state as a dependency.
useEffect(() => {
console.log('Updating!');
updateLocalStorage();
}, [state]);
I'm getting a "Unhandled Rejection (Error): Too many re-renders. React limits the number of renders to prevent an infinite loop." message for the following code. Not sure what is causing this issue.
I think it's because I'm calling the setNewNotifications(combineLikesCommentsNotifications) within the users.map loop. But if I move setNewNotifications(combineLikesCommentsNotifications) outside of the loop, it can no longer read likeNewNotifications / commentNewNotifications. What is the best approach to this?
Code below, for context, users returns:
const users = [
{
handle: "BEAR6",
posts: undefined,
uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1"
},
{
handle: "BEAR5",
posts: [
{
comment: false,
handle: "BEAR5",
key: "-Mmx7w7cTl-x2yGMi9uS",
like: {
Mn4QEBNhiPOUJPBCwWO: {
like_notification: false,
postId: "-Mmx7w7cTl-x2yGMi9uS",
postUserId: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1",
userLikeHandle: "BEAR6"
}},
post_date: 1635260810805,
title: "hello"
},
{
comment: false,
comments_text: {0: {
comment_date: 1635399828675,
comment_notification: false,
commenter_comment: "hi1",
commenter_handle: "BEAR6",
commenter_uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1",
key: "-Mn4QF1zT5O_pLRPqi8q"
}},
handle: "BEAR5",
key: "-MmxOs0qmFiU9gpspEPb",
like: {
Mn4QDCOrObhcefvFhwP: {
like_notification: false,
postId: "-MmxOs0qmFiU9gpspEPb",
postUserId: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
uid: "ckB4dhBkWfXIfI6M7npIPvhWYwq1",
userLikeHandle: "BEAR6"},
Mn4QKEk95YG73qkFsWc: {
postId: "-MmxOs0qmFiU9gpspEPb",
postUserId: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
uid: "rFomhOCGJFV8OcvwDGH6v9pIXIE3",
userLikeHandle: "BEAR5"
}},
post_date: 1635265250442,
title: "hi"
}
],
uid: "rFomhOCGJFV8OcvwDGH6v9pIXIE3"
}
]
Code
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
export default function Notifications() {
const [newNotifications, setNewNotifications] = useState('')
const users = useSelector(state => state.users)
return users.map((post) => {
if(post.posts){
return post.posts.map((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null
const comments = postContent.comments_text ? Object.values(postContent.comments_text) : null
const likeNewNotifications = likes ? likes.filter(post => {
return post.like_notification === false
} ) : null
const commentNewNotifications = comments ? comments.filter(post => {
return post.comment_notification === false
} ) : null
const combineLikesCommentsNotifications = likeNewNotifications.concat(commentNewNotifications)
setNewNotifications(combineLikesCommentsNotifications)
}
)
}
return (
<div>
<p>
{newNotifications}
</p>
</div>
);
}
)
}
There are multiple errors. But lets face it step by step.
I'll copy and paste your code, but with extra comments, to let you know where I'm referencing:
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
export default function Notifications() {
const [newNotifications, setNewNotifications] = useState('')
const users = useSelector(state => state.users)
// Step 0: I guess the error is because this users.map is running everytime (with any update in the component. So, when you set a new state, it'll render again. So, you have to do this, probably 2 times: on mount and after one update.
// Step 1: You're using users.map but it returns a new array. My recommendation would be: use users.forEach instead.
return users.map((post) => {
if(post.posts){
return post.posts.map((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null
const comments = postContent.comments_text ? Object.values(postContent.comments_text) : null
const likeNewNotifications = likes ? likes.filter(post => {
return post.like_notification === false
} ) : null
const commentNewNotifications = comments ? comments.filter(post => {
return post.comment_notification === false
} ) : null
const combineLikesCommentsNotifications = likeNewNotifications.concat(commentNewNotifications)
setNewNotifications(combineLikesCommentsNotifications)
}
)
}
return (
<div>
<p>
{newNotifications}
</p>
</div>
);
}
)
}
(Read Step 0 and Step 1 as comments in the code)
Also, about:
But if I move setNewNotifications(combineLikesCommentsNotifications) outside of the loop, it can no longer read likeNewNotifications / commentNewNotifications. What is the best approach to this?
You can do
Step 3: To be able to do that, you can use let, set one variable in the parent of the loop and update the value inside the loop (or if you have an array can push even if it's const). it'd be like:
function foo() {
const users = [{}, {}, {}, {}];
const usersWithEvenId = [];
users.forEach(user => {
if (user.id % 2 === 0) {
usersWithEvenId.push(user)
}
})
}
Taking in consideration these 3 steps the resulted code would be like:
import React, { useState, useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
export default function Notifications() {
const [newNotifications, setNewNotifications] = useState('');
const users = useSelector(state => state.users);
// Function to get new posts
const getNewPosts = () => {
const notifications = [];
users.forEach((user) => {
if (user.posts) {
posts.forEach((post) => {
// Your logic;
notifications.push(newNotifications)
})
}
});
setNewNotifications(notifications);
};
// Run to get newPosts on mount (but also in any other moment)
useEffect(() => {
getNewPosts();
}, [])
return (
<div>
<p>
{newNotifications}
</p>
</div>
);
}
Maybe you can write the code like this:
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
export default function Notifications() {
const users = useSelector((state) => state.users);
const combineLikesCommentsNotifications = users.map((post) => {
if (post.posts) {
return post.posts.map((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null;
const comments = postContent.comments_text
? Object.values(postContent.comments_text)
: null;
const likeNewNotifications = likes
? likes.filter((post) => {
return post.like_notification === false;
})
: null;
const commentNewNotifications = comments
? comments.filter((post) => {
return post.comment_notification === false;
})
: null;
const combineLikesCommentsNotifications = likeNewNotifications.concat(
commentNewNotifications
);
setNewNotifications(combineLikesCommentsNotifications);
});
}else{
return [];
}
})
const [newNotifications, setNewNotifications] = useState(combineLikesCommentsNotifications);
return (
<div>
<p>{newNotifications}</p>
</div>
); ;
}
My map dont appears in my component. I'm trying to make a carousel to show phrases and authors (one testimonial / author at time). I put the map in an array but it doesn't work. I have no idea what the best approach would be. I need a little help.
useQuoteQuery.js: (grabbing the data)
import { useStaticQuery, graphql } from 'gatsby'
export const useQuoteQuery = () => {
const data = useStaticQuery(graphql`
query QuoteQuery {
wpPage(databaseId: { eq: 13 }) {
id
ACF_HomePage {
socialProve {
testimony
author
}
}
}
}
`)
return data
}
on graphql: (it works perfectly)
Quote.js
import React, { useState, useEffect } from 'react'
import { useQuoteQuery } from '../../hooks/useQuoteQuery'
import QuoteImg from '../../images/quote.svg'
import { Content, Wrapper } from './Quote.styles'
import { BiRightArrow, BiLeftArrow } from 'react-icons/bi'
const Quote = () => {
const {
wpPage: { ACF_HomePage: data }
} = useQuoteQuery()
// edited - map return array but returns: Array(3)
// 0: {$$typeof: Symbol(react.element) ......
const quotes = data.socialProve.map(quote => {
return <li key={quote.toString()}>{quote.socialProve}</li>
})
// set interval
useEffect(() => {
const timer = window.setInterval(() => {
setActiveIndex(prev => (prev + 1 >= quotes.length ? 0 : prev + 1))
}, 5000)
return () => {
window.clearInterval(timer)
}
}, [quotes])
const [activeIndex, setActiveIndex] = useState(0)
const activeQuote = quotes[activeIndex]
const handleNextClick = () => {
setActiveIndex(prev => (prev + 1 >= quotes.length ? 0 : prev + 1))
}
const handlePrevClick = () => {
setActiveIndex(prev => prev - 1)
}
return (
<Wrapper>
<Content>
<img src={QuoteImg} alt="aspas" />
<h6>{activeQuote.testimony}</h6>
<p>{activeQuote.author}</p>
<BiLeftArrow
size="20"
className="button-arrow"
onClick={handlePrevClick}
>
Anterior
</BiLeftArrow>
<BiRightArrow
size="20"
className="button-arrow"
onClick={handleNextClick}
>
Próximo
</BiRightArrow>
</Content>
</Wrapper>
)
}
export default Quote
the result:
There is no error in the vs code terminal.
The quotes array is wrapping the array produced by the .map in an extraneous array. Remove the extra array around the result of the .map:
const quotes = data.socialProve.map((quote) => {
return <div key={quote.toString()}>{quote.socialProve}</div>;
});