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]);
Related
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 have a provider that receives data prop, puts it in a state. Also, there are a few methods to manipulate that state.
I pass the state and the data prop to consumers, but every time I change the state, there is no difference between the prop and the state. I want to be able to see what changed so I could update that value.
import { createContext, useContext, useEffect, useState } from "react";
const TableContext = createContext({
data: [],
headings: [],
onChangeCellContent: () => {},
});
const TableProvider = ({ data, headings, children }) => {
const [tableData, setData] = useState(data);
const [tableHeadings, setHeadings] = useState(headings);
useEffect(() => {
setData((previousData) => {
return data.length !== previousData.length ? data : previousData;
});
}, [data]);
const onChangeHeadingCell = ({ key, value }) => {
setHeadings((oldHeadings) =>
oldHeadings.map((heading) => {
if (heading.key === key) {
heading.title = value;
}
return heading;
})
);
};
const onChangeCellContent = ({ rowId, cellKey, value }) => {
setData((previousData) =>
[...previousData].map((row) => {
if (row.id === rowId) {
row[cellKey] = value;
return row;
}
return row;
})
);
};
const onAddNewRow = (rowData) => {
setData((oldData) => [...oldData, rowData]);
};
return (
<TableContext.Provider
value={{
tableData,
data,
onChangeCellContent,
onChangeHeadingCell,
onAddNewRow,
headings: tableHeadings,
}}
>
{children}
</TableContext.Provider>
);
};
export default TableProvider;
export const useTable = () => {
const context = useContext(TableContext);
if (context === "undefined") {
throw Error("Table provider missing");
}
return context;
};
Here is the change handler, it works, but it also changes the original data:
const Row = ({ data: row}) => {
const { onChangeCellContent, headings, data } = useTable();
...
// GIVES ME THE SAME VALUE WHEN I TRIGGER ONCHANGE
console.log(row.value, data.find((s) => s.id === row.id).value);
return <tr><td><select
className="w-full h-full focus:outline-none"
style={{
backgroundColor: "inherit",
}}
value={row.value}
onChange={(e) =>
onChangeCellContent({
rowId: row.id,
cellKey: "value",
value: e.target.value,
})
}
>...</select></td></tr>
I am doing a React JS Cart and I am having problems when I try to delete an Item from the there. It has already a function that adds the items and also another for the total quantity and the total price.
This is the ContextProvider:
import { useState } from "react";
import { CartContext } from "./CartContext";
export const CartProvider = ({ children }) => {
const [list, setList] = useState([]);
const addCart = (varietalCount) => {
if (list.find((item) => item.id === varietalCount.id)) {
const newVarietal = list.map((varietal) => {
if (varietal.id === varietalCount.id) {
return { ...varietal, count: varietalCount.count + varietal.count };
}
return varietal;
});
setList(newVarietal);
} else {
setList((state) => {
return [...state, varietalCount];
});
}
};
console.log("list", list);
// const deleteProd = (varietalCount) => {
// if (list.find((item) => item.id === varietalCount.id)) {
// const deleteVarietal = list.map((varietal) => {
// if (varietal.id === varietalCount.id) {
// return { ...varietal, count: null };
// }
// return varietal;
// });
// setList(deleteVarietal);
// } else {
// setList((state) => {
// return [...state, varietalCount];
// });
// }
// };
const totalPrice = () => {
return list.reduce((prev, next) => (prev + (next.count * next.price)), 0)
};
const totalQuantity = () => {
return list.reduce((prev, next) => (prev + (next.count)), 0)
};
return(
<>
<CartContext.Provider value={{ list, addCart, totalPrice, totalQuantity }}>
{children}
</CartContext.Provider>
</>);
};
If it is necessary I can add to the post the Cart.js or the ItemDetail.js. I hope someone can help me. Cheers
I think you can just use filter given that your state has value of an array. Something like:
const deleteProd = (varietalCount) => {
const newItems = list.filter((item) => item.id !== varietalCount.id)
setList(newItems);
};
You can check more array functions from here https://www.w3schools.com/jsref/jsref_obj_array.asp
I have a Chat component which uses API to populate the messages state, also there are different areas that have different chats which I pass as props to the component.
In this component I have 3 useEffects but I am interested in two of them which don't work properly. In the first useEffect I have some code that basically resets the messages state on area change to undefined. I need to do this to be able to distinguish between the API not being called yet where I display a loading component <Spinner /> or if the API has been called and it has retrieved an empty array to show the <NoData> component.
The problem that I have is that when I change areas the useEffects get triggered as they should but the first useEffect doesn't update the messages state to undefined before the second useEffect is called. And after a rerender because of history push the messages come as undefined but then the second useEffect doesn't get triggered anymore. I don't get why the state is not being updated in the first useEffect before the second. Also the weird thing is this used to work for me before now it doesn't. I changed some stuff up without pushing to git and now I am puzzeled. Code below:
export default function ChatPage({ history, match, area, ...props }) {
const [templates, setTemplates] = useState([]);
const [advisors, setAdvisors] = useState([]);
const [messages, setMessages] = useState(undefined);
const [conversation, setConversation] = useState([]);
const [chatToLoad, setChatToLoad] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [linkOrigin, setLinkOrigin] = useState("");
const [headerText, setHeaderText] = useState("");
// useEffect used to reset messages and conversation state
// triggered on area change(messages and conversation reset)
// and customer ID change(conversation reset).
// Required to distinguish between API call not being made yet
// and API returning no data.
useEffect(() => {
if (match.params.id) {
setLinkOrigin(match.params.id);
}
if (messages) {
if (match.params.id && messages.length !== 0) {
let matches = messages.filter(
(message) => message.ORIGINATOR === match.params.id
);
if (matches.length !== 0 && match.params.id === linkOrigin) {
setMessages(undefined);
history.push("/chats/" + match.params.area);
}
}
}
setConversation([]);
}, [area, match.params.id]);
// API calls
useEffect(() => {
if (templates.length === 0) {
api.getTemplates().then((templates) => {
setTemplates(templates);
});
}
if (advisors.length === 0) {
api.getAgents().then((advisors) => {
setAdvisors(advisors);
});
}
if (!messages || messages.length === 0) {
chooseQueue(match.params.area).then((queuesData) => {
let queues = queuesData.data.map((message) => ({
DATE_SORT: message.DATE_RECIEVED,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: "SMS_OUTBOUND",
ASSIGNED_TO: message.ASSIGNED_TO || null,
}));
setMessages(orderMessagesByDate(queues));
setChatToLoad(queues[0]);
});
}
}, [area]);
useEffect(() => {
if (messages) {
if (messages.length) {
let loadId = match.params.id ? match.params.id : messages[0].ORIGINATOR;
const params = {
MobileNumber: loadId,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
MSG_TYPE: message.MSG_TYPE2.replace("MobileOriginated", "SMS"),
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
setChatToLoad(
messages.find((message) => message.ORIGINATOR === loadId)
);
history.push("/chats/" + match.params.area + "/" + loadId);
}
}
}, [messages]);
function chooseQueue(queueType) {
switch (queueType) {
case "myqueue":
setHeaderText("My chats");
return queuesApi.getMyActiveQueues(area);
case "mycompleted":
setHeaderText("My completed chats");
return queuesApi.getMyCompletedQueues();
case "queues":
setHeaderText("Chats");
return queuesApi.getQueues(area);
case "completed":
setHeaderText("Completed chats");
return queuesApi.getCompletedQueues();
default:
setHeaderText("My chats");
return queuesApi.getQueues(area);
}
}
function classifyMessage(message) {
return message.MSG_TYPE.includes("OUTBOUND") ||
message.MSG_TYPE.includes("FAULT_TEST")
? "outbound"
: "inbound";
}
async function submitMessage(message) {
var params = {
number: message.ORIGINATOR,
message: message.MESSAGE,
smssize: message.MESSAGE.length
};
await messagingApi.replyToCustomer(params).then((res) => {
if (res.data[0].RVALUE === "200") {
let extendedMsg = [...messages, message];
let extendedConversation = [...conversation, message];
setConversation([...extendedConversation]);
setMessages(orderMessagesByDate([...extendedMsg]));
}
});
}
function orderMessagesByDate(list) {
return list.sort(function(x, y) {
return new Date(y.DATE_SORT) - new Date(x.DATE_SORT);
});
}
const modalHandler = () => {
setIsOpen(!isOpen);
};
let chatConfig = {
channelSwitch: true,
channels: channels,
templateModal: true,
templates: templates,
advisorModal: true,
advisors: advisors,
};
const onActiveChatChange = (message) => {
history.push("/chats/" + match.params.area + "/" + message.ORIGINATOR);
const params = {
MobileNumber: message.ORIGINATOR,
};
messagingApi.conversationHistory(params).then((conversationData) => {
setConversation(
conversationData.data.map((message) => ({
DATE_SORT: message.DATE_SORT,
UNIQUEID: message.UNIQUEID,
ORIGINATOR: message.ORIGINATOR,
MESSAGE: message.MESSAGE,
ASSIGNED_TO: message.ASSIGNED_TO || null,
}))
);
});
};
return (
<div data-test="component">
<BodyHeader
text={headerText}
children={
<FontAwesomeIcon
icon="plus-square"
aria-hidden="true"
size="2x"
onClick={modalHandler}
/>
}
/>
{messages && chatToLoad ? (
<>
<ChatWindow
messages={messages}
conversation={conversation}
chatToLoad={chatToLoad}
onActiveChatChange={onActiveChatChange}
classifyMessage={classifyMessage}
submitMessage={submitMessage}
config={chatConfig}
/>
<SendMessageModal isOpen={isOpen} toggle={modalHandler} />
</>
) : !messages ? (
<Spinner />
) : (
<NoDataHeader>There are no chats in this area</NoDataHeader>
)}
</div>
);
}
You can't get what you want this way. A state change applied in a useEffect won't have effect until the next rendering cycle, the following callbacks will still see the current const value.
If you want to change the value in the current rendering cycle the only option you have is to relax your const into let and set the variables yourself.
After all: you were expecting a const to change isn't it? ;)
I have a JSX element and a counter in the state, the JSX element uses the counter state in the state. I show the JSX element in component 1 with modal and set the JSX element in component2.
when I try to update the counter in component 2 it won't change in the JSX element counter in component 1.
Component 1
class Meeting extends Component {
render() {
const { dispatch, modalVisible, modalContent } = this.props;
return (
<Landing>
<Modal
title="Title"
visible={modalVisible}
onOk={() => { dispatch(showModal(false)); }}
onCancel={() => { dispatch(showModal(false)); }}
okText="Ok"
cancelText="Cancel"
>
<div>
{modalContent}
</div>
</Modal>
</Landing>
);
}
}
function mapStateToProps(state) {
const {
modalVisible,
modalContent,
counter
} = state.meeting;
return {
modalVisible,
modalContent,
counter
};
}
export default connect(mapStateToProps)(Meeting);
Component 2
class MeetingItem extends Component {
state = {
checked: []
}
handleChange = (event) => {
const { dispatch, counter } = this.props;
if (event.target.checked) {
this.setState(() => {
return { checked: [...this.state.checked, event.target.value] };
});
dispatch(setCounter(counter - 1));
} else {
const array = this.state.checked;
const index = array.indexOf(event.target.value);
if (index > -1) {
array.splice(index, 1);
this.setState(() => {
return { checked: array };
});
dispatch(setCounter(counter + 1));
}
}
};
isDisable = (val) => {
const array = this.state.checked;
const index = array.indexOf(val);
if (index > -1) {
return true;
} else {
return false;
}
}
showModal = () => {
const { dispatch, question, counter } = this.props;
const radioStyle = {
display: 'block',
height: '30px',
lineHeight: '30px',
marginTop: '6px'
};
dispatch(setCounter(0));
switch (question.question.TypeAnswer) {
case 'OneAnswer':
const answers = question.answer.map((record, i) => {
return <Radio.Button style={radioStyle} key={i} value={record.Id}>{record.Title}</Radio.Button>
});
const modalContent = <div>
<p>{question.question.Title}</p>
<Radio.Group buttonStyle="solid">
{answers}
</Radio.Group>
</div>
dispatch(setModalContent(modalContent));
break;
case 'MultiAnswer':
dispatch(setCounter(question.question.CountAnswer));
const multiAnswers = question.answer.map((record, i) => {
return <Checkbox key={i} value={record.Id} onChange={this.handleChange}>{record.Title}</Checkbox>
});
const multiModalContent = <div>
<p>{question.question.Title}</p>
<p>counter: {counter}</p>
<Checkbox.Group>
{multiAnswers}
</Checkbox.Group>
</div>
dispatch(setModalContent(multiModalContent));
break;
case 'PercentAnswer':
break;
default: <div></div>
break;
}
dispatch(showModal(true));
};
render() {
const { title, question, fileDoc } = this.props;
return (
<Timeline.Item className='ant-timeline-item-right'>
<span style={{ fontSize: '16px', fontWeight: 'bolder' }}>{title}</span> <span className='textAction' onClick={this.showModal}>showModal</span>
{/* <br /> */}
{/* <span>12:00</span> <Icon type="clock-circle" /> */}
</Timeline.Item>
);
}
}
function mapStateToProps(state) {
const {
visibleModal,
counter
} = state.meeting;
return {
visibleModal,
counter
};
}
export default connect(mapStateToProps)(MeetingItem);
Action
export const setCounter = (counter) => (dispatch) => {
return dispatch({ type: actionTypes.SET_COUNTER, counter })
}
You have to use mapDispatchToProps into your connect function.
...
function mapDispatchToProps (dispatch) {
propAction: (action) => dispatch(reduxAction(action))
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(MeetingItem);
...
Follow this tutorial.
Just using mapDispatchToProps inside the connection.