Edoardo Trotta
Edoardo Trotta

Reputation: 117

React useState with Object with multiple boolean fields

I have this object initialised with useState:

const [
    emailNotifications,
    setEmailNotifications,
  ] = useState<emailNotifications>({
    rating: false,
    favourites: false,
    payments: false,
    refunds: false,
    sales: false,
  });

And I created a function that should dynamically change the value for each field but I am struggling with assigning the opposite boolean value onClick. This is the function:

const handleEmailNotificationsSettings = (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    setEmailNotifications({
      ...emailNotifications,
      [event.target.id]: !event.target.id,
    });
  };

What am I doing wrong?

Upvotes: 2

Views: 3673

Answers (2)

Linda Paiste
Linda Paiste

Reputation: 42288

@kunal panchal's answer is totally valid Javascript, but it does cause a Typescript error because the type of event.target.id is string so Typescript does not know for sure that it's a valid key of emailNotifications. You have to assert that it is correct by using as.

!emailNotifications[event.target.id as keyof EmailNotifications]

One way to avoid this is to get the boolean value by looking at the checked property on the input rather than toggling the state.

As a sidenote, it's a good best practice to get the current state by using a setState callback so that you always get the correct value if multiple updates are batched together.

const _handleEmailNotificationsSettings = (
  event: React.ChangeEvent<HTMLInputElement>
) => {
  setEmailNotifications(prevState => ({
    ...prevState,
    [event.target.id]: event.target.checked,
  }));
};

This is probably the best solution for a checkbox.


Another approach which is more flexible to other situations is to use a curried function. Instead of getting the property from the event.target.id, where it will always be string, we pass the property as an argument to create an individual handler for each property.

const handleEmailNotificationsSettings = (
  property: keyof EmailNotifications
) => () => {
  setEmailNotifications((prevState) => ({
    ...prevState,
    [property]: !emailNotifications[property]
  }));
};

or

const handleEmailNotificationsSettings = (
  property: keyof EmailNotifications
) => (event: React.ChangeEvent<HTMLInputElement>) => {
  setEmailNotifications((prevState) => ({
    ...prevState,
    [property]: event.target.checked
  }));
};

which you use like this:

<input
  type="checkbox"
  checked={emailNotifications.favourites}
  onChange={handleEmailNotificationsSettings("favourites")}
/>

Those solutions avoid having to make as assertions in the event handler, but sometimes they are inevitable. I am looping through your state using (Object.keys(emailNotifications) and I need to make an assertion there because Object.keys always returns string[].

import React, { useState } from "react";

// I am defining this separately so that I can use typeof to extract the type
// you don't need to do this if you have the type defined elsewhere
const initialNotifications = {
  rating: false,
  favourites: false,
  payments: false,
  refunds: false,
  sales: false
};

type EmailNotifications = typeof initialNotifications;

const MyComponent = () => {
  // you don't really need to declare the type when you have an initial value
  const [emailNotifications, setEmailNotifications] = useState(
    initialNotifications
  );

  const handleEmailNotificationsSettings = (
    property: keyof EmailNotifications
  ) => (event: React.ChangeEvent<HTMLInputElement>) => {
    setEmailNotifications((prevState) => ({
      ...prevState,
      [property]: event.target.checked
    }));
  };

  return (
    <div>
      {(Object.keys(emailNotifications) as Array<keyof EmailNotifications>).map(
        (property) => (
          <div key={property}>
            <label>
              <input
                type="checkbox"
                id={property}
                checked={emailNotifications[property]}
                onChange={handleEmailNotificationsSettings(property)}
              />
              {property}
            </label>
          </div>
        )
      )}
    </div>
  );
};

export default MyComponent;

Upvotes: 3

kunal panchal
kunal panchal

Reputation: 798

Your approach is right just one minor thing that you trying to achieve here is wrong.

setEmailNotifications({
  ...emailNotifications,
  [event.target.id]: !event.target.id, //Here
});

when you are setting dynamic value to the state you are expecting it to be the Boolean value which is not

solution:

setEmailNotifications({
  ...emailNotifications,
  [event.target.id]: !emailNotifications[event.target.id],
});

Upvotes: 4

Related Questions