I need to increment quantity of item in cart if item already exists in the state array. In the reducer I got it to work but it needs to be immutable. Because of this, my component doesnt update properly.
This is the code for my reducer:
import { ADD_TO_CART, REMOVE_FROM_CART } from '../actions/types';
const initialState = [];
//Cannot mutate array in reducer
export default function(state = initialState, action){
const { type, payload } = action;
switch(type){
case ADD_TO_CART:
const newState = [...state]
for(var i = 0; i < newState.length; i++){
if(newState[i].item.uid === payload.item.uid){
newState[i].item.qty ++;
return [...newState];
}
}
return [...state, payload];
case REMOVE_FROM_CART:
for(var j = 0; j < state.length; j++){
if(state[j].item.uid === payload.uid){
state[j].item.qty = 1;
}
}
return state.filter(cartItem => cartItem.item.uid !== payload.uid);
default:
return state;
}
}
I need to increment qty if item is already in the array.
The issue I see is with newState[i].item.qty++;, that although you shallowly copied the array, the elements in the array still represent those of the previous state, so post incrementing it mutates the object.
To resolve this issue you should also copy each item into a new object, then update the new object reference.
The following also include a minor optimization to iterate the data once versus twice, by checking for the matching item ID as it copying the state into a new array.
let itemExists = false;
const newState = state.map(item => {
const newItem = { ...item };
if (newItem.item.uid === payload.item.uid) {
itemExists = true;
newItem.item.qty = item.qty + 1;
}
return newItem;
});
if (!itemExists) newState.push(payload);
return newState;
EDIT: Alternative method
const itemIndex = state.findIndex(item => item.item.id === payload.item.id);
if (itemIndex !== -1) {
return state.map((item, i) => ({
...item,
item: {
...item.item,
qty: item.item.qty + (itemIndex === i ? 1 : 0),
}
}));
} else {
return [...state, payload];
}
Related
I am building a react project for visualizing insertion sort using redux. I am using react-redux to create and handle actions. However, the problem is that in my insertionSort algorithm, I dispatch an updateArray action every time the array being sorted changes. I put print statements inside the reducer and saw that the state was in fact changing and the action was being dispatched correctly, however, my actual array does not re-render. I put prints inside the relevant UI component's render() function and saw that it was only being called once or twice rather than every time the reducer receives the action. I tried restructuring my code multiple times and reading about similar problems that people have had but their answers did not help me.
Am I just structuring this the wrong way? Should I not be using dispatches every second or so to update my array?
I have a main.js file which is used to render the UI components including my array:
class Main extends React.Component {
setArray = () => {
this.props.setArray(50, window.innerHeight / 1.4)
startSort = () => {
this.props.startSorting(this.props.algorithm, this.props.array)
}
render() {
let { array} = this.props
return (
<div>
<Navbar
startSort={this.startSort}
setArray={this.setArray}
/>
<MainWrapper>
<Row />
</MainWrapper>
</div>
)
}
}
const mapStateToProps = state => {
return {
array: state.array,
}
}
const mapDispatchToProps = dispatch => {
return {
setArray: (length, height) => {
let array = Array.from({ length: length }, () =>
Math.floor(Math.random() * height))
dispatch(setArray(array))
},
startSorting: (algorithm, array) => {
var doSort
if (algorithm == 'insertionSort') {
doSort = insertionSort
}
doSort(array, dispatch)
}
}
}
My actual array is generated with Row.js
class Row extends React.Component {
generateNodes(array) {
var elements = []
array.forEach((value, index) => {
elements.push(
<CenteredColumn>
<ArrayNode idx={index} value={value} />
</CenteredColumn>
)
})
return elements
}
render() {
let { array } = this.props
console.log('UPDATED ARRAY: ' + array)
var arrayElements = this.generateNodes(array)
return <RowWrapper>{arrayElements}</RowWrapper>
}
}
const mapStateToProps = state => {
return {
array: state.array
}
}
And finally, my actual algoritm is in insertionSort.js in which I import my actions from their reducers and pass in a dispatch function from main.js:
function delayedInsertion(array, dispatch) {
let n = array.length
var i = 0
function loop() {
setTimeout(function() {
var temp = array[i]
var j = i - 1
while (j >= 0 && array[j] > temp) {
array[j + 1] = array[j]
j--
}
array[j + 1] = temp
// console.log('ARRAY: ' + array)
dispatch(updateArray(array))
i++
if (i < n) {
loop()
}
}, 200)
}
loop()
console.log('DONE')
}
It seems that you are mutating your state.
You are passing this.props.array to your doSort action and as I understand your idea correctly, you are just calling delayedInsertion from that action (you did not post source code of that action).
But in delayedInsertion you are mutating the passed array when you are changing positions of you items, here:
while (j >= 0 && array[j] > temp) {
array[j + 1] = array[j]
j--
}
array[j + 1] = temp
You need to perform immutable change of the array.
I have a problem with changing state while splicing element from another array then changes in state. TO be clear, i don't want state to change, just want to splice one element from array arrayWithFilters = []
export const SHOW_RESULTS_WHEN_NOTHING = (state) => {
let selectedFilters = {...state.selected.filters},
arrayWithFilters = []; //Making splice from this array, but it also removes from state.selected.filters.
for (let filter in selectedFilters) {
arrayWithFilters.push(selectedFilters[filter])
}
arrayWithFilters.forEach((filter) => {
if (filter.values.includes('qcontrast')) {
filter.values.splice('qcolor', 1);
console.log(filter)
}
})
}
problem solved with the next code
export const SHOW_RESULTS_WHEN_NOTHING = (state) => {
let selectedFilters = {...state.selected.filters},
arrayWithFilters = [];
let selectedFiltersCopy = JSON.parse(JSON.stringify(selectedFilters));
arrayWithFilters = Object.values(selectedFiltersCopy);
arrayWithFilters.forEach((filter) => {
if (filter.values.includes('qcontrast')) {
filter.values.splice('qcontrast', 1);
}
})
console.log(arrayWithFilters)
}
I have a handler which is meant to update the 'quantity' property of my 'selected' array state. I have multiple fields for different items of which the quantities have to be updated by the user.When I am trying to update the state though, I get that maximum depth has been reached. When I use console.log, I get over 1000 calls of the handler somehow.. Maybe someone could guide me in what I am doing wrong. Here is the code:
{
this.state.purchase.selected.map(item => {
return (
<Grid item xs={4}>
<OrderItemsCard
item={item}
onChange={this.handleSelectedItemChange(item)} />
</Grid>
)
})
}
this.state = {
purchase: {
selected: [],
comments: ''
},
}
this.state.purchase.selected = []
for (let i = 0; i <= data.length - 1; i++) {
for (let j = 0; j <= items.length - 1; j++) {
if (this.props.data[i] === this.props.items[j]._id) {
this.state.purchase.selected.push(this.props.items[j])
}
}
}
handleSelectedItemChange = (item) => {
let selected = Object.assign([], this.state.purchase.selected)
selected.forEach(selectedItem => {
selectedItem.quantity = item.quantity
})
this.setState({selected})
}
Instead of:
onChange={this.handleSelectedItemChange(item)}
which directly calls the handler and passes its result as the event handler, you want to pass a function instead:
onChange={() => this.handleSelectedItemChange(item)}
And instead of shallow copying:
let selected = Object.assign([], this.state.purchase.selected)
you want to deep copy there:
let selected = this.state.purchase.selected.map(selected => {...selected});
I have the following, which works fine. It generates a unique random number for a given empty array and a Max determined by the another array (data) length. I would like to add a check that does:
when the array length is = MaxN, I want to store the last value of the array inside a variable so that if it is = to a new random generated number I will call "generateRandomNumber(array, maxN)" again.
const generateRandomNumber = (array, maxN, lastN) => {
let randomN = Math.floor(Math.random() * maxN) + 0;
console.log(lastN)
if(lastN == randomN) {
// do your thing
}
if(array.includes(randomN)) {
return generateRandomNumber(array, maxN, lastN);
}
if(array.push(randomN) == maxN) {
lastN = array.length - 1
array.length = 0;
}
return randomN
}
export default generateRandomNumber
however I am always getting undefined inside the console.log. I am passing lastN like so:
let lastN;
I would think that that value which is undefined at first would later get updated inside:
if(array.push(randomN) == maxN) {
lastN = array.length - 1
array.length = 0;
}
component where generateRandomNumber is used:
...
const utilityArray = []
const tempQuestions = []
let lastN
class App extends Component {
constructor(props) {
super(props);
this.state = {
collection: gridItemsCollection,
intro: false,
instructions: false,
grid: true,
questions: this.props.questions,
selectedQuestion: ""
}
}
getRandomN = (arr, max, lastN) => {
let s = generateRandomNumber(arr, max, lastN)
return s
}
hideGridItem(e) {
let //index = this.getRandomN(utilityArray, gridItemsCollection.length),
collection = this.state.collection,
newCollection,
//updatedGridItem = collection[index].hidden = true,
questions = this.state.questions.questions,
n = this.getRandomN(tempQuestions, questions.length, lastN);
console.log(lastN)
// this.setState({
// newCollection: [ ...collection, updatedGridItem ]
// })
// if(this.getAnswer(e)) {
this.generateNewQuestion(questions[n])
// }
// else {
// console.log('no')
// }
}
generateNewQuestion(selectedQuestion) {
this.setState({
selectedQuestion
})
}
componentDidMount = () => {
const questions = this.state.questions.questions
let randomNumber = this.getRandomN(tempQuestions, questions.length, lastN)
this.generateNewQuestion(questions[randomNumber])
}
getAnswer = (e) =>
e.target.getAttribute('data-option') == this.state.selectedQuestion.correct_option
render() {
const state = this.state
const { collection, grid, intro, selectedQuestion } = state
console.log(tempQuestions)
return (
<div className="wrapper">
<div className="wrapper-inner">
<View isVisible={state.intro}>
<p> intro screen </p>
</View>
<View isVisible={state.grid}>
<Grid gridItemsCollection={collection}/>
<Question question={selectedQuestion.question} />
<Controls
onClick={this.hideGridItem.bind(this)}
gridItemsCollection={collection}
answers={selectedQuestion}
answer={selectedQuestion.correct_option}
/>
</View>
</div>
</div>
);
}
}
export default App;
It looks like you declare lastN but it is never actually declared for the first time. This means the first time you access it, it'll always be undefined.
You have two options to solve this:
Define lastN to some suitable default value (I would think something like -1 may be suitable based on the code presented).
let lastN = -1;
Just ignore it. From your code, it doesn't look like lastN being undefined should even be a problem since the only check you do is lastN == randomN, which will always be false if lastN is undefined.
It looks like it should get updated, but possibly not on the first call. It looks like it depends on how many questions you have. Unless you have 1 question, it won't update for a few tries. Also, if you have zero questions, it'll never update (since array.push() will be 1 and maxN will be 0).
When you use lastN as a parameter, the local variable lastN takes precedence over global lastN and you are actually updating the local variable, not the global. Just change your argument name.
const generateRandomNumber = (array, maxN, lastN) => {
let randomN = Math.floor(Math.random() * maxN) + 0;
console.log(lastN)
if(lastN == randomN) {
// do your thing
}
if(array.includes(randomN)) {
return generateRandomNumber(array, maxN, lastN);
}
if(array.push(randomN) == maxN) {
lastN = array.length - 1 //-> Here you update the local copy, the global is not affected. So rename you argument to... say lastNArg
array.length = 0;
}
return randomN
}
export default generateRandomNumber
And don't forget to initialize your global lastN var like: let lastN = 0;
So, here it is modified:
const generateRandomNumber = (array, maxN, lastNArg) => {
let randomN = Math.floor(Math.random() * maxN) + 0;
console.log(lastNArg)
if(lastNArg == randomN) {
// do your thing
}
if(array.includes(randomN)) {
return generateRandomNumber(array, maxN, lastNArg);
}
if(array.push(randomN) == maxN) {
lastN = array.length - 1; //make sure lastN is initialized and you have access to it
array.length = 0;
}
return randomN
}
export default generateRandomNumber
I've been trying to debug weird issue and I've finally figured out why it's happening. Just not sure how to prevent it (; I have this function:
getInfo(id) {
id = id || "zero";
let i = routeDefinitions.findIndex(r => Boolean(r.name.toLowerCase().match(id)));
// console.log(i) - works in plunker
// but in my app sometimes returns -1...
let current = routeDefinitions[i];
let next = routeDefinitions[i + 1] ? routeDefinitions[i + 1] : false;
let prev = routeDefinitions[i - 1] ? routeDefinitions[i - 1] : false;
return { prev, current, next };
}
..it works perfectly in this plunker, but in my app I use its return value to update app state (custom implementation of redux pattern). When I send return value through this function:
private _update(_old, _new) {
let newState = Object.keys(_new)
.map(key => {
if (_old[key] === undefined) {
_old[key] = _new[key];
} else if (typeof _new[key] === "object") {
this._update(_old[key], _new[key]);
} else {
_old[key] = _new[key];
}
return _old;
})
.find(Boolean);
return Object.assign({}, newState || _old);
}
..routeDefinitions array is mutated and things start to break... I've tried couple of things:
let current = [...routeDefinitions][i];
// and:
return Object.assign({}, { prev, current, next });
..but it didn't work. How can I prevent mutatation of routeDefinitions array?
EDIT: I've managed to reproduce the error in this plunker
routeDefinitions array is mutated and things start to break
If your function is truly:
getInfo(id) {
id = id || "zero";
let i = routeDefinitions.findIndex(r => Boolean(r.name.toLowerCase().match(id)));
// console.log(i) - works in plunker
// but in my app sometimes returns -1...
let current = routeDefinitions[i];
let next = routeDefinitions[i + 1] ? routeDefinitions[i + 1] : false;
let prev = routeDefinitions[i - 1] ? routeDefinitions[i - 1] : false;
return { prev, current, next };
}
Then routeDefinitions is not mutated. Something else is mutating routeDefinitions.
I solved this by modifying _update() like this:
private _update2(_old, _new) {
let newState = {};
Object.keys(_new)
.map(key => {
if (_old[key] === undefined) {
newState[key] = _new[key];
} else if (typeof _new[key] === "object") {
newState[key] = this._update2(_old[key], _new[key]);
} else {
newState[key] = _new[key];
}
return newState;
})
.find(Boolean);
return Object.assign({}, _old, newState);
}
I use old state just to check values, don't modify it until later when _update() is finished.
Plunker