Search code examples
javascriptreactjsreduxreact-hooksredux-toolkit

I can edit certain inputs but not others in React/Redux Toolkit


I'm building a task manager app using Redux Toolkit to manage state. The activeBoard property in the initialState of the board slice. When I import that into the Edit page, I'm able to edit the boardName property in the input but not any of text in the columns array within the activeBoard object. I keep getting an error

"Cannot assign to read only property 'board' of object

The columns array from the activeBoard object are passed into the "columns" useState hook. The strange thing is when I hardcode an array onto the useState hook, I'm able to edit each columns input. Any help would be appreciated.

EditBoardModal.jsx

import React, {useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {editBoard} from "../../reducers/board/boardSlice";
import {hideModal} from "../../reducers/modal/modalSlice";
import Button from "../Button/Button";
import "./EditBoardModal.scss";

const EditBoardModal = () => {
  const dispatch = useDispatch();
  const {activeBoard} =  useSelector(store => store.board);
  const [boardName,
    setBoardName] = useState(activeBoard.name);
  const [columns,
    setColumns] = useState([...activeBoard.columns]);

  const nameChangeHandler = (e) => {
    setBoardName(e.target.value)
  }

  const columnsChangeHandler = (i, e) => {
    let columnsValues = [...columns];
    columnsValues[i][e.target.name] = e.target.value;
    console.log(columnsValues[0][e.target.name]);
    setColumns(columnsValues);
  }


  return (
    <div className="edit-board-modal">
      <h3 className="edit-modal-title">Edit Board</h3>
      <div className="edit-board-name-div">
        <label>Board Name</label>
        <input
          value={boardName}
          onChange={nameChangeHandler}
          className="edit-task-title"
          type="text"
          name="edit board name"
          placeholder="e.g. Web Design"/>
      </div>
      <div className="edit-board-columns-div">
        <label>Board Columns</label>
        {columns.map((column, index) => (
          <div className="edit-columns-item-div" key={index}>
            <input
             onChange={e => columnsChangeHandler(index, e)}
              className="edit-column-input"
              type="text"
              name="board"
              value={column.board}
              placeholder="e.g. Web Design"/>
            <svg onClick={() => deleteColumn(index)} key={index} width="15" height="15" xmlns="http://www.w3.org/2000/svg">
              <g fill="#828FA3" fillRule="evenodd"><path d="m12.728 0 2.122 2.122L2.122 14.85 0 12.728z"/><path d="M0 2.122 2.122 0 14.85 12.728l-2.122 2.122z"/></g>
            </svg>
          </div>
        ))}
      </div>

      <Button
        onClick={() => dispatch(hideModal())}
        text={"+ Add New Column"}
        className={"add-column-subtask"}/>
      <Button
        onClick={() => {
          dispatch(hideModal())
          dispatch(editBoard(boardName));
        }}
        text={"Save Changes"}
        className={"create-save-changes"}/>

    </div>
  )
}

export default EditBoardModal

boardSlice.js

import {createSlice} from "@reduxjs/toolkit";

const boardSlice = createSlice({
  name: "board",
  initialState: {
    boards: [],
    activeBoard: null
  },
  reducers: {
    setActiveBoard: (state, {payload}) => {
      const getBoardByID = state.boards.find(el => el.id === payload);
      state.activeBoard = getBoardByID;
    },
    addBoard: (state, {payload}) => {
      const newBoard = {
        id: payload.id,
        name: payload.name,
        columns: payload.columns
      }
      if (state.boards.length === 0) {
        state.activeBoard = newBoard;
      }
      state.boards.push(newBoard);
    },
    editBoard: (state, {payload}) => {
      state.activeBoard.name = payload;
    },
    deleteCurrentBoard: (state, {payload}) => {
      const deletedBoard = state.boards.filter(item => item.id !== state.activeBoard.id);

      state.boards = deletedBoard;

      state.activeBoard = state.boards[0];
    },
    addColumn: (state, {payload}) => {
      state.activeBoard.columns.push(payload)
    }

  }
});

export const {
  addBoard,
  setActiveBoard,
  deleteCurrentBoard,
  addColumn,
  editBoard
} = boardSlice.actions;

export default boardSlice.reducer;

Solution

  • The columns state is a shallow copy of the activeBoard.columns array, but the elements are all still references to the original array held in the slice.

    const [columns, setColumns] = useState([...activeBoard.columns]);
    

    The columnsChangeHandler function also creates a shallow copy of the columns array but again, the array elements are still original references to the original array in the slice. columnsChangeHandler is attempting to mutate these references.

    const columnsChangeHandler = (i, e) => {
      let columnsValues = [...columns]; // <-- copy of array, but not elements
      columnsValues[i][e.target.name] = e.target.value; // <-- mutate element
      setColumns(columnsValues);
    }
    

    When updating React state you must shallow copy all state (and nested state) that is being updated. Use Array.prototype.map to shallow copy the array and for the matching index create a new object reference and shallow copy the element and override the appropriate property.

    Example:

    const columnsChangeHandler = (i, e) => {
      const { name, value } = e.target;
      setColumns(columns => columns.map((el, index) => index === i
        ? {
          ...el,
          [name]: value,
        }
        : el
      ));
    }