toffee.beanns
toffee.beanns

Reputation: 435

Writing generic React input hook

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

Upvotes: 2

Views: 875

Answers (1)

junwen-k
junwen-k

Reputation: 3644

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

Upvotes: 3

Related Questions