I'm using react reducer to handle a request for a deleting an item from the list:
case 'REMOVE_ITEM': {
let products = state.products!
for( var i = 0; i < products.length; i++){
if ( products[i].id === action.payload) {
products.splice(i, 1);
}
}
let result = {...state, products: products, productSelected: products[0]}
localStorage.setItem('state', JSON.stringify(result))
console.log(result)
return { ...state, products: products, productSelected: products[0]}
}
When I click the first item everything works great, but when I delete other items, my state updating and console.log(result) work fine, but there are no updates to localstorage, so I assume that setItem is not launching.
I would greatly appreciate if someone could help me with this issue.
In React, the state is immutable. In simple terms it means that you should not modify it directly. Instead a new object should be created to set the state using setState.
The splice() methods mutate an array.
filter() does not mutate the array on which it is called. It is better to do the following method :
const deleted=state.products.filter((item)=>item.id !=== action.payload)
return { ...state, products: deleted}
And if you want to store something in localstorage, it is better to use the lifecycle in the react:
componentDidUpdate() for class component
useEffect for functional component
I am guessing that, when you are clicking it on the second time it is causing problem because of the synchronous nature of localstorage .
Try this
const asyncLocalStorage = {
setItem: function (key, value) {
return Promise.resolve().then(function () {
localStorage.setItem(key, value);
});
},
getItem: function (key) {
return Promise.resolve().then(function () {
return localStorage.getItem(key);
});
}
};
case 'REMOVE_ITEM': {
let products = state.products
for( var i = 0; i < products.length; i++){
if ( products[i].id === action.payload) {
products.splice(i, 1);
}
}
let result = {...state, products: products, productSelected: products[0]}
asyncLocalStorage.setItem('state', JSON.stringify(result))
console.log(result);
return { ...result};
}
Reference : Link
Related
I'm trying to run a function inside a reducer function to calculate a cart total, but the value in the state object is the function and not the result of the function. After render, the result is displayed, but I cannot pass the object to other components (I'm using a context). How do I do it? Here is the code (assume that the values work, because they do).
I've tried setting it to the const to no avail. I've tried an anonymous function that returns the function's total, and it still doesn't work. I've also tried just calling the function.
const reducer = (cart, action) => {
switch(action.type) {
case("ADD_ITEM"):
return {
...cart,
products: {
...cart.products,
[action.payload.product.id]: {...action.payload.product}
},
total: () => (cartTotal)
}
break
case("REMOVE_ITEM"):
delete cart.products[action.payload]
return {
...cart,
products: {
...cart.products
},
total: () => (cartTotal)
}
break
case("CLEAR_CART"):
return {
cart: {
...initialState
}
}
break
}
}
Here is the object:
cart: {
products: [{}],
total: 0
}
Here is the function to return the total:
const cartTotal = () => {
const total = Object.values(cart.products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price
return prev + currPrice
}, 0)
return total.toFixed(2)
}
Right now, I'm passing the method that allows you to calculate the total, but it seems like it is unnecessary, as I'm watching the cart state and updating the value of total each time items are added/removed. How do I set the value of a property inside of the reducer function as the return of another helper function? Thanks!
I don't know why are you storing derived state, well, in state? This should be computed via a selector when reading your state out (and/or passed to a custom Context provider).
If you must store the total in state then you need to call the cartTotal function to be able to store its return value. Unfortunately this will only compute the cart total on the unupdated cart since you are currently in the function that returns the new cart state.
You can factor out the cart update so you have an updated cart products object, and with a small revision of cartTotal it can consume this updated cart products object and compute a total.
Example:
const cartTotal = (products) => {
const total = Object.values(products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price;
return prev + currPrice;
}, 0);
return total.toFixed(2);
};
Cases
case "ADD_ITEM": {
const { payload } = action;
const products = {
...cart.products,
[payload.product.id]: { ...payload.product },
}
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
case "REMOVE_ITEM": {
const products = { ...cart.products };
delete products[action.payload];
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
You need to call cartTotal to calculate the value.
total: cartTotal()
In a previous question, I was given an answer on how to update an array, which was achieved in the following way:
onClick(obj, index) {
if (data.chosenBets[index]) {
// Remove object.
data.chosenBets.splice(index, 1);
} else {
// Add object.
data.chosenBets.splice(index, 0, obj);
}
}
This does not trigger a re-render in my UI. How do I update the array (in the same way as above) while triggering a re-render?
Just mutating a state won't trigger re-render. You need to call setState() function:
// class component
onClick = () => {
// update state
this.setState(newState);
}
// functional component
...
const [ state, setState ] = useState();
...
setState(newState);
Also, it's quite important to perform immutable state updates since React relies on refs usually (especially, when using memo() or PureComponent). So, it's better to create new instance of array with the same items.
onClick(obj, index) {
let newData;
if (data.chosenBets[index]) {
newData = data.slice();
newData.chosenBets.splice(index, 1);
} else {
newData = [ obj, ...data ];
}
setState(newData);
}
And you always can use some libraties for immutable update like immer, object-path-immutable etc.
Try avoiding impure functions when writing react codes. Here, splice is an impure method. I would recommend using the below code:
onClick(obj, index) {
if (this.state.data.chosenBets[index]) {
// Remove object.
const data = {
...this.state.data,
chosenBets: this.state.data.chosenBets.filter((cBet, i) => i !== index)
};
this.setState({ data });
} else {
// Add object.
const data = {
...this.state.data,
chosenBets: [ ...this.state.data.chosenBets, obj ]
};
this.setState({ data });
}
}
I am assuming you have that array already saved in your state. Then you can do something like this:
onClick = (idx) => {
let arr = [...this.state.arrayToModify];
arr.splice(idx,1);
this.setState({ arrayToModify: arr });
}
Hope this helps!
I needed to make a copy of the array:
let arr = appState.chosenBets
arr.splice(index, 1)
appState.chosenBets = arr
Rather than simply doing
data.chosenBets.splice(index, 1);
I'm loading data from saved session using:
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
this.setState({
cartItems: JSON.parse(localStorage.getItem('savedData')),
totalPrice: this.getPriceOnLoad(),
totalItems: this.getItemsOnLoad(),
});
}
}
cartItems is an array of objects. Which seems is updated before
this.getPriceOnLoad();
this.getItemsOnLoad();
functions are called, for example this.getPriceOnLoad function:
getPriceOnLoad() {
let itemsPrice = 0;
for (let i = 0; i <= this.state.cartItems.length - 1; i++) {
itemsPrice += this.state.cartItems[i].quantity * this.state.cartItems[i].price;
}
return itemsPrice;
}
but, in getPriceOnLoad function, this.state.cartItems.length is equal to 0, so for loop is not executing. I can see in React dev tools that this array has some length. Is it because componentDidMount() is executing state change synchronously and can't see updated array immediately? So my question is how could i update price and quantity of items after array is initialized?
Your state is not updated in order. For this, you could store the cartItems in a temporary value, and send it to each functions :
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems, //Short syntax for 'cartItems: cartItems'
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
}
}
You could also make your function significantly shorter by using reduce:
this.setState({
cartItems,
totalPrice: cartItems.reduce((total, item) => total + (item.quantity * item.price), 0),
totalItems: cartItems.reduce((total, item) => total + item.quantity, 0),
});
Can you show us your second function too ? It may be optimized as well. Done.
the wrong thing that you are doing is trying to use values from the state on your functions that will define your state.
you have 2 approaches to solve this:
1) use the callback function from setState and then set the state again with the new data (which into my opinion is not the best approach)
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems
}, ()=> {
this.setState({
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
})
}
}
2) send the values to your functions
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const savedCartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems,
totalPrice: this.getPriceOnLoad(savedCartItems),
totalItems: this.getItemsOnLoad(savedCartItems),
});
}
}
getPriceOnLoad() is executed before this.setState is executed. So you cannot refer to this.state in getPriceOnLoad().
When you call this.setState({}), JS first needs to generate the object for the setState() function. Means the functions you are referring to run first, then this.setState().
And in any case this.setState() is an asynchronous function, so this.state is not directly available after setState() execution.
i'm having hard time figure out this. Have component which is search filter and pushes all selected filters into url. Everything works like it should except in case of refresh, in that case reducer is updated for selected filter with array with single item in which i have all selected items, not spreaded into array.
f.e. i have url
myexampleapp.com/alltrips?tripType=short_walk,cycling,downhill_cycling,long_walks&season=spring,summer,alle,vinter&lengthTo=50
my reducer
// ------------------------------------
// Constants
// ------------------------------------
export const UPDATE_FILTERS = 'UPDATE_FILTERS';
// ------------------------------------
// Actions
// ------------------------------------
const updateFilter = (key, value) => ({
type: UPDATE_FILTERS,
payload: {
key,
value
}
});
// ------------------------------------
// Action creators
// ------------------------------------
export const updateFilterState = (key, value) => {
return dispatch => {
dispatch(updateFilter(key, value));
};
};
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
tripType: [],
season: [],
tripsTo: undefined,
tripsFrom: undefined
};
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const key = action.payload.key;
const value = action.payload.value;
if (key === 'tripsFrom' || key === 'tripsTo') {
return Object.assign({}, state, { [key]: value });
} else {
var newFilter = state[key].slice();
var ttIdx = state[key].indexOf(value);
if (ttIdx !== -1) {
newFilter.splice(ttIdx, 1);
} else {
newFilter.push(value);
}
}
console.log(newFilter);
return Object.assign({}, state, { [key]: newFilter });
}
default:
return state;
}
}
console.log returns array with 1 element in which have array with 5 elements. but i want that 5 ekements to be in parrent array.
and i'm parsing URL
componentDidMount () {
let {
location: { search },
updateFilterState
} = this.props;
search = search.slice(1);
var queries = search.split('&');
queries.forEach(q => {
var tmp = q.split('=');
if (tmp[0] && tmp[1]) {
if (tmp[0].toLowerCase() === 'triptype') {
updateFilterState(tmp[0], tmp[1].split(','));
console.log(tmp[1].split(','));
} else if (tmp[0].toLowerCase() === 'tripsto') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'tripsfrom') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'season') {
updateFilterState(tmp[0], tmp[1].split(','));
}
}
});
this.updateQuery(this.props);
}
So everything works except when i want to refresh.
Pretty new with all this, and been stuck for almost 3 days with this. Hope you understand what im trying to ask here as i'm pretty new and non-english speaker, so i don't know all the terms so i can better express myself. Can someone give me some pointers?
If I'm not mistaken you are feeding the reducer with an array for season and tripType. So, when you try to update those values, you are not actually spreading that array. This is your value parameter. Hence, if you do this you will have a parent array with your desired result:
newFilter.push(...value);
... is ES6's spread syntax. So we are spreading our array and pushing it into our newFilter.
But again if I don't see it wrong you will have problems with this code since you are not checking the existence of your values right. You are looking indexOf something but if you really feeding your reducer with an array, for which one you are looking this index?
Here is a cleaner way of doing this if I'm not mistaken what you are trying to do here:
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const { key, value } = action.payload;
if (key === 'tripsFrom' || key === 'tripsTo') {
return { ...state, [key]: value };
}
const newFilter = Array.isArray(value)
? [ ...new Set( [ ...state[key], ...value ] ) ]
: [ ...new Set( [ ...state[key], value ] ) ];
return { ...state, [key]: newFilter};
}
default:
return state;
}
}
Some differences with your code:
I am using spread syntax instead of Object.assign.
Instead of checking all the existence values (iterating the array and doing some logic) I'm using here Set object. It creates an object of unique values of what we give it. So I am cheating here and spreading our old state with spreading our value into an array, give this to our Set, and again at the top level spreading it again into an array. If you don't do the last spread you will get an object but here we want an array.
I'm looking for a pure function, to modify my immutable state object. The original state given as parameter must stay untouched. This is especially useful when working with frameworks like Redux and makes working with immutable object in javascript much easier. Especially since working with the object spread operator using Babel is already possible.
I did not found anything better than first copy the object, and than assign/delete the property I want like this:
function updateState(state, item) {
newState = {...state};
newState[item.id] = item;
return newState;
}
function deleteProperty(state, id) {
var newState = {...state};
delete newState[id];
return newState;
}
I feel like it could be shorter
Actions on state, where state is considered immutable.
Adding or Updating the value of a property:
// ES6:
function updateState(state, item) {
return Object.assign({}, state, {[item.id]: item});
}
// With Object Spread:
function updateState(state, item) {
return {
...state,
[item.id]: item
};
}
Deleting a property
// ES6:
function deleteProperty(state, id) {
var newState = Object.assign({}, state);
delete newState[id];
return newState;
}
// With Object Spread:
function deleteProperty(state, id) {
let {[id]: deleted, ...newState} = state;
return newState;
}
// Or even shorter as helper function:
function deleteProperty({[id]: deleted, ...newState}, id) {
return newState;
}
// Or inline:
function deleteProperty(state, id) {
return (({[id]: deleted, ...newState}) => newState)(state);
}
An ES6 solution, that has a bit more support is Object.assign:
const updateState = (state, item) => Object.assign({}, state, { [item.id]: item });
In a Map Function
To do this process within a map function (remove an attribute and add a new attribute on each object), given an array of objects -
const myArrayOfObjects = [
{id: 1, keyToDelete: 'nonsense'},
{id: 2, keyToDelete: 'rubbish'}
];
Delete the attribute keyToDelete, and add a new key newKey with the value "someVar".
myArrayOfObjects.map(({ keyToDelete, ...item}) => { ...item, newKey:'someVar'});
Updating the array to
[
{id: 1, newKey:'someVar'},
{id: 2, newKey:'someVar'}
]
See this great post for more information on the deletion method.
Instead of writing boilerplate code (as answered above: (({[id]: deleted, ...state}) => state)(state)) which is hard to read, you could use some library to do the same:
https://github.com/cah4a/immutable-modify
https://github.com/kolodny/immutability-helper
https://github.com/M6Web/immutable-set
https://github.com/bormind/immutable-setter
For example:
import {remove} from 'immutable-modify'
function updateState(state, item) {
return remove(state, item.id)
}
It's also supports any nested updates:
import {set} from 'immutable-modify'
function updateState(state, item) {
return set(state, 'user.products', (products) => ({
...products,
items: products.items.concat(item),
lastUpdate: Date.now()
}))
}
Try:
const { id, ...noId } = state;
And test:
console.log(noId);
Removing item from an array, just use filter ;)
CASE 'REMOVE_ITEM_SUCCESS':
let items = state.items.filter(element => element._id !== action.id);
return {
...state,
items
}