4

Please see this codesandbox.

This codesandbox simulates a problem I am encountering in my production application.

I have an infinite scrolling table that includes checkboxes, and I need to manage the every-growing list of checkboxes and their state (checked vs non-checked). The checkboxes are rendered via vanilla functions (see getCheckbox) that render the React components. However, my checkboxes do not seem to be maintaining the parent state (called state in the code) and clicking a checkbox does not work. What do I need to do to make sure that clicking a checkbox updates state and that all of the checkboxes listen to state? Thanks! Code is also below:

index.js:

import ReactDOM from "react-dom";
import "@elastic/eui/dist/eui_theme_amsterdam_light.css";
import React, { useState, useEffect } from "react";
import { EuiCheckbox, htmlIdGenerator } from "@elastic/eui";
import { arrayRange, getState } from "./utils";

const Checkbox = ({ id, isChecked, onClick }) => (
  <div style={{ margin: "1rem" }}>
    <EuiCheckbox
      id={htmlIdGenerator()()}
      label={isChecked ? `${id} - On` : `${id} - Off`}
      checked={isChecked}
      onChange={() => onClick()}
    />
  </div>
);

const getCheckbox = (props) => <Checkbox {...props} />;

const App = () => {
  const [state, setState] = useState(getState(0, 1));
  const [checkboxes, setCheckboxes] = useState([]);
  const [addMoreCheckboxes, setAddMoreCheckboxes] = useState(true);

  useEffect(() => {
    if (addMoreCheckboxes) {
      setAddMoreCheckboxes(false);

      setTimeout(() => {
        setState((prevState) => ({
          ...prevState,
          ...getState(checkboxes.length, checkboxes.length + 1)
        }));

        const finalCheckboxes = [...checkboxes].concat(
          arrayRange(checkboxes.length, checkboxes.length + 1).map((id) =>
            getCheckbox({
              id,
              isChecked: state[id],
              onClick: () => handleClick(id)
            })
          )
        );

        setCheckboxes(finalCheckboxes);

        setAddMoreCheckboxes(true);
      }, 3000);
    }
  }, [addMoreCheckboxes]);

  const handleClick = (id) =>
    setState((prevState) => ({
      ...prevState,
      [id]: !prevState[id]
    }));

  return <div style={{ margin: "5rem" }}>{checkboxes}</div>;
};

ReactDOM.render(<App />, document.getElementById("root"));

utils.js:

export const arrayRange = (start, end) =>
  Array(end - start + 1)
    .fill(null)
    .map((_, index) => start + index);

export const getState = (start, end) => {
  const state = {};

  arrayRange(start, end).forEach((index) => {
    state[index] = false;
  });

  return state;
};

2 Answers 2

1

There is one main reason your checkboxes are not updating.

It's your checkboxes state variable

  • This variable does not contain the data, but rather contains the processed React JSX elements themselves.
    • Not a wrong practice, but is uncommon. Kind of makes it easy to lose track of where data is actually stored, etc. I'd recommend using useMemo for UI-related memoization instead.
  • Related to previous point. Observe how in useEffect, when you're trying to append new data to the checkboxes variable, you're using the spread operator from the previous checkboxes value
    • Since the checkboxes value are simply processed JSX, they aren't re-rendered or re-processed (they are simply "there", like drawn pictures! No relations to the state at all!)
    • So basically, this operation is just adding new "drawings" of unchecked checkboxes to a previous list of drawn JSXes. So you just get a longer list of immutable, pre-rendered JSX checkboxes stuck in the unchecked state!

So.. to fix this I'd recommend that you separate data state from UI drawings. And maybe use useMemo to help.

I minimally modified your code, and it's located here (https://codesandbox.io/s/cranky-firefly-9ibgr). This should behave like you expect.

Here's the same exact modified code as in the sandbox for convenience.

import ReactDOM from "react-dom";
import "@elastic/eui/dist/eui_theme_amsterdam_light.css";
import React, { useState, useEffect, useMemo } from "react";
import { EuiCheckbox, htmlIdGenerator } from "@elastic/eui";
import { arrayRange, getState } from "./utils";

const Checkbox = ({ id, isChecked, onClick }) => (
  <div style={{ margin: "1rem" }}>
    <EuiCheckbox
      id={htmlIdGenerator()()}
      label={isChecked ? `${id} - On` : `${id} - Off`}
      checked={isChecked}
      onChange={() => onClick()}
    />
  </div>
);

const getCheckbox = (props, key) => <Checkbox {...props} key={key}/>;

const App = () => {
  // The checkbox check/uncheck state variable
  const [state, setState] = useState(getState(0, 1));

  // Checkboxes now just contains the `id` of each checkbox (purely data)
  const [checkboxes, setCheckboxes] = useState([]);
  const [addMoreCheckboxes, setAddMoreCheckboxes] = useState(true);

  useEffect(() => {
    if (addMoreCheckboxes) {
      setAddMoreCheckboxes(false);

      setTimeout(() => {
        setState((prevState) => ({
          ...prevState,
          ...getState(checkboxes.length, checkboxes.length + 1)
        }));

        // Add new ids to the list
        const finalCheckboxes = [...checkboxes].concat(
          arrayRange(checkboxes.length, checkboxes.length + 1)
        );

        setCheckboxes(finalCheckboxes);

        setAddMoreCheckboxes(true);
      }, 3000);
    }
  }, [addMoreCheckboxes]);

  const handleClick = (id) => {
    setState((prevState) => ({
      ...prevState,
      [id]: !prevState[id]
    }));
  };
  
  // use useMemo to check for rerenders. (kind of like, data-driven UI)
  const renderedCheckboxes = useMemo(() => {
    return checkboxes.map((id) => {
      // I'm adding a second argument to `getCheckbox` for its key
      // This is because `React` lists (arrays of JSXs) need keys on each JSX components
      // You could also just skip the function calling altogether
      // and create the <Checkbox .../> here. There's little performance penalty
      return getCheckbox({
        id,
        isChecked: state[id],
        onClick: () => handleClick(id)
      }, id)
    });
  }, [checkboxes, state]);

  return <div style={{ margin: "5rem" }}>{renderedCheckboxes}</div>;
};

ReactDOM.render(<App />, document.getElementById("root"));

P.S. I agree with Oliver's comment that using useEffect with another toggle (addMoreCheckboxes) is quite unorthodox. I would suggest refactoring it to setInterval (or maybe a setTimeout with a boolean condition in a useEffect, in case you want more control).

Sign up to request clarification or add additional context in comments.

Comments

1

The main problem here is that checkboxes is not directly dependent on state (the only time a checkbox is related to state is when a it is initialised with isChecked: state[id]).

This means that even though your state variable updates correctly when a checkbox is clicked, this will not be reflected on the checkbox itself.

The quickest fix here would be to amend the JSX returned by your component so as to directly infer the isChecked property for the checkboxes from the current state:

const App = () => {

    // [...]

    return <div style={{ margin: "5rem" }}>
        {Object.keys(state).map((id) => getCheckbox({
            id,
            isChecked: state[id],
            onClick: () => handleClick(id),
        }))
        }
    </div>;
};

You may however notice now that your checkboxes state variable is becoming rather unnecessary (state being sufficient and holding all the necessary information for rendering all the right checkboxes). So you could consider rewriting your logic without the redundant checkboxes state variable.

As a side note, you are using useEffect() in combination with the addMoreCheckboxes state variable as a kind of timer here. You could simplify that portion of the code through the use of the probably more appropriate setInterval()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.