2

I am trying to write a generic React Hook to allow me to update objects.

I took reference from: Input Hook - (source: https://rangle.io/blog/simplifying-controlled-inputs-with-hooks/) and made some changes:

import { useState } from "react";

export const useForm = initialObject => {
  const [values, setValues] = useState(initialObject);

  return {
    values: values || initialObject,
    setValues,
    reset: () => setValues({}),
    bind: {
      onChange: (event) => {
        setValues({
          ...values,
          [event.target.id]: event.target.value
        })
      }
    }
  };
};

This worked well from single level objects:

{ name: '', type: '' }

but for objects with nested values:

{ name: '', type: '', price: { dollar: 5, cents: 20  } }

I'm too sure how I should replace [event.target.id] to read nested level objects.

Could someone advise?

Updated:

import { useState } from "react";

export const useForm = initialObject => {
  const [values, setValues] = useState(initialObject);

  return {
    values: values || initialObject,
    setValues,
    reset: () => setValues({}),
    bind: {
      onChange: (event) => {
        // ###need make this part generic###
        // event.target.id will be "price.dollar"
        values['price']['dollar'] = event.target.value;
        setValues({
          ...values
        })
      }
    }
  };
};
toffee.beanns
  • 435
  • 9
  • 17

1 Answers1

3

Generally your hook should accept a name and value to update your local state. Apparently your hook always receive an event and you extract the event.target.id as the name of the field and event.target.value as the value of the field. I would suggest you to update your hook to receive a name and a value as argument instead, and letting the component that uses the hook to define what is name and value

Based on your hook, you can update nested object like this. Please take a look at this example.

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

const useForm = initialObject => {
  const [values, setValues] = useState(initialObject);

  return {
    values: values || initialObject,
    setValues,
    reset: () => setValues({}),
    bind: {
      onChange: event => {
        setValues({
          ...values,
          [event.target.id]: event.target.value
        });
      }
    }
  };
};

const App = () => {
  const { values, bind } = useForm({
    name: "",
    type: "",
    price: { dollar: 5, cents: 20 }
  });
  return (
    <div>
      Hook state:
      <pre>{JSON.stringify(values, null, 4)}</pre>
      <div>
        <div>
          <label>
            Name : <br />
            <input id="name" onChange={bind.onChange} />
          </label>
        </div>
        <div>
          <label>
            Type : <br />
            <input id="type" onChange={bind.onChange} />
          </label>
        </div>
        <div>
          <label>
            Price - Dollar : <br />
            <input
              id="dollar"
              type="number"
              onChange={e => {
                bind.onChange({
                  target: {
                    id: "price",
                    value: { ...values.price, [e.target.id]: e.target.value }
                  }
                });
              }}
            />
          </label>
        </div>
        <div>
          <label>
            Price - Cents : <br />
            <input
              id="cents"
              type="number"
              onChange={e => {
                bind.onChange({
                  target: {
                    id: "price",
                    value: { ...values.price, [e.target.id]: e.target.value }
                  }
                });
              }}
            />
          </label>
        </div>
      </div>
    </div>
  );
};

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

Update (11/25/2019):

You can, however, update your hook as the following

const useForm = initialObject => {
  const [values, setValues] = useState(initialObject);

  return {
    values: values || initialObject,
    setValues,
    reset: () => setValues({}),
    bind: {
      onChange: event => {
        setValues({
          ...values,
          [event.target.id]: event.target.value
        });
      },
      onNestedChange: (event, name) => {
        setValues({
          ...values,
          [name]: {
            ...values[name],
            [event.target.id]: event.target.value,
          }
        })
      }
    }
  };
};

Then in your inputs, you can write as the following:

<div>
  <label>
    Price - Dollar : <br />
    <input
      id="dollar"
      type="number"
      onChange={e => bind.onNestedChange(e, 'price')}
    />
  </label>
</div>
<div>
  <label>
    Price - Cents : <br />
    <input
      id="cents"
      type="number"
      onChange={e => bind.onNestedChange(e, 'price')}
    />
  </label>
</div>

That way you created another bind method for nested object, and perhaps you can add another called array or something. Hope that this gives you some idea on how to improve the hook. By the way there are plenty of way to do this, this is just an example. There are probably better ways of doing this.

Update 2 (11/25/2019):

I've updated your useForm hook, now you can set nested object property to your state. However, I've not tested with array and it will probably cause issue.

const useForm = initialObject => {
  const [values, setValues] = useState(initialObject);
  // Copied and modified from https://stackoverflow.com/a/18937118/11125492
  const nestedObjectSet = (obj, path, value) => {
    let schema = obj; // a moving reference to internal objects within obj
    const pList = path.split(".");
    const len = pList.length;
    for (let i = 0; i < len - 1; i++) {
      let elem = pList[i];
      if (!schema[elem]) schema[elem] = {};
      schema = schema[elem];
    }
    schema[pList[len - 1]] = value;
  };
  // handleOnChange update state value
  const handleOnChange = event => {
    let newValues = Object.assign({}, values);
    nestedObjectSet(newValues, event.target.name, event.target.value);
    setValues(newValues);
  };
  return {
    values: values || initialObject,
    setValues,
    reset: () => setValues({}),
    bind: {
      onChange: handleOnChange
    }
  };
};

You can use it like that. Notice that I've changed the key of the object to take from event.target.id to event.target.name. The key should be set in name instead of id

const App = () => {
  const { values, bind } = useForm({
    name: "",
    type: "",
    price: { dollar: 5, cents: 20 }
  });
  return (
    <div>
      Hook state:
      <pre>{JSON.stringify(values, null, 4)}</pre>
      <div>
        <div>
          <label>
            Name : <br />
            <input name="name" {...bind} />
          </label>
        </div>
        <div>
          <label>
            Type : <br />
            <input name="type" {...bind} />
          </label>
        </div>
        <div>
          <label>
            Price - Dollar : <br />
            <input name="price.dollar" type="number" {...bind} />
          </label>
        </div>
        <div>
          <label>
            Price - Cents : <br />
            <input name="price.cents" type="number" {...bind} />
          </label>
        </div>
      </div>
    </div>
  );
};

Sandbox Demo Link : https://codesandbox.io/s/react-useform-hook-nested-object-cqn9j?fontsize=14&hidenavigation=1&theme=dark

junwen-k
  • 3,454
  • 1
  • 15
  • 28
  • Thanks @dev_junwen, I get your approach.. But there are many fields in the object and I am hoping to write a generic approach where the onChange is binded by the hook and than on individual element. Do you know how can I tweak the approach? – toffee.beanns Nov 25 '19 at 08:58
  • thanks for the update.. I updated the question with a closer example to what I was hoping for.. Ideally, not having to bind each component separately. Would be great if you can help take a look again. – toffee.beanns Nov 25 '19 at 09:37
  • Thanks @dev_junwen for your help. Yeah I went with a similar approach in the end! – toffee.beanns Nov 26 '19 at 11:42