score:3

Accepted answer

it's a judgement call, but in most cases, yes, use separate usestate calls for each of those three pieces of state information (or use usereducer instead). that way, setting them is simple and straightfoward. e.g.:

const [name, setname] = usestate("");
const onnamechange = e => setname(e.target.value);
<input
    type="text"
    onchange={onnamechange}
>

class-based components get a setstate function that accepts partial state updates and merges them into the component's state, but that isn't how usestate's setter works; it completely replaces the state item.¹ that means if you're using a compound state item as in your question, you have to include all of its parts in every call to the setter — which sets you up to accidentally use stale copies of the parts you're not intentionally updating:

const [formobj, setformobj] = usestate({name: "", link: "", error: ""});
const onnamechange = ({target: {value}}) => setformobj({...formobj, name: value}); // <== usually wrong

the problem there is that the data in formobj can be out of date. you'd have to use the callback form instead:

const [formobj, setformobj] = usestate({name: "", link: "", error: ""});
const onnamechange = ({target: {value}}) => setformobj(formobj => ({...formobj, name: value}));

the other issue is that if you're using any of those pieces of state as dependencies in a useeffect/usememo/usecallback/etc., it's easy to get that wrong too.

so yes, your manager and tech architect are likely correct, use individual usestate calls (or usereducer). but, again, it's a judgement call; you could just always use the callback form of useformobj.


another option:

you could create a hook for the form object with individual setters for the name, link, and error that accept either a string or an event, like this:

// a reusable utility function: if the argument is an object with a `target` property,
// return `x.target.value`; otherwise, just return `x`.
function unwrapvalue(x) {
    if (typeof x === "object" && x.target) {
        x = x.target.value;
    }
    return x;
}

// off-the-cuff, untested, just a sketch
function useformobj(initialformobj = {name: "", link: "", error: ""}) {
    const [formobj, setformobj] = usestate(initialformobj);
    const setters = useref(null);
    if (!setters.current) {
        // only true on first call
        setters.current = {
            setname(name) {
                name = unwrapvalue(name);
                setformobj(formobj => ({...formobj, name}));
            },
            setlink(link) {
                link = unwrapvalue(link);
                setformobj(formobj => ({...formobj, link}));
            },
            seterror(error) {
                error = unwrapvalue(error);
                setformobj(formobj => ({...formobj, error}));
            },
        };
    }
    return [formobj, setters.current];
}

and then:

const [formobj, {setname, setlink, seterror}] = useformobj();
<input
    type="text"
    onchange={setname}
>

that's handy when you need to use form objects in more than one component, or you just want to have smaller more easily-testable pieces.


¹ from the documentation (you have to scroll down slightly to the note:):

note

unlike the setstate method found in class components, usestate does not automatically merge update objects. you can replicate this behavior by combining the function updater form with object spread syntax:

setstate(prevstate => {
  // object.assign would also work
  return {...prevstate, ...updatedvalues};
});

another option is usereducer, which is more suited for managing state objects that contain multiple sub-values.

score:1

it is true that usestate replaced the state completely, but you can create your own custom hooks which you can try to update the state using spread operator, eg {...obj, ...values} so this way you don't have to update the whole object, spread operator will take care.

i think you can go for custom hooks and manage this.

score:5

i realize this is an old question, but if anyone else is trying to figure this out, i'd like to say i've built a whole production app this way. every page (it was a next.js app) used one usestate and in it was an objected with many properties. error, form data, loading, etc.

if you read the react docs they recommend against it because of possible performance issues due to the fact that an object is re-evaluated every time. even with that, the app runs quickly. i think we overcomplicate things way too often. a reducer would have been better, sure. but the speed you can develop with not going into abstract complexities by keeping things simple has a lot less bugs and performance issues than people seem to think. remember, the react team are smart. they're not going to set you up for failure. if it really hindered performance in a great way and it was a basic and fundamental thing like usestate is, they'd go out of their way to prevent you from doing such things.

by the way this production app services hundreds of active users every week for ordering food and we've had no complaints and heard only good things.


Related Query

More Query from same tag