htmlreactjscheckboxrefs

React tri-state checkbox indeterminate null error


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 :)


Solution

  • 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