Custom React Hook: useEasyState
🎉
Let's dive into useEasyState, a custom React hook that streamlines state management. With this hook, you can handle state updates, resets, and more with minimal fuss. It’s a handy tool to keep your state management clean and straightforward, so you can spend less time on boilerplate code and more on building features.
export const useEasyState = <T = any>(defaultValue?: T) => {
const [value, _set] = useState<T>(defaultValue);
const valueRef = useRef<T>(defaultValue);
valueRef.current = value;
const [defaultState] = useState(defaultValue);
const [touched, setTouched] = useState(false);
const [dirty, setDirty] = useState(false);
const set: Dispatch<SetStateAction<T>> = useCallback((...args) => {
_set(...args);
setDirty(true);
setTouched(true);
}, []);
const eventHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>) => set((e?.target?.value as any) || ""),
[set]
);
const reset = useCallback(() => {
setDirty(false);
set(defaultState);
}, [defaultState, set]);
const clear = useCallback(() => {
if (Array.isArray(defaultState)) return set([] as any);
if (defaultState === null || defaultState === undefined)
return set(defaultState);
if (defaultState instanceof Date) return set(null);
switch (typeof defaultState) {
case "boolean":
return set(false as any);
case "number":
return set(0 as any);
case "string":
return set("" as any);
case "object":
return set({} as any);
default:
return set(defaultState);
}
}, [defaultState, set]);
return useMemo(
() => ({
value,
valueRef,
set,
eventHandler,
initial: defaultState,
reset,
clear,
touched,
dirty,
}),
[clear, defaultState, dirty, eventHandler, reset, set, touched, value]
);
};
export type EasyState<T> = ReturnType<typeof useEasyState<T>>;
Code Breakdown
1. State Management 🧠
const [value, _set] = useState<T>(defaultValue);
const valueRef = useRef<T>(defaultValue);
valueRef.current = value;
const [defaultState] = useState(defaultValue);
const [touched, setTouched] = useState(false);
const [dirty, setDirty] = useState(false);
- value: Your current state.
-
valueRef: Reference to track
value
changes. - defaultState: Your "reset to factory settings" button. Because sometimes, you just need a fresh start. 🔄
- touched: Did someone touch the state? Now you'll know. 👀
- dirty: Tracks if the state has been modified. 🕵️
2. State Setter with Side Effects 🔄
const set: Dispatch<SetStateAction<T>> = useCallback((...args) => {
_set(...args);
setDirty(true);
setTouched(true);
}, []);
- set: Updates the state and flags it as touched and dirty.
3. Event Handler ✋
const eventHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>) => set((e?.target?.value as any) || ""),
[set]
);
- eventHandler: Automatically updates your state based on user input. It's like magic, but without the wand. 🎩✨
4. Reset Functionality 🧹
const reset = useCallback(() => {
setDirty(false);
set(defaultState);
}, [defaultState, set]);
- reset: Reverts everything back to square one. It's like hitting the undo button. 💥
5. Clear Functionality ❌
const clear = useCallback(() => {
if (Array.isArray(defaultState)) return set([] as any);
if (defaultState === null || defaultState === undefined)
return set(defaultState);
if (defaultState instanceof Date) return set(null);
switch (typeof defaultState) {
case "boolean":
return set(false as any);
case "number":
return set(0 as any);
case "string":
return set("" as any);
case "object":
return set({} as any);
default:
return set(defaultState);
}
}, [defaultState, set]);
- clear: Clears the state based on its type. 🧹✨
6. Memoized Return Object 🧳
return useMemo(
() => ({
value,
valueRef,
set,
eventHandler,
initial: defaultState,
reset,
clear,
touched,
dirty,
}),
[clear, defaultState, dirty, eventHandler, reset, set, touched, value]
);
- useMemo: Neatly packages everything together, It only changes when it absolutely needs to, so you’re not dealing with any unnecessary re-renders.
Real-World Usage of useEasyState
🌎
Ready to see this in action? Let's check out how useEasyState
makes your life easier and your code cleaner.
Example 1: Managing Form State ✍️
Let’s say you’re building a form to collect user info. You need something that can handle all that input without breaking a sweat.
import React from "react";
import { useEasyState } from "./useEasyState";
const UserForm = () => {
const nameState = useEasyState<string>("");
const emailState = useEasyState<string>("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("Name:", nameState.value);
console.log("Email:", emailState.value);
// Simulate form submission...
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
value={nameState.value}
onChange={nameState.eventHandler}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={emailState.value}
onChange={emailState.eventHandler}
/>
</div>
<div>
<button type="submit">Submit</button>
<button type="button" onClick={nameState.reset}>
Reset Name
</button>
<button type="button" onClick={emailState.clear}>
Clear Email
</button>
</div>
</form>
);
};
export default UserForm;
Key Points:
-
nameState
andemailState
make managing your form a breeze. -
reset
andclear
are there when you need them.
Example 2: Controlled Component with Default Value 🎛️
Need to control an input with a default value? useEasyState
has your back.
import React from "react";
import { useEasyState } from "./useEasyState";
const ControlledInput = () => {
const defaultText = "Hello World";
const textState = useEasyState(defaultText);
return (
<div>
<input
type="text"
value={textState.value}
onChange={textState.eventHandler}
/>
<button onClick={textState.reset}>Reset</button>
<button onClick={textState.clear}>Clear</button>
<p>Current Value: {textState.value}</p>
<p>Dirty: {textState.dirty ? "Yes" : "No"}</p>
<p>Touched: {textState.touched ? "Yes" : "No"}</p>
</div>
);
};
export default ControlledInput;
Key Points:
- The default value of
"Hello World"
is easy to manage, modify, or reset. - It tracks whether you've messed with the input like a responsible adult.
Example 3: Managing Non-String State 🚀
Who says useEasyState
is only for strings? It’s ready to handle arrays, objects, and whatever else you throw at it.
import React from "react";
import { useEasyState } from "./useEasyState";
const ManageArrayState = () => {
const itemsState = useEasyState<string[]>([]);
const addItem = () => {
itemsState.set((prevItems) => [
...prevItems,
`Item ${prevItems.length + 1}`,
]);
};
return (
<div>
<button onClick={addItem}>Add Item</button>
<button onClick={itemsState.clear}>Clear Items</button>
<ul>
{itemsState.value.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
export default ManageArrayState;
Key Points:
- This hook handles arrays like a champ.
-
addItem
andclear
keep things fresh and clean.
Why useEasyState Makes Development Faster and Easier ⏳🧠
Less Boilerplate 🚫📄: Forget about juggling multiple useState hooks, writing event handlers, and managing reset logic. useEasyState consolidates these tasks into one hook, reducing code clutter and saving you time for other important tasks.
Consistent State Management 🔑: Achieve uniform and reliable state management across your components. useEasyState ensures that state handling is smooth and consistent, making your codebase more dependable and easier to maintain.
Versatile and Flexible 🌀: Whether you're dealing with strings, numbers, arrays, or other data types, useEasyState adapts to your needs. The hook’s clear and reset functions handle different types automatically, freeing you from manual type-checking.
Focus on What Matters 🎉: Skip the repetitive tasks and dive into the exciting parts of development. useEasyState simplifies state management, allowing you to concentrate on building features and enhancing your application.
⚠️ Caveat: Handling useEffect
Dependencies with useEasyState
When using useEasyState
, a common issue arises when setting state within a useEffect
hook. The hook tends to add the entire useEasyState
object as a dependency if you attempt to set it directly from within the useEffect
. This can lead to unintended re-renders and performance issues. 😅
🧩 The Problem
Say you've got this:
const something = useEasyState();
And then you decide to do this in a useEffect
:
useEffect(() => {
something.set("some value");
}, [something.set]);
Now, you'd think everything's fine, right? But no. React sees something.set
and says, "Hey, why not take the whole something
object along for the ride?" And boom, your effect starts re-running more often than you'd like. 😩
The useEffect
will consider something
(the entire object) as a dependency, leading to unnecessary re-executions of the effect. 😬
🛠️ Why This Happens
Technically, you don't need to use the depFn function here. If you add something.set to the dependency list and use it as expected, React will behave. But—there's always a "but"—the ESLint rule that checks your dependencies doesn't quite get it. It sees you accessing a function within an object and thinks, "Hmm, better play it safe and throw the whole object in there." 🤦♂️
It's frustrating, right? If you don't actually invoke the function but just refer to it, ESLint stays quiet. But the moment you start using it, the complaints roll in. One day, maybe we'll live in a world where this rule is smarter. Until then, we have a workaround.
🛠️ The Solution: depFn
To avoid this, you can use a utility function like depFn
to limit the dependencies to just the setter method:
type AnyFunction = (...args: any[]) => any | Promise<any>;
export const depFn = <T extends AnyFunction>(fn: T, ...args: Parameters<T>) =>
fn?.(...args);
When using depFn
, your useEffect
would look like this:
useEffect(() => {
depFn(something.set, "some value");
}, [something.set]);
This way, the dependency array only includes something.set
instead of the entire something
object, which prevents unnecessary re-renders. 🚀
📜 Summary
While useEasyState
simplifies state management significantly, it's important to be aware of how React handles dependencies in hooks like useEffect
. By using a helper like depFn
, you can keep your effects efficient and avoid performance pitfalls. 💪
If you found this article helpful and enjoyed exploring the useEasyState, please consider starring middleware repository where we use it live in action.
Your support helps us continue improving and sharing valuable content with the community. Thank you!
middlewarehq / middleware
✨ Open-source DORA metrics platform for engineering teams ✨
Open-source engineering management that unlocks developer potential
Join our Open Source Community
Introduction
Middleware is an open-source tool designed to help engineering leaders measure and analyze the effectiveness of their teams using the DORA metrics. The DORA metrics are a set of four key values that provide insights into software delivery performance and operational efficiency.
They are:
- Deployment Frequency: The frequency of code deployments to production or an operational environment.
- Lead Time for Changes: The time it takes for a commit to make it into production.
- Mean Time to Restore: The time it takes to restore service after an incident or failure.
- Change Failure Rate: The percentage of deployments that result in failures or require remediation.
Table of Contents
Top comments (7)
Just
useEffect()
? I ditched React completely a year ago. I'll never go back.As far as I know, react is user across the industry for front end, why did you quit ?
Nope. Svelte is my religion now.
Svelte has pretty solid decisions made at a framework level. Good choice.
Were it more popular, I'd consider using it more myself.
Be the change you want! Svelte can do anything React can, and more. Contribute to its popularity by switching to Svelte today! 😄
He talks like a cult leader, haha! I guess I have to check Svelte out
Great read! hooks can be tricky but this summed dit up fairly well.