Libraries like Valtio and Pinia for Vue use a mutator pattern instead of the actions, dispatch, and reducers pattern.
You can find the Valtio example in the react-state GitHub repo and the Pinia example in the vue-state GitHub repo. I'm using React 19.1.0, Valtio 2.3.2, Vue 13.5.40 and Pinia 4.0.3. Mutability Mutators Pinia Example Valtio Example Conclusion
Mutators mutate mutable state. You can change a part of your state without replacing the whole thing.
This article is a lot shorter than the last part of the series. Mutators seem much more intuitive to learn and more computationally efficient. (We don't have to replace the entire object.) So why not always use mutable state?
The mutator pattern allows you to have side effects in your state update methods. Mutable state is non-deterministic. It is easy to create transient bugs that are difficult to troubleshoot, like race conditions. The result of your updates may vary wildly depending on the order of method calls, (especially with parallel processing.)
The actions, dispatch, and reducers pattern forces you to define and handle every possible permutation of your state in the store. Mutators allow you to handle possible permutations when and how you want, including outside of the store. This may lead to verbose code handling every possible permutation in your components.
This isn't to say that the mutator pattern inherently produces bugs. Where the actions, dispatch, and reducers pattern imposes strict rules, the mutator pattern gives you freedom. With freedom comes the opportunity to create more bugs.
In the first part of this series, I said that reactive state is essentially creating getters and setters. Another set of terms for the same pattern is accessors and mutators.
In other words, instead of creating the observer pattern with actions, dispatch, and reducers, mutators just use the observer pattern.
Typically, this uses a Proxy under the hood. A Proxy is a copy of a JavaScript object (the target) and a handler. The handler defines operations to be performed on the target like accessors and mutators.
The methods for accessing state are called getters or accessors. The methods for mutating state are typically called actions.
I'll be using the Setup Store syntax, because I'm more familiar with Composition API syntax. Plus, I want the ability to use watchers and composables in my stores. The docs provide examples for the Options Stores syntax, if you're more comfortable with that.
Next, I create my store. Everything can go inside the store, include my update methods, called actions.
I'm using a Valtio example because Zustand is kind of a hybrid. Zustand's update syntax doesn't use actions, dispatch, and reducers. It's closer to accessors and mutators. However, it uses Immer under the hood to keep state immutable. If your updates get too complex and Immer can't keep up, you have to handle the immutability yourself.
Then, I create my component that accesses the proxy using useSnapshot. This creates a read-only subscriber that watches for changes. Note that unlike Redux, I don't have to rely on useEffect to trigger side effects.
Finally, I create my increment action. Valtio is purposefully unopinionated about organizing your actions. I like keeping it all together, so I'm just going to export my actions from the same file as my state.
The mutator pattern is much more intuitive than the actions, dispatch, and reducers pattern, but freedom makes it easier to create hard to troubleshoot bugs. It's hard not to view the actions, dispatch, and reducers pattern as the default - these examples even call their update methods actions. However, I've just touched the surface of both of these libraries. They're more flexible and much easier to get started with.
