Search code examples
javascriptreactjsreact-routerrenderingreact-hooks

React Hook: The set function of useState does not trigger re-render


I'm currently working on a simplified Trello clone project. I defined a boardList with useState, and implemented two functions create() and remove() which are capable of adding or deleting a board, list, or card. Everything works fine except when deleting a list.

When I click the button to delete a list, the component doesn't re-render instantly. Deleting boards and cards give instant changes. Just in the case of lists, it doesn't seem to re-render although the boardList has been modified to delete the list. The changes are applied when I go to the home page and get back. I'm curious why setBoardList() does not trigger render, only in this specific situation. My functions always provide a new object newBoardList, so it's probably not about returning the same object that react can't recognize.

Below is my App.js and Board.js. Home route shows the boards, and users can click any one of them to go into the BoardPage route and modify the lists and cards within the board. Board.js includes the deleteList() that is related to the problem.

You can see the full project here: https://codesandbox.io/s/upbeat-lederberg-l3e3e

App.js

import React from 'react';
import { HashRouter, Route } from 'react-router-dom';
import { createGlobalStyle } from "styled-components";
import Home from './routes/Home';
import BoardPage from './routes/BoardPage';

const GlobalStyle = createGlobalStyle`
    body{
        padding: 0;
        margin: 0;
        box-sizing: border-box;
        font-family: 'Montserrat';
    }
`;

export const EMPTY =  '-';

function App() {
    const [boardList, setBoardList] = React.useState([]);
    const create = (boardKey, listKey, text) => {
      if(boardKey===EMPTY){ //createBoard
        const check = boardList.filter(board => board.boardName === text);
        if(!check.length && text.length){
            const newBoardList = boardList.concat([{boardKey: text.concat(Date.now()), boardName: text, listList: []}]);
            setBoardList(newBoardList);
        } 
      }
      else if(listKey===EMPTY){ //createList
        const newBoardList = [...boardList];
        newBoardList.forEach(board => {
          if(board.boardKey===boardKey){
            const check = board.listList.filter(list => list.listName === text);
            if(!check.length && text.length){
              board.listList.push({listKey: text.concat(Date.now()), listName: text, cardList: []});
            }
          }
        });
        setBoardList(newBoardList);
      }
      else{ //createCard
        const newBoardList = [...boardList];
        newBoardList.forEach(board => {
          if(board.boardKey===boardKey){
            board.listList.forEach(list => {
              if(list.listKey===listKey){
                const check = list.cardList.filter(card => card.content === text);
                if(!check.length && text.length){
                  list.cardList.push({cardKey: text.concat(Date.now()), content: text});
                }
              }
            });
          }
        });
        setBoardList(newBoardList);
      } 
    };


    const remove = (boardKey, listKey, cardKey) => {
      if(boardKey===EMPTY){
        return;
      }
      else{
        if(listKey===EMPTY){ //removeBoard
          const newBoardList = boardList.filter(board => board.boardKey !== boardKey);
          setBoardList(newBoardList);   
        }
        else if(cardKey===EMPTY){ //removeList
          const newBoardList = [...boardList];
          newBoardList.forEach(board => {
            if(board.boardKey===boardKey){
              board.listList = board.listList.filter(list => list.listKey !== listKey);
            }
          });
          setBoardList(newBoardList);
        }
        else{ //removeCard
          const newBoardList = [...boardList];
          newBoardList.forEach(board => {
            if(board.boardKey===boardKey){
              board.listList.forEach(list => {
                if(list.listKey===listKey){
                  list.cardList = list.cardList.filter(card => card.cardKey !== cardKey);
                }
              });
            }
          });
          setBoardList(newBoardList);
        }
      }
    };
    return (
        <>
            <GlobalStyle />
            <HashRouter>
              <Route path="/" exact={true} render={props => <Home {...props} boardList={boardList} create={create} remove={remove}/>} />
              <Route path="/board/:boardName" render={props => <BoardPage {...props} create={create} remove={remove} />} />
            </HashRouter>
        </>
    );
}

export default App;

Board.js

import React, { useState } from 'react';
import styled from 'styled-components';
import List from './List';
import {EMPTY} from '../App';

export default function Board({boardKey, boardName, listList, create, remove}) {
    const [text, setText] = useState("");
    const createNewList = text => {
        create(boardKey, EMPTY, text);
    };
    const deleteList = (key) => {
        remove(boardKey, key, EMPTY);
    };
    const onChange = e =>{
        setText(e.target.value);
    };
    const onSubmit = e => {
        e.preventDefault();
        createNewList(text);
        setText("");
    };
    return(
        <BoardContainer>
            <h4>{boardName}</h4>
            {listList.map(list => (
                <span key={list.listKey}>
                    <List boardKey={boardKey} listKey={list.listKey} listName={list.listName} cardList={list.cardList} create={create} remove={remove} />
                    <button onClick={()=>deleteList(list.listKey)}>DelL</button>
                </span>
            ))}
            <ListAdder>
                <input type="text" value={text} onChange={onChange} />
                <button onClick={onSubmit}>AddL</button>
            </ListAdder>
        </BoardContainer>
    );

}

const BoardContainer = styled.div`
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: flex-start;
`;

const ListAdder = styled.div`
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    border: 1px solid gray;
`;

Solution

  • The issue is that you are keeping the state in two separate places, making it inconsistent. You are passing board.listList as a location state in the Home component, and this reference will not keep track of changes to the boardList in App.

    I made an example here which seems to work as intended: https://codesandbox.io/s/sharp-mclaren-pv2s2?fontsize=14&hidenavigation=1&theme=dark

    In the code I linked, we are not passing the list in the location state anymore, but passing the boardList to the BoardPage and extracting the proper board from there based on the boardKey. This allows us to always access the current state.

    There are of course multiple ways you can do this and you might prefer a different style than my example. Just make sure to pass the actual state and not create a separate reference to it.