Search code examples
javascriptreactjsreact-hooksuse-effectuse-state

Should I use separate useState for each property


I have written following code:

function CreateModal({toggleCreatePorjectModal, fetchProjects}) {
    const [formObj, setFormObj] = useState({name: "", link: "", error: ""})


    function validateLinkAndCreateProject() {
        if(formObj.link.trim() === "" || formObj.name.trim() === "") {
            setFormObj(state => ({...state, error: "ALL fields are mandatory, please enter Project Name and Feed Link to create project."}))
            return
        }
        
        rssFeedParser(formObj.link.trim())
            .then((res) => {
                axios.post(`${ServerPath}/projects/create`, {
                    name: formObj.name, 
                    link: formObj.link
                })
                .then(response => {
                    if(response.data.error) {
                        setFormObj(state => ({...state, error: "Something went wrong. Please try again after some time."}))
                    } else {
                        fetchProjects()
                        toggleCreatePorjectModal(false)
                    }  
                })
                .catch((err) => {
                    setFormObj(state => ({...state, error: err.msg}))
                })
            })
            .catch((err) => {
                setFormObj(state => ({...state, error: err.msg})) 
            })
    }
    return  (
        <div >
            {/*Will Render Something based on above code*/}
        </div>
    )
}

I have used only one useState to store properties like "name", "link", "error" in one object. As I wanted to keep logic of FormObj and validateLinkAndCreateProject together. All three properties use only one useEffect. Hence, I thought that it is better to keep all three properties inside useState instead of creating 3 different usestate.

But my manager, and my technical architect telling me to create 3 useState, one useState for each property,that is, separate useState for name, link and error. According to them, never create object inside useState like useState({name: "", link: "", error: ""}). I should always create separate useState for each property.

As per my understanding, in older react we used to have only one state which use to have 30 to 40 properties in one object. With introduction of React hooks, as per "separation of concern", we are suppose to separate related logic from rest of the code. Hence, we will end up 5 to 6 states having objects with 4 to 5 properties.

Note: Here "separation of concerns" means breaking class code in such way that dependent state + useEffect + useRef etc kept together. below is quote from React documentation.

Hooks let you split one component into smaller functions based on what pieces are related (such as setting up a subscription or fetching data), rather than forcing a split based on lifecycle methods.

I wanted to Am I missing anything??, Is it okay to create useState({name: "", link: "", error: ""}). or should I create separate state for each one of them?


Solution

  • 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.