1

Why does ReactJS remove the last element when the array is different after removing the middle element when using array.splice?

This is my code. I am using React-Redux.

const reducerNotesAndLogin = (state = initialState, action) => {
var tableNotes = "notities";
var tableCategories = "categories";

switch(action.type){
case "CATEGORY_REMOVE":
        // Remove the category
        var newCategories = state.categories;

        console.log("state.categories", state.categories);
        console.log("before: ", {newCategories});
        var index = 0;
        for(var i = 0; i < newCategories.length; i++){
            if(newCategories[i].id === action.payload.categoryId){

                newCategories.splice(i, 1);
                index = i;
                i--;
            }
        }

        console.log("after: ", {newCategories});

        state = {
            ...state,
            categories: newCategories
        }

break;
        default:
            break;
    }

    return state;
}

export default reducerNotesAndLogin;

Output below (I deleted the middle element. My web app always removes the last element of the categories (but not from the array).

Step 1: Initial state

Initial state of app

Step 2: Remove middle item, expecting the middle item to be removed.

Problem

Step 3: Confusion

Why is the array correct, but the view incorrect? I am updating the state.categories correctly right?

This is my render code (as is - without filtering away any other code that mihgt be important)

CategoriesBody:

import React from 'react';
import { connect } from 'react-redux';

import CategoryItem from './CategoryItem';

import Button from './../../Button';
import store from '../../../redux/store-index';

class CategoriesBody extends React.Component {
render(){
    return (
        <div>
            <ul className="list--notes">
                {this.props.categories.map((category) => {
                    if(category.id === undefined){ // No categories
                        return <li>No categories</li>
                        } else {
                            return (
                                <div>
                                    <CategoryItem category={category} />
                                    <div className="mb-small hidden-sm hidden-md hidden-lg"> </div>
                                </div>
                            );
                        }
                    })}
                </ul>
            </div>
        );
    }
}

function mapStateToProps(state){
    return {
        categories: state.reducerNotesAndLogin.categories,
        categoriesLength: state.reducerNotesAndLogin.categories.length
    };
}

export default connect(mapStateToProps)(CategoriesBody);

CategoriesItem.js:

    import React from 'react';
import store from './../../../redux/store-index';
import Button from './../../Button';

class CategoryItem extends React.Component {
    constructor(props){
        super();
        this.state = {
            edit: false,
            categoryName: props.category.categoryName,
            categoryColor: props.category.categoryColor
        }

        this.onClickEdit = this.onClickEdit.bind(this);

        this.onChangeCategoryColor = this.onChangeCategoryColor.bind(this);
        this.onChangeInputCategoryName = this.onChangeInputCategoryName.bind(this);

        this.onClickEditSave = this.onClickEditSave.bind(this);
        this.onClickEditCancel = this.onClickEditCancel.bind(this);
    }

    removeCategory(id, name){
        console.log("nsvbsvbfjvbdjhbvv");
        store.dispatch({ type: "CATEGORY_REMOVE", payload: {
            categoryId: id
        }});

        // store.dispatch({type: "NOTIFY", payload: {
        //     type: 'success',
        //     message: 'Category "' + name + '" removed!'
        // }});
    }

    onClickEdit(){
        this.setState({
            edit: true
        });
    }

    onChangeCategoryColor(e){
        this.setState({
            categoryColor: e.target.value
        });
    }

    onChangeInputCategoryName(e){
        this.setState({
            categoryName: e.target.value
        });
    }

    onClickEditSave(){
        this.setState({
            edit: false,
            categoryName: this.state.categoryName,
            categoryColor: this.state.categoryColor
        });

        store.dispatch({type: "CATEGORY_EDIT", payload: {
            categoryId: this.props.category.id,
            categoryName: this.state.categoryName,
            categoryColor: this.state.categoryColor
        }});

        store.dispatch({type: "NOTIFY", payload: {
            type: "success",
            message: "Category saved!"
        }});
    }

    onClickEditCancel(){
        this.setState({
            edit: false,
            categoryName: this.props.category.categoryName,
            categoryColor: this.props.category.categoryColor
        });
    }

    render(){
        return (
            <li key={this.props.category.id} className={this.state.edit === true ? "mt mb" : "flex-justify-between flex-align-center"}>
                <div className={this.state.edit === true ? "d-none" : ""}>
                    <div className="input--color" style={{
                        backgroundColor: this.state.categoryColor
                        }}>&nbsp;</div>

                    {this.state.categoryName}

                </div>

                {/* Mobile */}
                <div className={this.state.edit === true ? "d-none" : "hidden-sm hidden-md hidden-lg"}>
                    <Button onClick={() => this.onClickEdit()} buttonType="primary">Edit</Button>
                    <div className="mt-small"> </div>
                    <Button onClick={() => this.removeCategory(this.props.category.id, this.props.category.categoryName)} type="primary">Remove</Button>
                </div>

                {/* Tablet and desktop */}
                <div className={this.state.edit === true ? "d-none" : "hidden-xs"}>
                    <div style={{float:'left',}}><Button onClick={() => this.onClickEdit()} buttonType="primary">Edit</Button></div>
                    <div style={{float:'left',marginLeft:'15px'}}><Button onClick={() => this.removeCategory(this.props.category.id, this.props.category.categoryName)} type="primary">Remove</Button></div>
                </div>


                {/* EDITING STATE */}

                <div className={this.state.edit === true ? "" : "d-none"}>
                    <div className="row">
                        <div className="col-xs-12">
                            <input onChange={this.onChangeCategoryColor} className="input--wide" type="color" value={this.state.categoryColor} 
                                style={{backgroundColor: this.state.categoryColor, height: '30px'}}
                            />
                            <input onChange={this.onChangeInputCategoryName} className="input--wide" type="text" value={this.state.categoryName} />
                        </div>
                    </div>
                    <div className="row mt">
                        <div className="col-xs-12">
                            <Button buttonType="primary" onClick={() => this.onClickEditSave()}>Save</Button>
                        </div>
                    </div>
                    <div className="row mt-small">
                        <div className="col-xs-12">
                            <Button buttonType="secondary" onClick={() => this.onClickEditCancel()}>Cancel</Button>
                        </div>
                    </div>
                </div>
            </li>
        )
    }
}

export default CategoryItem;

I think it has something to do with the rendering. Because the arrays are correct when I console.log them. Only the view is different...

1
  • Can you share the rendering code? Commented Apr 23, 2020 at 13:44

2 Answers 2

2

Do not modify the state in reducer directly. Create a copy of state value and then modify it.

Change:

var newCategories = state.categories;

To:

var newCategories = [...state.categories];

You should not modify the same array while looping through it.

for (var i = 0; i < newCategories.length; i++) {
      if (newCategories[i].id === action.payload.categoryId) {
        newCategories.splice(i, 1);
        index = i;
        i--;
      }
    }
Sign up to request clarification or add additional context in comments.

2 Comments

What does newCategories = [...state.categories] do? It makes a copy of the array?
yes, it makes a shallow copy of the array. we-are.bookmyshow.com/…
1

I got the answer after looking through it with a friend of mine. The solution is pretty simple...

Lesson 101: Make sure that you have a unique "key" property when looping through an array in your UI.

The solution is to add this to my code:

<div key={category.id}>
    {this.props.categories.map....
    ...
</div>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.