React tri-state checkbox indeterminate null error

2k views Asked by At

I am trying to implement a triple-state checkbox to react. The repeated clicking on the checkbox would cycle through blank->checked->crossed->blank->... like this. With all look around and searching for answers, I came along a reference: Indeterminate checkbox in React JSX.

I tried something like this:

export default function App() {
  const [state, setState] = React.useState({
    data: [
      {
        id: "1",
        one: 0
      },
      {
        id: "2",
        one: 0
      }
    ]
  });
  const indetSetter = React.useCallback(
    (el, id) => {

      if (el) {
        if (el.id === id && state.data.map((datum) => datum.one === 2)) {
          el.indeterminate = true;
        }
      }
    },
    [state]
  );

  const advance = (id, e) => {
    setState((state) => {
      
      state.data.map((datum) =>
        datum.id === id ? { ...datum, one: (datum.one + 1) % 3 } : { ...datum }
      );
    });

  return (
    <>
      {state.data.map((item) => {
        // console.log("show", item);
        return (
          <input
            key={item.id}
            id={item.id}
            type="checkbox"
            checked={item.one === 1}
            ref={(el) => indetSetter(el, item.id)}
            onChange={(e) => advance(item.id, e)}
          />
        );
      })}
    </>
  );
}

But eventually it throws me an error: can't access property "indeterminate", el is null or can't access property "data", state is undefined.

Working snippet: https://codesandbox.io/s/relaxed-hill-qqb4y

Any help to resolve the same is highly appretiated.

Thanks in advance :)

1

There are 1 answers

2
diedu On BEST ANSWER

As I commented, you need to encapsulate the indeterminate logic in a separate component to be able to update the dom when value changes via useEffect as explained here

This is the component for a single indeterminate checkbox

IndeterminateInput.js

import React, { useCallback, useEffect, useRef } from 'react';

export default function IndeterminateInput({ id, name, value, onChange }) {
  const el = useRef(null);

  const onChangeHandler = useCallback(
    event => {
      onChange({ id, name, value: (value + 1) % 3 });
    },
    [id, name, value, onChange],
  );

  useEffect(() => {
    el.current.checked = value === 1;
    el.current.indeterminate = value === 2;
  }, [value]);

  return (
    <input type="checkbox" name={name} ref={el} onChange={onChangeHandler} />
  );
}

And then you render one IndeterminateInput for each item in your main component passing the necessary props

export default function App() {
  const [state, setState] = ...

  const onChange = useCallback((item) => {
    const { id, value } = item;
    setState((prevState) => {
      return {
        data: prevState.data.map((datum) => {
          return id === datum.id
            ? {
                ...datum,
                one: value
              }
            : datum;
        })
      };
    });
  }, []);

  return (
    <>
      {state.data.map((item, i) => {
        return (
          <IndeterminateInput
            key={item.id}
            id={item.id}
            name={item.name}
            value={item.one}
            onChange={onChange}
          />
        );
      })}
    </>
  );
}

you can see it working in here: https://codesandbox.io/s/stoic-euler-e97so?file=/src/App.js