skip to Main Content

I need a button to be enabled when all the checkboxes in a table are checked and disabled otherwise.

Outer component with button:

import Box from "@mui/system/Box";
import Stack from "@mui/system/Stack";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useState } from "react";
import HRMButton from "../Button/HRMButton";
import ToDoTable from "./ToDoTable";
import { fonts } from "../../Styles";

export default function OnboardingTasks({style}) {
    const tasks = [
        {
            name: 'Discuss and set your first 30/60/90-day goals with your manager',
            done: false
        },
        {
            name: 'Complete personal info & documents on Bluewave HRM',
            done: false
        },
        {
            name: 'Sign and submit essential documents',
            done: false
        }
    ];

    const [allTasksComplete, setAllTasksComplete] = useState(false);

    function checkTasks(tasks) {
        return tasks.every((task) => task.done);
    }

    return (
        <Box sx={{...{
            border: "1px solid #EBEBEB",
            borderRadius: "10px",
            minWidth: "1003px",
            paddingX: "113px",
            paddingY: "44px",
            fontFamily: fonts.fontFamily
        }, ...style}}>
            <h4 style={{textAlign: "center", marginTop: 0}}>Complete your to-do items</h4>
            <p style={{textAlign: "center", marginBottom: "50px"}}>
                You may discuss your to-dos with your manager
            </p>
            <ToDoTable 
                tasks={tasks} 
                onChange={() => setAllTasksComplete(checkTasks(tasks))} 
                style={{marginBottom: "50px"}}
            />
            <Stack direction="row" alignContent="center" justifyContent="space-between">
                <HRMButton mode="secondaryB" startIcon={<ArrowBackIcon />}>Previous</HRMButton>
                //This button needs to be enabled
                <HRMButton mode="primary" enabled={allTasksComplete}>Save and next</HRMButton>
            </Stack>
        </Box>
    );
};

Table component:

import TableContainer from "@mui/material/TableContainer";
import Table from "@mui/material/Table";
import TableHead from "@mui/material/TableHead";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import { styled } from "@mui/system";
import PropTypes from "prop-types";
import Checkbox from "../Checkbox/Checkbox";
import { fonts, colors } from "../../Styles";

export default function ToDoTable({tasks, onChange, style}) {
    //Custom style elements
    const TableHeaderCell = styled(TableCell)({
        color: colors.darkGrey,
        paddingTop: "10px",
        paddingBottom: "10px"
    });

    function handleChange(e, index, value) {
        console.log(tasks);
        tasks[index].done = value;
        onChange();
    }

    return (
        <TableContainer sx={{...{
            minWidth: "1003px",
            fontFamily: fonts.fontFamily
        }, ...style}}>
            <Table>
                <TableHead>
                    <TableRow sx={{backgroundColor: "#F9FAFB"}}>
                        <TableHeaderCell>
                            <b style={{color: colors.grey}}>To-Do</b>
                        </TableHeaderCell>
                        <TableHeaderCell align="right">
                            <b style={{color: colors.grey}}>Done</b>
                        </TableHeaderCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {tasks.map((task, index) => (
                        <TableRow>
                            <TableCell>
                                {task.name}
                            </TableCell>
                            <TableCell align="right">
                                <Checkbox
                                    type="checkbox"
                                    id={`${task.name}-done`}
                                    name={`${task.name}-done`}
                                    value={`${task.name}-done`}
                                    size="large"
                                    onChange={(e) => handleChange(e, index, !task.done)}
                                />
                            </TableCell>
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
        </TableContainer>
    );
};

When using React useState like in the code example given, the entire OnboardingTasks component rerenders every time the AllTasksComplete state is changed which resets the status of all the tasks back to false. When I use React useRef the button is not rerendered and doesn’t react to the change in AllTasksComplete.

How do I enable the button when all the checkboxes are checked while retaining the status of the tasks variable?

Buttons and checkboxes in this example are custom variations from MUI. If you need to run this code in your local machine, leave a comment and I’ll provide the code.

2

Answers


  1. You are missing some React basics. The main issue is that the tasks data you are using is declared inside OnboardingTasks, so any time OnboardingTasks renders for any reason, tasks is redeclared with the done: false properties. tasks should be the React state, and the allTasksComplete should not be in state since it is a derived value from the tasks "source of truth" state.

    Suggested refactor:

    • Convert the tasks to local state
    • Convert allTasksComplete to a memoized derived value
    • Move the change handler into OnboardingTasks and pass down to ToDoTable as props
    const initialState = [
      {
        name: 'Discuss and set your first 30/60/90-day goals with your manager',
        done: false
      },
      {
        name: 'Complete personal info & documents on Bluewave HRM',
        done: false
      },
      {
        name: 'Sign and submit essential documents',
        done: false
      }
    ];
    
    export default function OnboardingTasks({ style }) {
      const [tasks, setTasks] = useState(initialState);
    
      const allTasksComplete = useMemo(() => tasks.every((task) => task.done));
    
      const handleChange = (index, done) => {
        // Use functional state update to shallow copy tasks state, 
        // and then shallow copy the task that is being updated
        setTasks(tasks => tasks.map((task, i) =>
          i === index
            ? { ...task, done }
            : task
        ));
      }
    
      return (
        <Box
          sx={{
            ...style
            border: "1px solid #EBEBEB",
            borderRadius: "10px",
            minWidth: "1003px",
            paddingX: "113px",
            paddingY: "44px",
            fontFamily: fonts.fontFamily
          }}
        >
          <h4 style={{textAlign: "center", marginTop: 0}}>
            Complete your to-do items
          </h4>
          <p style={{textAlign: "center", marginBottom: "50px"}}>
            You may discuss your to-dos with your manager
          </p>
          <ToDoTable 
            tasks={tasks} 
            onChange={handleChange} 
            style={{marginBottom: "50px"}}
          />
          <Stack direction="row" alignContent="center" justifyContent="space-between">
            <HRMButton
              mode="secondaryB"
              startIcon={<ArrowBackIcon />}
            >
              Previous
            </HRMButton>
            //This button needs to be enabled
            <HRMButton
              mode="primary"
              enabled={allTasksComplete}
            >
              Save and next
            </HRMButton>
          </Stack>
        </Box>
      );
    };
    
    • Move TableHeaderCell declaration outside ReactTree so it’s a stable reference and not redeclared each render cycle
    // Custom style elements
    const TableHeaderCell = styled(TableCell)({
      color: colors.darkGrey,
      paddingTop: "10px",
      paddingBottom: "10px"
    });
    
    export default function ToDoTable({ tasks, onChange, style }) {
      return (
        <TableContainer
          sx={{
            ...style,
            minWidth: "1003px",
            fontFamily: fonts.fontFamily
          }}
        >
          <Table>
            <TableHead>
              <TableRow sx={{backgroundColor: "#F9FAFB"}}>
                <TableHeaderCell>
                  <b style={{color: colors.grey}}>To-Do</b>
                </TableHeaderCell>
                <TableHeaderCell align="right">
                  <b style={{color: colors.grey}}>Done</b>
                </TableHeaderCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {tasks.map((task, index) => (
                <TableRow key={index}>
                  <TableCell>{task.name}</TableCell>
                  <TableCell align="right">
                    <Checkbox
                      type="checkbox"
                      id={`${task.name}-done`}
                      name={`${task.name}-done`}
                      value={`${task.name}-done`}
                      size="large"
                      onChange={() => onChange(index, !task.done)}
                    />
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
      );
    };
    
    Login or Signup to reply.
  2. Most of your code is well set, we need to discuss only the below exception.

    The exception:

    Although the intent of the below code is correct, the implementation is incorrect. The code is intended to keep the tasks updated with respect to the user interaction. It is absolutely required. However, implementing the same by changing props is incorrect. It affects the purity of the component. Impure components are inconsistent in rendering. You can read more about it from here : Keeping Components Pure.

    tasks[index].done = value; // handleChange in ToDoTable
    

    Therefore please try refactoring the code to meet intent implemented in the most recommended way. One of the refactored versions may be as below.

    A solution:

    // a) Bring the tasks down into the component ToDoTable
    
    // b) And make it a state over there
    

    Please see below a sample code in the same line in its most basic form.

    App.js

    import { useState } from 'react';
    
    export default function OnboardingTasks() {
      const [allTasksComplete, setAllTasksComplete] = useState(false);
      return (
        <>
          <ToDoTable onCompletingTask={setAllTasksComplete} />
          <button
            disabled={!allTasksComplete}
            onClick={() => console.log('a handler to follow')}
          >
            Save and Next
          </button>
        </>
      );
    }
    
    function ToDoTable({ onCompletingTask }) {
    
      // please note a new property sortorder added
      // and is used to keep the order in rendering
      const initialTasklist = [
        {
          name: 'Discuss and set your first 30/60/90-day goals with your manager',
          done: false,
          sortorder: 1,
        },
        {
          name: 'Complete personal info & documents on Bluewave HRM',
          done: false,
          sortorder: 2,
        },
        {
          name: 'Sign and submit essential documents',
          done: false,
          sortorder: 3,
        },
      ];
      const [tasks, setTasks] = useState(initialTasklist);
    
      function handleClick(updatedTask) {
        // updating tasks state without mutating
        // step 1: filter out the tasks except the currently updated one
        const tasktemp = tasks.filter(
          (tasktemp) => tasktemp.sortorder !== updatedTask.sortorder
        );
        // step 2: Add the currently updated task into the list of other tasks
        tasktemp.push(updatedTask);
        // update the tasks state
        setTasks(tasktemp);
        // update the task completion status in the parent object
        onCompletingTask(tasktemp.filter((task) => !task.done).length == 0);
      }
      return (
        <>
          Tasks:
          <ul>
            {tasks
              .sort((task1, task2) => task1.sortorder - task2.sortorder)
              .map((task) => (
                <li key={task.sortorder}>
                  <label>
                    {task.name}
                    <input
                      type="checkbox"
                      value={task.done}
                      onChange={(e) =>
                        handleClick({
                          name: task.name,
                          done: e.target.checked,
                          sortorder: task.sortorder,
                        })
                      }
                    />
                  </label>
                </li>
              ))}
          </ul>
        </>
      );
    }
    

    Test run:

    On load of the app, the button is disabled

    Browser display on load of the app

    On completing the first task, still the button is disabled

    Browser display - On completing the first task

    On completing the last task, the button is enabled

    Browser display - On completing the last task

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search