kahjuksEi
kahjuksEi

Reputation: 135

Adding handlers and state dynamically in React

I need to render plenty of inputs for every name of the array, but the problem is creating dynamically useState const and onChange handler for every rendered input. I try to create handleChange key for every item in an array and pass it to input onChange but got the "String Instead Fuction" error. How to resolve the problem in another way and also avoid code duplication?

export const myArr = [
  { name: "Crist", data: "crist", color: "lightblue", handleChange: "cristHandleChange"},
  { name: "Ruby", data: "ruby", color: "lightpink", handleChange: "rubyHandleChange"},
  { name: "Diamond", data: "diamond", color: "white", handleChange: "diamondHandleChange"},
];

export const myComponent = () => {
  const [cristQuantity, setCristQuantity] = useState(0);
  const [rubyQuantity, setRubyQuantity] = useState(0);
  const [diamondQuantity, setDiamondQuantity] = useState(0);

  function cristHandleChange(event) {
    setCristQuantity(event.target.value);
  }

  function rubyHandleChange(event) {
    setRubyQuantity(event.target.value);
  }

  function diamondHandleChange(event) {
    setDiamondQuantity(event.target.value);
  }

  return (
    <>
      {myArr
        ? myArr.map((item, index) => (
            <div className="main-inputs__wrapper" key={`item${index}`}>
              <label htmlFor={item.data}>{item.name}</label>
              <input
                type="number"
                name={item.data}
                style={{ background: item.color }}
                onChange={item.handleChange} //???
              />
            </div>
          ))
        : null}
    </>
  );
};

Upvotes: 3

Views: 173

Answers (4)

Vladislav Rogozhin
Vladislav Rogozhin

Reputation: 36

You should create one handler for all inputs, and save values in a object with a key as item.data. Such way: {crist: 1, ruby: 3, diamond: 5}

export const myArr = [
  {
    name: "Crist",
    data: "crist",
    color: "lightblue"
  },
  {
    name: "Ruby",
    data: "ruby",
    color: "lightpink"
  },
  {
    name: "Diamond",
    data: "diamond",
    color: "white"
  }
];

export function MyComponent() {
  // Lazy initial state 
  // https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [quantities, setQuantities] = useState(() =>
    myArr.reduce((initialQuantities, item) => {
      initialQuantities[item.data] = 0;
      return initialQuantities;
    }, {})
  );

  // common handler for every onChange with computed property name
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names
  const handleChange = (event) => {
    setQuantities((prevQuantities) => ({
      ...prevQuantities,
      [event.target.name]: event.target.value
    }));
  };

  return (
    <>
      {Array.isArray(myArr)
        ? myArr.map((item, index) => (
            <div className="main-inputs__wrapper" key={`item${index}`}>
              <label htmlFor={item.data}>{item.name}</label>
              <input
                type="number"
                name={item.data}
                style={{ background: item.color }}
                onChange={handleChange}
                value={quantities[item.data] || ""}
              />
            </div>
          ))
        : null}
    </>
  );
}

Upvotes: 2

Chris
Chris

Reputation: 6631

Let me start by saying that you might want to use a library like React Hook Form for this, although, if this is the only form and you don't need all the fancy features (or additional bundle size), you can do it like this as well.

The first step is to store your form data in an Object. After this change, you can use a single useState({}) to store and read your form data and drastically simplifies your handlers.

For example:

export const myArr = [
  { name: "Crist", data: "crist", color: "lightblue"},
  { name: "Ruby", data: "ruby", color: "lightpink"},
  { name: "Diamond", data: "diamond", color: "white"},
];


// generate Object with data and initialValue or empty string
// e.g. `{ 'crist': '', 'ruby': '', 'diamond': '' }`
const getInitialFormValues = (arr) => Object.fromEntries(
  arr.map(({ data, initialValue }) => [data, initialValue || ''])
);

export const MyComponent = () => {
  const [formValues, setFormValues] = useState(getInitialFormValues(myArr));

  function handleChange(event) {
    // update the changed form value
    setFormValues(current => ({ 
      ...current, // other form values
      [event.target.name]: event.target.value // updated value
    }));
  }

  return (
    <>
      {myArr
        ? myArr.map((item, index) => (
            <div className="main-inputs__wrapper" key={`item${index}`}>
              <label htmlFor={item.data}>{item.name}</label>
              <input
                type="number"
                name={item.data}
                style={{ background: item.color }}
                onChange={handleChange}
                value={formValues[item.data]}
              />
            </div>
          ))
        : null}
    </>
  );
};

Upvotes: 1

Nick Vu
Nick Vu

Reputation: 15520

You're passing a string (not a function) to onChange which is causing that problem.

To fix it, you can wrap all functions into another object

  const onChangeFunctions = {
    cristHandleChange: (event) => {
      setCristQuantity(event.target.value);
    },
    rubyHandleChange: (event) => {
      setRubyQuantity(event.target.value);
    },
    diamondHandleChange: (event) => {
      setDiamondQuantity(event.target.value);
    },
  };

and call onChangeFunctions[item.handleChange] like below

export const myArr = [
  {
    name: "Crist",
    data: "crist",
    color: "lightblue",
    handleChange: "cristHandleChange",
  },
  {
    name: "Ruby",
    data: "ruby",
    color: "lightpink",
    handleChange: "rubyHandleChange",
  },
  {
    name: "Diamond",
    data: "diamond",
    color: "white",
    handleChange: "diamondHandleChange",
  },
];

export const myComponent = () => {
  const [cristQuantity, setCristQuantity] = useState(0);
  const [rubyQuantity, setRubyQuantity] = useState(0);
  const [diamondQuantity, setDiamondQuantity] = useState(0);

  const onChangeFunctions = {
    cristHandleChange: (event) => {
      setCristQuantity(event.target.value);
    },
    rubyHandleChange: (event) => {
      setRubyQuantity(event.target.value);
    },
    diamondHandleChange: (event) => {
      setDiamondQuantity(event.target.value);
    },
  };

  return (
    <>
      {myArr
        ? myArr.map((item, index) => (
            <div className="main-inputs__wrapper" key={`item${index}`}>
              <label htmlFor={item.data}>{item.name}</label>
              <input
                type="number"
                name={item.data}
                style={{ background: item.color }}
                onChange={onChangeFunctions[item.handleChange]}
              />
            </div>
          ))
        : null}
    </>
  );
};

Upvotes: 1

Oliver Heward
Oliver Heward

Reputation: 461

I haven't actually tested this but this is something I would do which will get you on the way.


export const myArr = [
    {
        name: `Crist`,
        data: `crist`,
        color: `lightblue`,
        handleChange: `cristHandleChange`,
    },
    {
        name: `Ruby`,
        data: `ruby`,
        color: `lightpink`,
        handleChange: `rubyHandleChange`,
    },
    {
        name: `Diamond`,
        data: `diamond`,
        color: `white`,
        handleChange: `diamondHandleChange`,
    },
]

export const myComponent = () => {
    const [quantities, setQuantities] = useState({
        crist: 0,
        ruby: 0,
        diamond: 0,
    })

    const onChangeHandler = ({ name, value }) => {
        setQuantities((prevState) => ({ ...prevState, [name]: value }))
    }

    return (
        <>
            {myArr.length > 0
                ? myArr.map(({ data, name, color }, index) => {
                    // if you need to do an update to push more items to the object for any dynamic reason
                        if (!quantities.name)
                            setQuantities((prevState) => ({ ...prevState, [name]: 0 }))
                        return (
                            <div className="main-inputs__wrapper" key={`item${index}`}>
                                <label htmlFor={data}>{name}</label>
                                <input
                                    type="number"
                                    name={name}
                                    value={quantities.name}
                                    style={{ background: color }}
                                    onChange={(e) =>
                                        onChangeHandler({ name, value: e.currentTarget.value })
                                    }
                                />
                            </div>
                        )
                  })
                : null}
        </>
    )
}

Upvotes: 1

Related Questions