Reputation: 435
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
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);
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.
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