State management is one of the most important pieces of an app, and there are a ton of choices for those in the React ecosystem.
In particular, developers building iOS and Android mobile apps with React using Capacitor and Ionic React often ask us for state management recommendations. Of course, there's Redux, which I remain a major fan of, but also much simpler state management approaches like MobX and rolling your own using the Context API.
I've spent a lot of time using Redux and also the bespoke approach with the Context API. Yet, I wasn't satisfied. I wanted to find something that was simple but high performance, and had native integration with Hooks and Function components which I now use exclusively in React (sorry, never want to write the word class
ever again 😆).
That's when I stumbled on Pullstate. Pullstate is a small, relatively unknown library (just 300 stars at the time of this writing), but I expect it will become much more popular in time.
Exploring Pullstate
Pullstate provides a simple Store object that is registered globally, and provides hooks for accessing data from that store in a component:
store.ts:
interface StoreType {
user: User | null;
currentProject: Project | null;
}
const MyStore = new Store<StoreType>({
user: null,
currentProject: null
});
export default MyStore;
Then, in your component, simply use the useState
method provided on the store to select data from the store:
const UserProfile: React.FC = () => {
const user = MyStore.useState(s => s.user);
}
Modifying state
To update state in the store, use the update
method:
const setUser = (user: User) => {
MyStore.update((s, o) => {
s.user = user;
});
}
The update
function works by mutating a Draft of the state. That draft is then processed to produce a new state.
Usually, a state mutation would raise a red flag, but the magic of Pullstate comes from a really interesting project called Immer. Immer essentially proxies an object and then turns mutations on that object into a new object (in my limited experience with it). Sort of how the vdom does diffing to figure out a new DOM tree.
This is incredibly powerful and simple, but does have a few gotcha's. First, reference comparisons on objects in the s
value above will fail, because they are actually Proxy
objects. That means doing something like this won't work:
MyStore.update(s => {
s.project = s.projects.find(p => p === newProject)
});
Instead, use the second argument, o
above, which contains the un-proxied original state. Another gotcha is making sure not to return anything from the update
function.
Next steps
After having used Pullstate, I will have a hard time not recommending it to all Ionic React developers, and those using Capacitor with other React UI libraries.
I think Pullstate is a great middle ground between being simple for small projects, but clearly capable of scaling to much more complicated projects. For larger projects multiple stores can be created in parallel, for a sort of redux reducer-inspired organization.
Pullstate also comes with some convenience helpers for async actions to cut down on async state boilerplate (such as handling success and failure states), though I have not used those extensively yet.
Next on my list is exploring how this might work with something like reselect for building reusable, memoized selectors.
What do you think? Have you used Pullstate? Please share in the comments!