A custom React hook that enhances the standard useState
hook by providing history tracking and navigation capabilities (undo/redo functionality).
- Drop-in replacement for
useState
for basic usage - Keeps a history of state changes
- Provides functions to navigate back (
back
) and forward (forward
) through the history - Provides functions to jump directly to the first (
first
) and last (last
) state in history - Provides a function to jump to a specific index (
go
) in the history - Provides functions to clear history:
clear
(clears all history, keeping current state),trimStart
(clears history before current pointer), andtrimEnd
(clears history after current pointer) - Exposes the complete history array and the current pointer position
- Works with any data type (numbers, strings, objects, arrays, etc.)
- Written in TypeScript for type safety
npm install @n0n3br/react-use-state-with-history
# or
yarn add @n0n3br/react-use-state-with-history
# or
pnpm add @n0n3br/react-use-state-with-history
import { useStateWithHistory } from "@n0n3br/react-use-state-with-history";
function Counter() {
const [count, setCount, { back, forward }] = useStateWithHistory(0);
return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={back}>Undo</button>
<button onClick={forward}>Redo</button>
</div>
);
}
import { useStateWithHistory } from "@n0n3br/react-use-state-with-history";
function TextEditor() {
const [
text,
setText,
{
history,
pointer,
back,
forward,
go,
first,
last,
clear,
trimStart,
trimEnd,
},
] = useStateWithHistory("");
return (
<div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
rows={5}
cols={40}
/>
<div>
<button onClick={back} disabled={pointer <= 0}>
Undo
</button>
<button onClick={forward} disabled={pointer >= history.length - 1}>
Redo
</button>
<button onClick={first} disabled={pointer === 0}>
First Version
</button>
<button onClick={last} disabled={pointer === history.length - 1}>
Latest Version
</button>
<button onClick={clear} disabled={history.length <= 1}>
Clear History
</button>
<button onClick={trimStart} disabled={pointer === 0}>
Trim Start
</button>
<button onClick={trimEnd} disabled={pointer === history.length - 1}>
Trim End
</button>
</div>
<div>
<p>History States: {history.length}</p>
<p>Current Position: {pointer}</p>
{/* Display history entries */}
<div>
{history.map((item, index) => (
<button
key={index}
onClick={() => go(index)}
style={{
fontWeight: index === pointer ? "bold" : "normal",
margin: "2px",
}}
>
{index}
</button>
))}
</div>
</div>
</div>
);
}
function useStateWithHistory<T>(initialValue: T): [
T,
(value: T | ((prevState: T) => T)) => void,
{
history: T[];
pointer: number;
back: () => void;
forward: () => void;
go: (index: number) => void;
first: () => void;
last: () => void;
clear: () => void;
trimStart: () => void;
trimEnd: () => void;
}
];
initialValue: T
- The initial state value (can be any type)
Returns a tuple with three elements:
- Current State (
T
): The current state value - State Setter (
(value: T | ((prevState: T) => T)) => void
): Function to update the state- Accepts a new value or a function that receives the previous state and returns a new value
- Each update adds a new entry to the history
- History Controls (Object): An object containing:
history: T[]
- Array of all state values in historypointer: number
- Current position in the history arrayback: () => void
- Move to the previous state in historyforward: () => void
- Move to the next state in historygo: (index: number) => void
- Jump to a specific index in historyfirst: () => void
- Jump to the first state in historylast: () => void
- Jump to the most recent state in historyclear: () => void
- Clears all history, keeping only the current state as the initial state. The pointer is reset to 0.trimStart: () => void
- Clears all history entries before the current pointer. The current state becomes the first state in the new history, and the pointer is reset to 0.trimEnd: () => void
- Clears all history entries after the current pointer. The pointer remains at its current position, which is now the last entry in the history.
The hook works seamlessly with complex data types like objects and arrays:
import { useStateWithHistory } from "@n0n3br/react-use-state-with-history";
function UserForm() {
const [user, setUser, { back, forward }] = useStateWithHistory({
name: "",
email: "",
age: 0,
});
const updateField = (field, value) => {
setUser((prevUser) => ({
...prevUser,
[field]: value,
}));
};
return (
<div>
<div>
<label>Name:</label>
<input
value={user.name}
onChange={(e) => updateField("name", e.target.value)}
/>
</div>
<div>
<label>Email:</label>
<input
value={user.email}
onChange={(e) => updateField("email", e.target.value)}
/>
</div>
<div>
<label>Age:</label>
<input
type="number"
value={user.age}
onChange={(e) => updateField("age", parseInt(e.target.value) || 0)}
/>
</div>
<button onClick={back}>Undo</button>
<button onClick={forward}>Redo</button>
</div>
);
}
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
To publish this package to npm under the @n0n3br scope:
# Login to npm (if not already logged in)
npm login
# Build the package
npm run build
# Publish to npm
npm publish
Note: The package is configured with "access": "public"
in package.json, which allows the scoped package to be publicly accessible.