Table of contents
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:
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:
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:
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 theinput
allowing me to easily clear it whenpush
orUnshift
are clicked 😉
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!