0

I have a component that renders multiple checkboxes. Each checkbox is handled dynamically with their checked property to be assigned while rendering. I need to print/render the values of the checkboxes selected. This is how a sample state looks like:

{
        Filters: [
          {
            name: "Vegetables",
            options: [
              {
                value: "tmto",
                name: "Tomato"
              },
              {
                value: "ptato",
                name: "Potato"
              }
            ]
          },
          {
            name: "Fruits",
            options: [
              {
                value: "ornge",
                name: "Orange"
              },
              {
                value: "grps",
                name: "Grapes"
              }
            ]
          }
        ],
      selected: []
    }

I am mapping a category name first and then again mapping the checkboxes with respective values and names, something like:

Vegetables
checkbox 1
checkbox 2

Fruits
checkbox 3
checkbox 4

I have a working snippet for the same https://codesandbox.io/s/react-functional-component-forked-wb1ew but not sure why it is unable to set the state which matches the checkbox selected. I am a bit confused about how to handle this nested checkboxes state.

Any help to resolve this is highly appreciated.

3 Answers 3

2

I forked your CodeSandbox and applied the fixes. You can find a working solution below.

https://codesandbox.io/s/react-functional-component-forked-gmwv0


There was quite a bit that needed to be changed, but I'll try to cover it as best as I can.

  1. You were referencing this.setState in your handleCheckboxChange, which doesn't work in a functional component. In a functional component, you use the 2nd argument of your useState hook, which in this case is equal to setState. Functional components don't use this for any internal methods.
  2. There was invalid syntax inside of your render function. You need to make sure you're always returning a JSX object with a single top level wrapping component.
  3. The rest of the issues were inside of your handleCheckboxChange. Due to the asynchronous nature of setState, it is best to use the callback inside of setState which would be setState((prevState) => {}) which will make sure you don't have stale state. You'll also need to return an object from this which will be your new state.
  4. I updated the value of the checkbox (checked) to use the selected value that you calculate within this function.
  5. The final thing I did was update the mapping to properly return the correctly formatted state.

Final Solution

const App = (props) => {
  const [state, setState] = useState({
    Filters: [
      {
        name: "Vegetables",
        options: [
          {
            value: "tmto",
            name: "Tomato"
          },
          {
            value: "ptato",
            name: "Potato"
          }
        ]
      },
      {
        name: "Fruits",
        options: [
          {
            value: "ornge",
            name: "Orange"
          },
          {
            value: "grps",
            name: "Grapes"
          }
        ]
      }
    ],
    selected: []
  });

  const handleCheckboxChange = (value) => {
    setState((state) => {
      const updatedEtables = state.selected.find((obj) => obj === value);
      const selected = updatedEtables
        ? state.selected.filter((obj) => obj !== value)
        : [...state.selected, value];

      return {
        selected,
        Filters: [
          ...state.Filters.map((filter) => {
            return {
              ...filter,
              options: filter.options.map((ele) => {
                return {
                  ...ele,
                  checked: selected.includes(ele.value)
                };
              })
            };
          })
        ]
      };
    });
  };

  return (
    <div>
      {state.Filters.map((ele) => {
        return (
          <React.Fragment>
            <h6>{ele.name}</h6>
            {ele.options.map((item) => {
              return (
                <div>
                  <input
                    checked={item.checked || false}
                    onChange={() => handleCheckboxChange(item.value)}
                    type="checkbox"
                  />
                </div>
              );
            })}
          </React.Fragment>
        );
      })}

      <strong>Selected: </strong>
      <br />
      <span>{state.selected.join(",")}</span>
    </div>
  );
};
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks a lot for the help mate. I did fix those functional and mapping issues. Was about to update the sandbox, meanwhile, you helped out too :) Can you please help me understand the use-cases where the use of spread operator vs callback inside setState()?
This is nice and your summary is good but not very helpful to future visitors if the link changes or goes down over time. Would you be able to include enough code in the answer itself to stand as a resource on its own? OP as well has hidden most of the relevant code behind a link which is not really in line with the standalone resource curation-oriented approach of SO.
I've updated my answer to include the inline code. @bubble-cord if your new state value is calculated based off of the current/previous state value, you will want to use the callback approach to make sure that you are always calculating your new state from the most up to date state. If you are setting state and don't need to calculate the old state value, then you can simply pass in the new state value. If you have more questions, then you'll either need to create a new question or search for existing answers.
@Chris Hi. Can you help me out with another question somewhat related to this only?
@bubble-cord extended discussions and additional questions within comments are generally discouraged within the Stack Overflow community. I have contact information on my profile if you'd like to discuss further.
|
1

The linked code throws an error because the {...} notation must always be sandwiched between tags.

The following is invalid JSX:

return (
  <h1>Hello World!</h1>
  {data}
);

JSX must always have one single root component. If you want to return multiple root level components wrap them in a React.Fragment.

The above invalid example should be written as:

return (
  <React.Fragment>
    <h1>Hello World!</h1>
    {data}
  </React.Fragment>
);

This makes sure there is only one single root component which solves your error.


Applying this to your code would me changing:

return (
  <div>
    {state.Filters.map(ele => {
      return (
        <h6>{ele.name}</h6>
        {ele.options.map(item => {
          return (
            <div>
              <input 
                checked={item.checked}
                onChange={() => handleCheckboxChange(item.value)}
                type="checkbox"
              />
            </div>
          )
        })}
      )
    })}
    <strong>Selected: </strong>
    <br />
    <span>{state.selected.join(",")}</span>
  </div>
);

into:

return (
  <div>
    {state.Filters.map(ele => {
      return (
        <React.Fragment>
          <h6>{ele.name}</h6>
          {ele.options.map(item => {
            return (
              <div>
                <input 
                  checked={item.checked}
                  onChange={() => handleCheckboxChange(item.value)}
                  type="checkbox"
                />
              </div>
            )
          })}
        </React.Fragment>
      )
    })}
    <strong>Selected: </strong>
    <br />
    <span>{state.selected.join(",")}</span>
  </div>
);

Notice the use of React.Fragment. You can also use the newer short syntax (<>...</>) which looks cleaner (but lacks some tool support). You can also further clean up the code by using the implicit return value of arrow functions.

return (
  <div>
    {state.Filters.map(ele => (
      <>
        <h6>{ele.name}</h6>
        {ele.options.map(item => (
          <div>
            <input 
              checked={item.checked}
              onChange={() => handleCheckboxChange(item.value)}
              type="checkbox"
            />
          </div>
        ))}
      </>
    ))}
    <strong>Selected: </strong>
    <br />
    <span>{state.selected.join(",")}</span>
  </div>
);

Comments

0

As helped by @Chris and precious inputs by @3limin4t0r, I am compiling the final working code here, as suggested by @ggorlen, for someone who might come across the same issue or problem in future:

import React, { useState } from "react";
import { render } from "react-dom";

const App = (props) => {
  const [state, setState] = useState({
    Filters: [
      {
        name: "Vegetables",
        options: [
          {
            value: "tmto",
            name: "Tomato"
          },
          {
            value: "ptato",
            name: "Potato"
          }
        ]
      },
      {
        name: "Fruits",
        options: [
          {
            value: "ornge",
            name: "Orange"
          },
          {
            value: "grps",
            name: "Grapes"
          }
        ]
      }
    ],
    selected: []
  });

  const handleCheckboxChange = (value) => {
    setState((state) => {
      const updatedEtables = state.selected.find(
        (obj) => obj === value
      );
      const selected = updatedEtables
        ? state.selected.filter(
            (obj) => obj !== value
          )
        : [...state.selected, value];

      return {
        selected,
        Filters: [
          ...state.Filters.map((filter) => {
            return {
              ...filter,
              options: filter.options.map((ele) => {
                return {
                  ...ele,
                  checked: selected.includes(ele.value)
                };
              })
            };
          })
        ]
      };
    });
  };

  return (
    <div>
      {state.Filters.map((ele) => {
        return (
          <React.Fragment>
            <h6>{ele.name}</h6>
            {ele.options.map((item) => {
              return (
                <div>
                  <input
                    checked={item.checked || false}
                    onChange={() => handleCheckboxChange(item.value)}
                    type="checkbox"
                  />
                </div>
              );
            })}
          </React.Fragment>
        );
      })}

      <strong>Selected: </strong>
      <br />
      <span>{state.selected.join(",")}</span>
    </div>
  );
};

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

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.