3

I have two React components, namely, Form and SimpleCheckbox. SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.

In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].

My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)

Interestingly, when I log the checks it gives(after api call resolves):

{Pain: false}

What I expect is:

{
  Information: false,
  Investigation: false,
  Transaction: false,
  Pain: false,
}

Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:

{
  Pain: false,
  Information: true,
  Investigation: true,
}

Here is the components:

const Form = () => {
  const [checks, setChecks] = useState({});
  const [categories, setCategories] = useState([]);

  const handleCheckChange = (isChecked, category) => {
    setChecks({ ...checks, [category]: isChecked });
  }

  useEffect(() => {
    api
      .getCategoryNames()
      .then((_categories) => {
        setCategories(_categories);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  return (
    {categories.map(category => {
       <SimpleCheckbox 
         label={category}
         onCheck={handleCheckChange}
         key={category}
         id={category}
       />
    }
  )
}

const SimpleCheckbox = ({ onCheck, label, id }) => {
  const [check, setCheck] = useState(false);

  const handleChange = (event) => {
    setCheck(event.target.checked);
  };

  useEffect(() => {
    onCheck(check, id);
  }, [check]);

  return (
    <FormControl>
      <FormControlLabel
        control={
          <Checkbox checked={check} onChange={handleChange} color="primary" />
        }
        label={label}
      />
    </FormControl>
  );
}
Cagri Uysal
  • 255
  • 3
  • 11

2 Answers2

1

It looks like you're controlling state twice, at the form level and at the checkbox component level.

I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning

import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "@material-ui/core";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <Form />
    </div>
  );
}

const Form = () => {
  const [checks, setChecks] = useState({
    Information: false,
    Investigation: false,
    Transaction: false,
    Pain: false
  });
  const [categories, setCategories] = useState([]);
  console.log("checks", checks);
  console.log("categories", categories);
  const handleCheckChange = (isChecked, category) => {
    setChecks({ ...checks, [category]: isChecked });
  };

  useEffect(() => {
    // api
    //   .getCategoryNames()
    //   .then(_categories => {
    //     setCategories(_categories);
    //   })
    //   .catch(error => {
    //     console.log(error);
    //   });
    setCategories(["Information", "Investigation", "Transaction", "Pain"]);
   
  }, []);

  return (
    <>
      {categories.map(category => (
        <SimpleCheckbox
          label={category}
          onCheck={handleCheckChange}
          key={category}
          id={category}
          check={checks[category]}
        />
      ))}
    </>
  );
};

const SimpleCheckbox = ({ onCheck, label, check }) => {
  return (
    <FormControl>
      <FormControlLabel
        control={
          <Checkbox
            checked={check}
            onChange={() => onCheck(!check, label)}
            color="primary"
          />
        }
        label={label}
      />
    </FormControl>
  );
};

If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state

  const fetchChecks = async () => {
    let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
    setCategories(categoriesFromAPI);
    let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
      acc[cur] = false
      return acc
    }, {})
    setChecks(initialChecks)
   
  }
  useEffect(() => {
    fetchChecks()
  }, []);

I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.

let categoriesFromApi = await axios.get(url)

Lastly, set your initial slice of state to an empty object

const [checks, setChecks] = useState({});
Hyetigran
  • 1,150
  • 3
  • 11
  • 29
1

What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.

So after changing:

const handleCheckChange = (isChecked, category) => {
    setChecks({ ...checks, [category]: isChecked });
  }

to

const handleCheckChange = (isChecked, category) => {
    setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
  }

It has started to work as I expected.

Cagri Uysal
  • 255
  • 3
  • 11