DEV Community

Cover image for React Native app built with zustand and tailwind
Juan Rodriguez
Juan Rodriguez

Posted on • Updated on

React Native app built with zustand and tailwind

This post was originally published at https://blog.jbrio.net/post/rn-zustand/

Introduction

In this article I'm going to share my impressions after migrating my React Native app from redux/sagas to zustand for state management.

The app is ControlR, a mobile app to manage Unraid servers.

What is Unraid ?

Unraid is a software solution for storing your media content in-house.

I purchased my first Pro license in 2010 and the second one in 2013.
I'm sort of an early adopter, being among the first 2.5k owners of a key :)

I have a third license (Plus), which was very graciously offered by the Unraid owners to support the development of ControlR for Unraid.

Today's approach of storing content in the cloud & subscribing to multiple streaming services is convenient, but it does have some down sides:

  • Content can diseappear from time to time
  • You need to over subscribe in order to access all the content you want
  • You're not really in control of the media you want to consume

Unraid is a solution that helps you overcome these issues.

What about ControlR ?

ControlR is a React Native cross-platform (Android and iOS) mobile app, born out of my necessity of having an easy way to manage my Unraid servers.

I first published it in 2016.

Fast forward to 2021, and having hit the 5 years milestone :), the app was still using the same architecture and UI/UX from day one.

It definitely needed a UI overhaul, to support recent mobile features (notch, system color scheme, etc.)

This led me to create ControlR 5.

rn-zustand-servers

Architecture

In 2016, the most popular option for state management was redux, which I used, as well as redux-saga middleware to handle business logic.

I really like how redux/sagas helps you keep the business logic separate from the UI.

Conceptually, you get a very clear MVC (Model/View/Controller) pattern.

For example, I had a metrics saga to collect analytics, taking advantage of redux's single store centralized actions flow.

const SCollect = function* GSCollect(action) {
  if (!__DEV__ && action.meta) {
    let content = JSON.stringify(action.payload);
    Analytics.trackEvent(action.type, {
      payload: content,
    });
  }
};

export const sagas = {
  collect: takeEvery("*", SCollect),
};
Enter fullscreen mode Exit fullscreen mode

This is completely separate from the code that defines and renders the UI.

Using redux did mean you needed to write quite a lot of boilerplate code, but this can be solved today by using Redux Toolkit (RTK), as well as RTK Query.

When I started working on the rewrite of the app (at the beginning of 2020), I actually started with RTK and it reduced the boilerplate dramatically.

const env = createSlice({
  name: "env",
  initialState: envInitialState,
  reducers: {
    setup: (state, { payload }: PayloadAction<EnvSetup>) => {
      state.colors = themes[payload.theme];
      state.version = payload.version;
    },
    setRefreshing: (state, { payload }: PayloadAction<boolean>) => {
      state.refreshing = payload;
    },
    setLoadingApps: (state, { payload }: PayloadAction<boolean>) => {
      state.loadingApps = payload;
    },
  },
  extraReducers: {
    [setTheme.type]: (state: EnvState, { payload }: PayloadAction<Theme>) => {
      state.colors = themes[payload];
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

I had to suspend the rewrite, though, due to the real-lifeβ„’ well known events.

That code laid dormant for about a year, until I had to release an app update to fix an issue users were reporting.

At that point in time, I decided to explore state management alternatives.

But before digging into that, let me briefly mention tailwindcss.

Styling

The previous version of the app used react-native-tachyons, which one could argue is a precursor of tailwind.

I've always enjoyed functional styling libraries, for how immediate they are when applying style to a component/element and the learning curve is a matter of hours, few days at most.

I decided to switch from tachyons to tailwind because I had already used it for a web app and enjoyed doing so.

Out of the box, tailwindcss offered everything I needed to style my mobile app, so I was very satisfied with how everything turned out.

Enter zustand

I took a look at react-query, zustand, jotai and recoil, eventually settling on zustand.

It's a very easy to use but quite powerful library, hooks based, with built-ins such as persistence and a strong redux likeness.

My redux env slice became the zustand env store:

const useEnvStore = create<Env>(
  persist(
    (set) => ({
      hydrated: false,
      firstRun: true,
      clearFirstRun: () => set({ firstRun: false }),
      setHydrated: () => set({ hydrated: true }),
    }),
    {
      name: 'env',
      getStorage: () => storage,
      whitelist: ['firstRun'],
      onRehydrateStorage: () => (state, error) => {
        if (state) {
          state.setHydrated()
        }
      },
    },
  ),
)
Enter fullscreen mode Exit fullscreen mode

Most of the code here is for setting up persistence.

It's concise yet expressive.

Redux vs Zustand

Redux and sagas allowed great control of the overall app flow, sagas excel at that kind of orchestration, I could even navigate screens from sagas.

With zustand it feels like you have more UI coupling.

I think you can eventually move everything into zustand store actions, but I still need to dig a bit deeper into it.

For example, one feature that was very easy with redux/sagas was navigate to the detail screen of a server if there was only one item in the list.

I haven't found a way to replicate this with zustand (aided by my existing constraints), although I haven't put a lot of effort into it yet.

I've also experienced some verbosity: binding state and actions can get a bit repetitive, although I've read this can be improved.

const order = useServers((state) => state.order);
const setCurrent = useServers((state) => state.setCurrent);
const servers = useServers((state) => state.items);
const refreshing = useServers((state) => state.refreshing);
const refresh = useServers((state) => state.refresh);
const deleteServer = useServers((state) => state.deleteServer);
Enter fullscreen mode Exit fullscreen mode

Conclusion

zustand works perfectly for my app and I have enjoyed using it.

There are other features I want to implement (use immer for example), I'll tackle them as part of the learning process in upcoming releases.

Addenda

Check out Unraid, it's extremely versatile and once you're in, you will be hooked.

The Spaceinvader One Youtube channel has many tutorials on getting the most out of your Unraid server.

Also check out ControlR for Unraid, I can assure it'll help you manage your Unraid servers (shameless plug πŸ™Œ )

Discussion (1)

Collapse
pkhalisoft profile image
Muhammad Khalil

This is interesting.

Am trying out Zustand, and this just motivated me the more...