How do I set the value of a property via a function? - javascript

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()

Related

localstorage setItem() is not working in some cases

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

How can I dynamically write a reducer (useReducer hook)?

I am trying to write a dynamic reducer to loop over a list and setting each item in this list a counter.
I am not sure I am doing it right - mainly in the section where I set it a value of ' ' (and can't dynamically name it or set the value I want initially (each will have a different value))
const reducer = (state, action) => {
switch(action.type) {
case 'SET_COUNTER':
return {
...state,
[`counter${action.id}`]: action.payload
}
default:
return state
}
}
//is the below correct?
let [{ counter }, dispatchReducer] = useReducer(reducer, {
counter: '',
})
I am then looping over an array of objects to create different counters (e.g. counter0, counter1, counter2 ...) and set each of them a value
//this dispatch is not working
useEffect(() => {
availableTimes.map(item =>
dispatchReducer({
type: 'SET_COUNTER',
id: item.id,
payload: counts[`${item.time}`]
})
)
}, [])
The payload comes from an object which I am using to count the instances of a time. E.g. if "2230" appears 3 times, this object will have "2230": 3
const counts = {}
extractedTiesm.forEach(x => {
counts[x] = (counts[x] || 0) + 1
})
//console.log(counts["2230"]) --> 3

Adding key value pairs to redux store

I am trying to use redux to add key value pairs to my store. However, Im not sure how to accomplish this. In short, i am retrieving data from firebase, I want to add that data to my redux store, but I have to do it one item at a time. My desired structure of my state object is something like this:
reminders
- reminder key 1
- reminder title
- reminder date 1
- reminder key 2
- reminder title
- reminder date 1
and so on.
But I cant figure out how to add children to my state.reminders object
Here is my action:
const fetchReminders = (uid) => async dispatch => {
firebaseReminders.child(uid).orderByChild("date").on("value", snapshot => {
snapshot.forEach(function(child) {
console.log(child.val())
dispatch({
type: 'fetchReminders',
value: child.val(),
key: child.key
});
})
});
};
so this would dispatch the action for every single item that I retrieve from the database, and then in my reducer I want to add that item to the state tree using action.key as the key. Currently I have
const remindersReducer = (state = initialState, action) => {
switch(action.type) {
case "fetchReminders":
return Object.assign({}, state, {
reminders: action.value
});
default: return state;
}
};
which is not correct. How can I add a child node to my state.reminders object with the key of action.key, and the value of action.value
let initialState = {
reminders: {}
}
const remindersReducer = (state = initialState, action) => {
switch(action.type) {
case "fetchReminders":
return Object.assign({}, state, {
reminders: {
...state.reminders,
[action.key]: action.value
}
});
default: return state;
}
};
let state1 = remindersReducer(initialState, {
type: 'fetchReminders',
key: 'reminderKey1',
value: 'reminderValue1'
});
console.log(state1)
let state2 = remindersReducer(state1, {
type: 'fetchReminders',
key: 'reminderKey2',
value: 'reminderValue2'
});
console.log(state2)
let state3 = remindersReducer(state2, {
type: 'fetchReminders',
key: 'reminderKey3',
value: 'reminderValue3'
});
console.log(state3)
The snippet should help you achieve what you want to do.
You can assign an object as the key of action.key by using the following format:
{
[action.key]: action.value
}
Its called Computed Property Names.
Starting with ECMAScript 2015, the object initializer syntax also
supports computed property names. That allows you to put an expression
in brackets [], that will be computed and used as the property name.
Source

React class component methods: is my code imperative?

I'm new to react and as well to the terms of functional, imperative, declarative. And I get to know that pure function is easy to test. I am self taught to program with Javascript. So far, it is working but my goal is to learn to write clean and maintainable code.
my question is the method addProductToSaleList below is bad and untestable because it is imperative? and how can I do it differently.
class SaleComponent extends React.Component {
addProductToSaleList = (values, dispatch, props) => {
//filter product from productList
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
if (productFound) {
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.filter(detail => {
if (productFound.name === detail.product) {
return detail
}
return undefined
})[0]
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
} else {
alert('The product code you add is not exist in product list');
}
}
render() {
// Render saleList
}
}
I belive this question should go to Code Review, but I will give it a shot. Part of the code can be improved
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
First, filter function receives a callback and for each item that callback will be executed. If the callback returns a value interpreted as true, it will return the item in the new array the function will build. Otherwise, it will skip that item. Assuming you're trying to find one item in the code, you could use the function find which will return you that element directly (no need for [0]), or undefined if that item is not found. So your code could be rewrite to
const productFound = props.productList.find(product => values.productCode === product.code.toString());
Note: No IE support.
Then, if the value was not found, you could just alert and do an early return. (You might also want to handle errors differently, with a better format than plain alert).
The code would look like
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// rest of the function
in order to find details, you can use find method as well
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
and then just call the rest of the code
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
Another improvement:
You're receiving a dispatch function as a parameter, but you're not using it. So you could remove it from function's declaration
(values, props) => { ... }
And you could split the last part into two different functions, something like
const getAction = details => `${detailFound ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = detailFound;
return {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
};
}
and then just call
props.dispatcher(getAction(details), getObject(details, productFound));
The end result would look like
addProductToSaleList = (values, props) => {
//filter product from productList
const productFound = props.productList.find(product => values.productCode === product.code.toString());
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
const getAction = details => `${details ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = details;
return {
...rest,
qty: parseInt(details.qty, 10) + 1
};
}
props.dispatcher(getAction(details), getObject(details, productFound));
}
my question is the method addProductToSaleList below is bad and
untestable because it is imperative
Well your code is testable, there are no external dependencies. So you could pass mocked values and props and add unit tests to that. That means, passing a fake values and props (they are just plain js object) and make assertions over that.
For instance:
You could mock dispatcher function and given the fake values in productList and saleItem.details you could see if dispatcher is called with the proper values. You should test different combinations of that
Mock alert function (Again, I would use another UI approach) and verify it is called, and that no other code is called (asserting that your fake dispatcher is not called). Something like this:
let actionToAssert;
let objectToAssert;
let values = { productCode: 'somecode' };
let props = {
productList: // your item listm with id and price, name, etc,
saleItem: {
details: // your details array here
}
dispatcher: (action, newObject) => {
actionToAssert = action;
objectToAssert = newObject;
}
}
addProductToSaleList(values, props); // make here assertions over actionToAssert and objectToAssert

How to replace an object property value in redux

I am trying to create an online shop using redux. I have got it so that a person can add an item to their basket. However, having difficulty with adding quantity. I have a method that works that wont let someone add the same product twice, I just need to now make it increase the quantity for that same product.
My basket state is stored as an array of objects.
This is my basket reducer:
const initialState = [];
const isProductInBasket = (state, action) => {
for (var i=0; i < state.length; i++){
if(state[i].product.id == action.data.product.id){
return true;
}
}
}
export default (state = initialState, action) => {
switch(action.type) {
case "ADD_TO_BASKET":
if (isProductInBasket(state, action)) {
for (var i=0; i < state.length; i++){
if(state[i].product.id = action.data.product.id){
console.log(state, 'stst');
const basketState = state[i].product.quantity + 1;
return {...state, basketState}; //problem is here
}
}
}
else {
const basketState = [].concat(state).concat(action.data)
return basketState;
break;
}
default:
return state
};
};
Clearly what im doing is wrong as im returning an object, but im wondering how i can return that new object in place of the old object. i need to return it as an object but inside an array...
just to be uber clear, when I have this:
{name: "Rucksack", price: "15.00", id: 1, quantity: 0}
and I click add to basket, it should then come back as:
{name: "Rucksack", price: "15.00", id: 1, quantity: 1}
I'd recommend reading this section of the Redux docs - it shows you how to update an individual element in an array without mutation.
Effectively, what you need to do is create a new array that has a modified copy of your basket item. When you need to perform a transformation on an array without mutating, Array.prototype.map is your friend:
if (isProductInBasket(state, action)) {
return state.map(product => {
if (product.id == action.data.product.id) {
return { ...product, quantity: product.quantity + 1 };
}
return product;
});
}
You could use findIndex to check if the object already exists and update it else push the payload data into state
switch(action.type) {
case "ADD_TO_BASKET":
const index = state.findIndex(productData => productData.product.id === action.data.product.id);
if(index > -1) {
return [
...state.slice(0, index),
{
...state[index],
product: {
...state.product,
quantity: state[index].product.quantity + 1
}
},
...state.slice(index + 1)
]
}
return [...state, action.data]

Categories