This content originally appeared on DEV Community and was authored by Luke Brannagan
Patterns stand the test of time because they describe how something should work, not exactly how it’s built. The challenge comes when we design at too granular of a level.
Building something that’s overly specific to a single feature quickly becomes hard to maintain, and documenting it, well is even harder.
Think about the simple act of boiling water.
The pattern looks like this:
- Heat up the water
- Wait until it boils
- Congratulations, you’ve boiled water
The implementation might look like this:
- Pour water into a kettle
- Plug the kettle in
- Press the switch
- Wait for it to boil
- Congratulations, you’ve boiled water
The pattern doesn’t change, only the method does. You could use a kettle, a campfire, or any other heat source however, the underlying logic stays the same.
We can apply this same thinking to frontend development. Instead of building a hook that only handles one very specific use case, we can build a more general hook that defines the pattern, then inject the details as dependencies. It’s like choosing whether you use the kettle or the campfire. The steps stay consistent; only the context changes.
This approach helps keep code consistent, easier to maintain, and simpler to explain. By focusing on patterns that represent the essence of what we’re trying to do, we can swap out the details without rewriting the logic every time.
An Example
❌Imperative solution
The imperative solution handles each step manually, tightly coupled to the feature, hard to reuse, document and test.
function useSignupSteps() {
const [step, setStep] = useState(0);
const next = () => setStep((s) => s + 1);
const prev = () => setStep((s) => Math.max(0, s - 1));
// Steps are hard-coded here
const steps = [
{ id: "account", label: "Create Account" },
{ id: "profile", label: "Set Up Profile" },
{ id: "confirm", label: "Confirm Details" },
];
const current = steps[step];
return { steps, step, current, next, prev };
}
// Usage
const { steps, current, next } = useSignupSteps();
đź§© Pattern Version
This version defines a general “steps pattern” and lets you inject the specifics.
import { useState, useCallback } from "react";
type Step<T = any> = {
id: string;
label?: string;
data?: T;
};
type UseStepsOptions<T> = {
steps: Step<T>[];
onStepChange?: (step: Step<T>, index: number) => void;
};
export function useSteps<T = any>({ steps, onStepChange }: UseStepsOptions<T>) {
const [index, setIndex] = useState(0);
const next = useCallback(() => {
setIndex((i) => {
const nextIndex = Math.min(i + 1, steps.length - 1);
onStepChange?.(steps[nextIndex], nextIndex);
return nextIndex;
});
}, [steps, onStepChange]);
const prev = useCallback(() => {
setIndex((i) => {
const prevIndex = Math.max(i - 1, 0);
onStepChange?.(steps[prevIndex], prevIndex);
return prevIndex;
});
}, [steps, onStepChange]);
const goTo = useCallback(
(id: string) => {
const idx = steps.findIndex((s) => s.id === id);
if (idx !== -1) {
setIndex(idx);
onStepChange?.(steps[idx], idx);
}
},
[steps, onStepChange]
);
return {
current: steps[index],
index,
next,
prev,
goTo,
isFirst: index === 0,
isLast: index === steps.length - 1,
};
}
the key take aways from this document should be to design software more with patterns in mind than the imperative solution.
For example, If you need to use localStorage write a hook that handles all implementations of it rather than just thinking about your specific Kettle use case.
This content originally appeared on DEV Community and was authored by Luke Brannagan
Luke Brannagan | Sciencx (2025-10-25T14:23:44+00:00) đź«– Thinking in patterns not features. Retrieved from https://www.scien.cx/2025/10/25/%f0%9f%ab%96-thinking-in-patterns-not-features/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.