This is a state management solution I've been using for apps that require persistance and an undo / redo stack. In an app, you would extend the StateManager
class.
interface State {
count: number
user: {
name: string
age: number
}
}
class AppState extends StateManager<State> {
constructor(initialState: State) {
super(initialState)
}
}
const appState = new AppState({
count: 0,
user: {
name: "Steve",
age: 93
}
})
The base class provides methods for changign the state. For changes that should produce a new state without changing the undo/redo stack, call patchState
with a "patch", or deep partial of the state. (Note that arrays are replaced, not merged.)
getOlder() {
const state = this.getState()
this.patchState({
user: {
age: state.user.age + 1
}
})
}
For changes that are intended to be part of the undo/redo stack, call setState
with a "command" made of two "patches": an after
patch that will make the change and a before
patch that would reverse the change. (You can also provide an id
to help with debugging.)
getOlder() {
const state = this.getState()
this.setState({
before: {
user: {
age: state.user.age
}
},
after: {
user: {
age: state.user.age + 1
}
}
})
}
The current state (either the initial state or the one produced by the last call to setState
or patchState
is available at this.current
. Calling either setState
or patchState
will change the value of this.current
.
Calling setState
will also update the value of this.previous
. Calling patchState
will not update this.previous
. In this way, this.previous
allows for patterns a user must be able to undo back to an original point while ignoring all points in between.
startScrubbing() {
const { current } = this
this.setState({
before: {
count: current.count
},
after: {
count: current.count + 1
}
})
}
scrub(delta: number) {
const { current } = this
this.patchState({
count: current.count + delta
})
}
stopScrubbing() {
const { previous, current } = this
this.setState({
before: {
count: previous.count
},
after: {
count: current.count
}
})
}
In the example above, imagine a user began a scrubbing session when the count is 0
, then scrubbed the number from 1
to 2
to 3
, and then stopped scrubbing. Calling undo
would change the count from 3
to 0
. Calling redo
would change the count from 0
to 3
.
The StateManager
class keeps its state in a zustand store. A React component can select from / subscribe to this store.
// ...
const appState = new AppState({
count: 0,
user: {
name: "Steve",
age: 93
}
})
function App() {
const { count } = appState.useAppState(state => state.count)
<div>
<h1>{count}</h1>
<button onClick={() => appState.increment()}>Increment</button>
<button onClick={() => appState.undo()}>Undo</button>
<button onClick={() => appState.redo()}>Redo</button>
</div>
}