Home > Blockchain >  Adding handlers and state dynamically in React
Adding handlers and state dynamically in React

Time:12-21

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}
    </>
  );
};

CodePudding user response:

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}
        </>
    )
}

CodePudding user response:

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}
    </>
  );
};

CodePudding user response:

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}
    </>
  );
};

CodePudding user response:

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}
    </>
  );
}
  • Related