I am trying to increment the productQuantity of an item that has been pushed into an array. If the productID of the item matches that of an item already in the array, the quantity should be increased.
export function ADD_ITEM(state, product) {
// state.isAdded = true;
const added = state.storeCart.find(product => product ===
product.productID)
if (!added) {
state.storeCart.push(product)
} else {
product.productQuantity++
}
It looks like there are a few issues.
The product argument in your find callback function is shadowing the product argument from the ADD_ITEM function. We'll change this to p.
In the find callback, you should check if the productID of p is equal to that of the provided product.
It seems that you just want to push the entire product onto the storeCart, not push each individual property.
You should increment productQuantity on added since that's a ref to the actual existing item in state.
export function ADD_ITEM(state, product) {
const added = state.storeCart.find(p => p.productID === product.productID);
if (!added) {
state.storeCart.push(product);
} else {
added.productQuantity++;
}
}
Just to demonstrate that this is functional, here's a bare-bones example.
function ADD_ITEM(state, product) {
const added = state.storeCart.find(p => p.productID === product.productID);
if (!added) {
state.storeCart.push({...product});
} else {
added.productQuantity++;
}
}
const state = {
storeCart: []
}
const hat = { productID: 1, name: "hat", productQuantity: 1 };
const jacket = { productID: 2, name: "jacket", productQuantity: 1 };
const shoes = { productID: 3, name: "shoes", productQuantity: 1 };
ADD_ITEM(state, hat);
ADD_ITEM(state, jacket);
ADD_ITEM(state, shoes);
ADD_ITEM(state, jacket);
console.log(state);
Related
Hi everyone!
I have a question that I hope you can help me with.
I just started with React Native and I'm working on a simple name generator.
I have an array with different names in it.
When I click on the button, a random number is generated. This number is associated with the array's list of names.
This all works, but I'm getting duplicate names. I would like to go through the whole list without there being a duplicate name. When all names have been passed, the list starts again.
I was thinking of making a separate array that keeps track of the numbers that have passed. And then exclude those numbers. But I'm not sure how to add this and if this is the right way.
See below my code.
Apologies if this is a bit messy or cumbersome.
import React, { useState } from "react";
import { StyleSheet, Text, View, Button } from "react-native";
export default function GirlScreen() {
const RandomNumber = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
const [count, setCount] = useState(0);
const onPress = () => {
setCount(RandomNumber(1, 100));
};
const random = RandomNumber(1, 5);
var differentNames = {
namesContainer: {
names: [
{ name: "(1) Sophie", id: 1 },
{ name: "(2) Emma", id: 2 },
{ name: "(3) Lisa", id: 3 },
{ name: "(4) Esmée", id: 4 },
{ name: "(5) Zoe", id: 5 },
],
},
};
function findLinkByName(random) {
for (const item of differentNames.namesContainer.names) {
if (item.id === random) {
return item.name;
}
}
}
return (
<View style={styles.countContainer}>
<Text style={styles.name}>{findLinkByName(random)}</Text>
<Button onPress={onPress} title="Next Name" />
</View>
);
}
const styles = StyleSheet.create({
countContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
name: {
color: "black",
fontSize: 30,
},
});
You could keep track of two states. One holds already selectedNames and the other one holds still availableNames as follows.
const [selectedNames, setSelectedNames] = useState([])
const [availableNames, setAvailableNames] = useState([
{ name: "(1) Sophie", id: 1 },
{ name: "(2) Emma", id: 2 },
{ name: "(3) Lisa", id: 3 },
{ name: "(4) Esmée", id: 4 },
{ name: "(5) Zoe", id: 5 },
])
Then, we choose a random number between 0 and the length of avialableNames which represents the index we want to pick from avialableNames.
const random = RandomNumber(0, availableNames.length - 1);
Then, your onPress function looks as follows.
const onPress = () => {
setAvailableNames(availableNames.filter(n => n !== availableNames[random]))
setSelectedNames([...selectedNames, availableNames[random]])
};
We add the new randomly picked name to selectedNames and remove it from availableNames at the same time.
Your findLinkByName function could look as follows.
function findLinkByName(random) {
if (availableNames.length === 0) {
setAvailableNames(selectedNames.sort((a, b) => a.id - b.id))
setSelectedNames([])
return availableNames[0]
}
return availableNames[random].name
}
As long as there are names in availableNames, that is its length is not equal to 0, we just pick it and return its name. If all avialable names have been selected, we reset the states, sort the selectedNames by their id prop and return the first name of the list again.
Here is a working snack.
if I understand you want to select without return
let arr1 = ['a', 'b', 'c'];
let value;
Option1: Copy the array to temp array
let arr2 = [...arr1];
let random_index = Math.floor(Math.random() * arr2.length);
value = arr2[random_index];
arr2 = arr2.filter((val, index) => index !== random_index);
if (arr2.length === 0)
arr2 = [...arr1];
Option2: Save the indexes of the array
let arr2 = Array.from(Array(arr1.length).keys());
let random_index = Math.floor(Math.random() * arr2.length);
value = arr1[arr2[random_index]];
arr2 = arr2.filter((val, index) => index !== random_index);
if (arr2.length === 0)
arr2 = Array.from(Array(arr1.length).keys());
Option 1: Quick and Easy
Create an object in state to track the used name IDs.
const [usedIds, setUsedIds] = useState([]);
Then update the findLinkByName function to use this array. You should also invoke the random number generator inside the function.
function findLinkByName() {
// clear array if full
if(usedIds.length === differentNames.namesContainer.names.length) {
setUsedIds([]);
}
// find unique ID
let randomId;
do {
randomId = RandomNumber(1,5);
} while(usedIds.includes(randomId));
// add used ID to array
setUsedIds(prev => [...prev, randomId]);
// return random name
return differentNames.namesContainer.names.find(n => n.id === randomId);
}
Option 2: Move Names to State
You can also simply append a used property to each name object in the name container and change it to be stored in state so that we can mutate it. The ugliest part of this is that the names are kept 3 levels deep in the object. If that can be lifted up, then the following statements could be much shorter.
const [names, setNames] = useState({
namesContainer: {
names: [
{ name: "(1) Sophie", id: 1, used: false },
{ name: "(2) Emma", id: 2, used: false },
{ name: "(3) Lisa", id: 3, used: false },
{ name: "(4) Esmée", id: 4, used: false },
{ name: "(5) Zoe", id: 5, used: false },
],
},
});
I'd also recommend using const and let over var for various reasons 😉
Then your findLinkByName function can be updated to work much more efficiently like this:
function findLinkByName() {
// clear array if full
if(names.namesContainer.names.filter(n => !n.used).length === 0) {
let newNames = {...names};
newNames.namesContainer.names.map(n => {...n, used: false});
setNames(newNames);
}
// find random ID
const unusedNames = names.filter(n => !n.unsed);
const randId = Math.floor(Math.random() * unusedNames.length);
// update state
let newNames = {...names};
newNames.namesContainer.names.map(n => {
return (n.id === randId) ? {...n, used: true} : {...n}
});
setNames(newNames);
// return random name
return names.namesContainer.names.find(n => n.id === randId);
}
There is a function that adds objects product to products in state
addProductToCart(productId, productName)
{
var product = {
id: productId,
name: productName,
};
this.setState({
products: {
...this.state.products,
[product.id]: product
}
});
}
but these objects are sorted by [product.id]. How do I sort them in the order they are added to the cart?
maintain one more array ids in which, you can append the productIds as they are added :
addProductToCart(productId, productName)
{
var product = {
id: productId,
name: productName,
};
this.setState({
products: {
...this.state.products,
[product.id]: product
},
productIds : [...ids, product.id]
});
}
You can then iterate over the array to retrieve the product in the order of their insertion.
If you read this objects are by default key value pairs,
You have 2 options you can use #Easwar solution or you can use array instead to store.
As far as I see your strucutre there is nothing wrong in using the array structure for your requirement.
You should restructure your state like this
constructor() {
super();
this.addProductToCart = this.addProductToCart.bind(this);
this.state = {
products: []
};
}
addProductToCart() {
productsId--;
var product = {
id: productsId,
name: 'test',
};
this.setState({
products: [...this.state.products,product]
});
}
Demo
As I can see you have a problem getting removed object from array you can use this easily
removefromCart(value) {
var array = [...this.state.products]; // make a separate copy of the array
var index = array.findIndex(a => a.id === value);
if (index !== -1) {
array.splice(index, 1);
this.setState({ products: array });
}
}
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
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]
The relevant Redux state consists of an array of objects representing layers.
Example:
let state = [
{ id: 1 }, { id: 2 }, { id: 3 }
]
I have a Redux action called moveLayerIndex:
actions.js
export const moveLayerIndex = (id, destinationIndex) => ({
type: MOVE_LAYER_INDEX,
id,
destinationIndex
})
I would like the reducer to handle the action by swapping the position of the elements in the array.
reducers/layers.js
const layers = (state=[], action) => {
switch(action.type) {
case 'MOVE_LAYER_INDEX':
/* What should I put here to make the below test pass */
default:
return state
}
}
The test verifies that a the Redux reducer swaps an array's elements in immutable fashion.
Deep-freeze is used to check the initial state is not mutated in any way.
How do I make this test pass?
test/reducers/index.js
import { expect } from 'chai'
import deepFreeze from'deep-freeze'
const id=1
const destinationIndex=1
it('move position of layer', () => {
const action = actions.moveLayerIndex(id, destinationIndex)
const initialState = [
{
id: 1
},
{
id: 2
},
{
id: 3
}
]
const expectedState = [
{
id: 2
},
{
id: 1
},
{
id: 3
}
]
deepFreeze(initialState)
expect(layers(initialState, action)).to.eql(expectedState)
})
One of the key ideas of immutable updates is that while you should never directly modify the original items, it's okay to make a copy and mutate the copy before returning it.
With that in mind, this function should do what you want:
function immutablySwapItems(items, firstIndex, secondIndex) {
// Constant reference - we can still modify the array itself
const results= items.slice();
const firstItem = items[firstIndex];
results[firstIndex] = items[secondIndex];
results[secondIndex] = firstItem;
return results;
}
I wrote a section for the Redux docs called Structuring Reducers - Immutable Update Patterns which gives examples of some related ways to update data.
You could use map function to make a swap:
function immutablySwapItems(items, firstIndex, secondIndex) {
return items.map(function(element, index) {
if (index === firstIndex) return items[secondIndex];
else if (index === secondIndex) return items[firstIndex];
else return element;
}
}
In ES2015 style:
const immutablySwapItems = (items, firstIndex, secondIndex) =>
items.map(
(element, index) =>
index === firstIndex
? items[secondIndex]
: index === secondIndex
? items[firstIndex]
: element
)
There is nothing wrong with the other two answers, but I think there is even a simpler way to do it with ES6.
const state = [{
id: 1
}, {
id: 2
}, {
id: 3
}];
const immutableSwap = (items, firstIndex, secondIndex) => {
const result = [...items];
[result[firstIndex], result[secondIndex]] = [result[secondIndex], result[firstIndex]];
return result;
}
const swapped = immutableSwap(state, 2, 0);
console.log("Swapped:", swapped);
console.log("Original:", state);