loading...
Ionic

Pullstate - Simple hooks-based state management for React

maxlynch profile image Max Lynch ・3 min read

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!

Posted on by:

maxlynch profile

Max Lynch

@maxlynch

Co-creator of Ionic Framework. Creator of Capacitor. Programmer turned startup CEO

Ionic

The open source UI toolkit for developing high-quality cross-platform apps for native iOS, Android, and the web — all from a single codebase.

Discussion

pic
Editor guide
 

Looks interesting, might have to check it out. The use of selector to minimize re-renders is a nice feature. We found Contexts to be useful in certain circumstances, but could create a lot of overhead due to re-renders if the Context tried to handle too many things. We've been using Zustand lately which Pullstate looks to be quite similar to. Big fan of the transient updates and selectors.

 

Same feeling here, I used Zustand in two recent apps, it looks kind of similar (simple small state management, async friendly and powered by nice hooks).

That being said, doesn't mean I should not give Pullstate a try for a next project 🤙.

 

Thanks James, Zustand does indeed look very similar, will check it out. re: Context API, yes I've found that poor implementations using the Context API can have unintended performance issues. It's tempting to try to just use plain React for state management but in this case I find these simple libraries reduce the custom code you need to write quite a bit and have the added benefit of avoiding those unnecessary re-renders

 

In our experience it was less poor implementation and more misconception. It sounded like a replacement for state management at a Redux level (Context API + reducers). The part that was (and still seems to be) missing from the official docs is the fact that any change within the contexts state will have a knock-on effect. That seems like a pretty important piece of information not to mention.