Search code examples
javascriptreactjsuse-effectuse-statereact-functional-component

Change React child component's state from an onClick event in parent, with callbacks and hooks (useState, useEffect)


My parent component contains an array of categories. The component renders a list, and each list item has a checkbox, which is a child component.

I use useState() for the checkedCategories array, and for the checked/unchecked state of the the child component Checkbox.

  • If I check a checkbox, the category is added to the list. The checkbox child component's state (checked) is updated in the child component (Checkbox.js)
  • If I uncheck a checkbox, the category is removed from the list. The checkbox child component's state (checked) is updated in the child component (Checkbox.js).
  • I have a "clear all" button that updates the parent's checkedCategories state.
  • Every time the checkedCategories array is updated, I trigger a console.log with the useEffect hook, and this works for all three cases.

There is one detail left: when the "clear all" button is clicked, all the checkboxes should be unchecked. So I have to manipulate the checked state of all the Checkbox children somehow.

Parent component:

import {useState, useEffect} from "react";

import Checkbox from "../functions/Checkbox.js";

function CategoryList() {

    const categories = ['CategoryA','CategoryB', 'CategoryC']

    const [checkedCategories, setCheckedCategories] = useState([]);

    const addToCheckedCategories = id => {
        const updatedCheckedCategories = [...checkedCategories];
        updatedCheckedCategories.push(id);
        setCheckedCategories(updatedCheckedCategories);
    };

    const removeFromCheckedCategories = id => {
        const updatedCheckedCategories = checkedCategories.filter(cat => cat !== id);
        setCheckedCategories(updatedCheckedCategories);
    };

    const removeFilters = () => {
        //????
    }

    useEffect(() => {

        console.log('checked categories updated');
        console.log(checkedCategories);

        if (!checkedCategories.length) {
            console.log('the array is empty');

            //Set all the checkboxes' checked state to "false" somehow...?

        }

    }, [checkedCategories]);

    return(
        <div>
            <ul>
                {categories.map(categories =>
                     <li key={categories.toLowerCase()}>
                         <Checkbox id={categories.toLowerCase()}
                               label={categories}
                               addToCheckedCategories={addToCheckedCategories}
                               removeFromCheckedCategories={removeFromCheckedCategories}
                         />        
                    </li>
                 )}
            </ul>
           <button onClick={removeFilters}>Clear all</button>
      </div>
    )
}

export default CategoryList;

Child component:

import { useState } from 'react';

   function Checkbox({id, label, addToCheckedCategories, removeFromCheckedCategories}) {

   const [checked, setChecked] = useState(false);

   const handleChange = id => {

        if (checked) {
            removeFromCheckedCategories(id);
            console.log('removed ' + id);

        } else {
            addToCheckedCategories(id);
            console.log('added ' + id);

        }
        setChecked(!checked);
        console.log('changed value of checkbox');
    }

    return(
        <label htmlFor={id} >
            <input type="checkbox"
                   name="category-input"
                   id={id}
                   onChange={handleChange}
            />
            {label}
        </label>

    );
}

export default Checkbox;

Solution

  • I would lift the state completely to the parent making the Checkbox component stateless:

    function Checkbox({
      id,
      label,
      checked,
      addToCheckedCategories,
      removeFromCheckedCategories,
    }) {
      const toggle = () => {
        if (checked) {
          removeFromCheckedCategories(id);
        } else {
          addToCheckedCategories(id);
        }
      };
    
      return (
        <label htmlFor={id}>
          <input
            type="checkbox"
            name="category-input"
            id={id}
            onChange={() => toggle()}
            checked={checked}
          />
          {label}
        </label>
      );
    }
    

    From your parent you can pass down the checked property simply checking if that category is present in the checkedCategories array.

    function CategoryList() {
      const categories = ['CategoryA', 'CategoryB', 'CategoryC'];
    
      const [checkedCategories, setCheckedCategories] = useState([]);
    
      const addToCheckedCategories = (id) => {
        const updatedCheckedCategories = [...checkedCategories];
        updatedCheckedCategories.push(id);
        setCheckedCategories(updatedCheckedCategories);
      };
    
      const removeFromCheckedCategories = (id) => {
        const updatedCheckedCategories = checkedCategories.filter(
          (cat) => cat !== id
        );
        setCheckedCategories(updatedCheckedCategories);
      };
    
      // Remove filters is as easy as setting an empty array
      const removeFilters = () => {
        setCheckedCategories([]);
      };
    
      useEffect(() => {
        console.log('checked categories updated');
        console.log(checkedCategories);
      }, [checkedCategories]);
    
      return (
        <div>
          <ul>
            {categories.map((category) => (
              <li key={category.toLowerCase()}>
                <Checkbox
                  id={category.toLowerCase()}
                  label={category}
                  checked={checkedCategories.includes(category.toLowerCase())}
                  addToCheckedCategories={addToCheckedCategories}
                  removeFromCheckedCategories={removeFromCheckedCategories}
                />
              </li>
            ))}
          </ul>
          <button onClick={removeFilters}>Clear all</button>
        </div>
      );
    }
    

    Using this approach clearing all is very easy, all you have to do is setting the checkedCategories array to an empty one.

    Demo: https://stackblitz.com/edit/react-pwxgaq?file=src%2FApp.js