Johnnybossboy
Johnnybossboy

Reputation: 311

ReactJS rendering issue with edited array

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...

Upvotes: 1

Views: 369

Answers (2)

Johnnybossboy
Johnnybossboy

Reputation: 311

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>

Upvotes: 1

Jagrati
Jagrati

Reputation: 12222

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--;
      }
    }

Upvotes: 2

Related Questions