Sam Phillips
The Frontier

The Frontier

React Hooks You Will Love ♥

React Hooks You Will Love ♥

Sam Phillips's photo
Sam Phillips
·Aug 16, 2022·

5 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • Introduction
  • useValue
  • useCounter
  • useObject
  • useArray
  • Conclusion

Introduction

If there is one this I love, it's doing more, with less.

useState is the bread and butter of React. And it's simplicity offers many use cases. However it's simplicity comes at the cost of having to implement various complexities outside of the hook that could be included in the hook itself.

In this article I offer a few abstractions that you can use and extend to help tackle some of the common use cases you may come across when building an application in React.

useValue

useValue is the simplest in this list. All it adds to useState is a utility function that resets the value to whatever you specified as the default value. This however is deceptively useful in situations where you need to clear text boxes and can create a better user experience.

const useValue = (defaultValue) => {
    const [value, setValue] = useState(defaultValue);

    const resetValue = () => {
        setValue(defaultValue);
    };

    return [value, setValue, resetValue];
};

Here it is in action:

1.gif

const [value, setValue, resetValue] = useValue("Test");

return (
    <>
        <input
            value={value}
            onChange={(ev) => setValue(ev.target.value)}
        />
        <button onClick={resetValue}>Reset</button>
    </>
);

useCounter

useCounter does exactly what it says, it counts. However with a little extra logic around allowing the specification of min, max and step parameters, it becomes everything you need to implement paging, or anything that would require incrementing or decrementing a number.

I would use this as a base in a usePaging hook for paging logic that fits my specific use case on that page.

const useCounter = (defaultValue, step = 1, min, max) => {
    const [value, setValue] = useState(defaultValue);

    const resetValue = () => {
        setValue(defaultValue);
    };

    const setCount = (newValue) => {
        newValue = typeof max === "number" ? Math.min(newValue, max) : value;
        newValue = typeof min === "number" ? Math.max(newValue, min) : value;
        setValue(newValue);
    };

    const increment = () => {
        setValue(
            typeof max === "number" ? Math.min(value + step, max) : value + step
        );
    };

    const decrement = () => {
        setValue(
            typeof min === "number" ? Math.max(value - step, min) : value - step
        );
    };

    return [value, setCount, resetValue, increment, decrement];
};

Here it is in action:

2.gif

const [
    count,
    setCount,
    resetCount,
    incrementCount,
    decrementCount
] = useCounter(1, 5, 0, 400);

return (
    <>  
        <input
            value={count}
            onChange={(ev) => setCount(Number(ev.target.value))}
        />
        <button onClick={incrementCount}>Increment Count</button>
        <button onClick={decrementCount}>Decrement Count</button>
        <button onClick={resetCount}>Reset Count</button>
    </>
);

useObject

useObject saves so much boilerplate code over time and can be used in some really sophisticated use cases elegantly. The main use case for it is simply filling out a form of many values that you eventually need to validate and send off to an API. I use this constantly and it has saved me so many keystrokes!

const useObject = (defaultObj) => {
    const [obj, setObj] = useState(defaultObj);

    const updateObj = (updateObj) => {
        setObj({ ...obj, ...updateObj });
    };

    const resetObj = () => {
        setObj(defaultObj);
    };

    return [obj, setObj, updateObj, resetObj];
};

Here it is in action:

3.gif

const [
    person,
    /* setPerson not needed */,
    updatePerson,
    resetPerson
] = useObject({});

return (
    <>  
        <input
            value={person.name || ""}
            onChange={(ev) => updatePerson({ name: ev.target.value })}
        />
        <input
            value={person.country || ""}
            onChange={(ev) => updatePerson({ country: ev.target.value })}
        />
        <button onClick={resetPerson}>Reset</button>
        <p>{JSON.stringify(person, null, 2)}</p>
    </>
);

useArray

useArray is another one that blew me away when I first used it! You can extend this hook to expose any array operations you like and never have to do another shallow clone in you component to get the changes to reflect ever again! Need a map function? Throw it in there!

const useArray = (defaultArray) => {
    const [arr, setArr] = useState(defaultArray);

    const push = (value) => {
        setArr([...arr, value]);
    };

    const unshift = (value) => {
        setArr([value, ...arr]);
    };

    const pop = () => {
        const current = [...arr];
        const deleted = current.pop();
        setArr(current);
        return deleted;
    };

    const shift = () => {
        const current = [...arr];
        const deleted = current.shift();
        setArr(current);
        return deleted;
    };

    const concat = (newArr) => {
        setArr([...arr, ...newArr]);
    };

    const inverseConcat = (newArr) => {
        setArr([...newArr, ...arr]);
    };

    const splice = (index) => {
        const current = [...arr];
        current.splice(index, 1);
        setArr(current);
    };

    const reset = () => {
        setArr(defaultArray);
    };

    return [
        arr,
        setArr,
        {
            push,
            unshift,
            pop,
            shift,
            concat,
            inverseConcat,
            splice,
            reset
        }
    ];
};

Here it is in action:

Note the use of useValue for the input allowing me to easily clear it when push or Unshift are clicked 😉

4.gif

const [
    people,
    /* setPeople not needed */,
    {
        push: pushPeople,
        unshift: unshiftPeople,
        pop: popPeople,
        shift: shiftPeople,
        reset: resetPeople,
        splice: splicePeople
    }
] = useArray([]);

return (
    <>  
        <div>
            <input
                value={name}
                placeholder="Name"
                onChange={(ev) => setName(ev.target.value)}
            />
            <button
                onClick={() => {
                    pushPeople(name);
                    resetName();
                }}
            >
                Push
            </button>
            <button
                onClick={() => {
                    unshiftPeople(name);
                    resetName();
                }}
            >
                Unshift
            </button>
            <button onClick={() => popPeople()}>Pop</button>
            <button onClick={() => shiftPeople()}>Shift</button>
            <button onClick={() => resetPeople()}>Reset</button>
        </div>
        <ol>
            {people.map((p, i) => (
                <li key={i}>
                    {p}{" "}
                    <button onClick={() => splicePeople(i)}>
                        Delete
                    </button>
                </li>
            ))}
        </ol>
    </>
);

Conclusion

That's it! I hope this article helps you reduce the code you need to write, and gets you thinking about how you can use custom hooks for elegant abstractions 👍

Follow me on Twitter to keep up to date with my projects and get notified of new articles like this in the future!

 
Share this