Building Universal Apps in React Native — Part 2 — State Management

Anurag Alla
Tech at ZET
Published in
3 min readAug 10, 2023

State management is a topic of utmost importance in the world of React and React Native. With a plethora of options available, it can be overwhelming to explore each one in detail. However, the decision-making process revolves around determining whether your application’s state is predominantly driven by the server or the client.

Server State:

In our case, ensuring that our app remains synchronized with the server state is crucial in most scenarios. To achieve this, we required a library that abstracts API calls (side effects) while offering features such as caching, TypeScript support, and an exceptional developer experience (DX).

Among the various contenders, one library stood out as the clear winner: Tanstack Query (formerly known as react-query). This is by far the best library that one can use for handling server state for Restful services, given its remarkable capabilities. While SWR by Vercel came close, Tanstack Query’s superior DX, Query-based approach, automatic caching, background updates, and pagination support made it the optimal choice for our needs. However, it’s important to note that the choice would differ for graphql-based backend services, a topic beyond the scope of this blog post.

import React from 'react';
import { View, Text, FlatList } from 'react-native';
import { useQuery } from 'react-query';
import axios from 'axios';

interface User {
id: number;
name: string;
}

const fetchUsers = async (): Promise<User[]> => {
const response = await axios.get('/api/users');
return response.data;
};

const UserList: React.FC = () => {
const { data: users, isLoading, isError }
= useQuery({ queryKey: ['users'], queryFn: fetchUsers });

if (isLoading) {
return <Text>Loading...</Text>;
}

if (isError) {
return <Text>Error fetching data</Text>;
}

return (
<View>
<Text>User List</Text>
<FlatList
data={users}
keyExtractor={user => user.id.toString()}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
</View>
);
};

export default UserList;

Client State:

Although our application heavily relies on server state, it is essential to handle client state for various use cases in mobile apps. For instance, managing UI state to track form progress across screens or storing filter selections in listing screens. During our evaluation, we considered multiple options:

Redux: Redux is a widely adopted state management solution; however, it often involves a significant amount of boilerplate code that doesn’t align well with our specific use case.

Zustand: Zustand caught our attention due to its effortless setup and the ability to access a global store using hooks, similar to Redux. It strikes a balance between simplicity, maintainability, and an improved DX.

Jotai: Jotai is a suitable choice for decentralized state management. However, it requires meticulous planning and additional effort to create stores. Considering our team’s familiarity with the Redux approach, we preferred a solution that offers reduced boilerplate, maintainability, and a favorable DX.

With our requirements of simplicity, maintainability, and a favorable DX in mind, we ultimately selected Zustand as our preferred client state management library. Zustand’s simple API, which closely resembles Redux, makes it an excellent fit for our project.

import create from 'zustand';

interface User {
id: number;
name: string;
email: string;
}

interface UserStore {
users: User[];
addUser: (user: User) => void;
updateUser: (userId: number, user: User) => void;
deleteUser: (userId: number) => void;
}

export const useUserStore = create<UserStore>((set) => ({
users: [],
addUser: (user) => set((state) => ({ users: [...state.users, user] })),
updateUser: (userId, updatedUser) => {
set((state) => ({
users: state.users.map((user) =>
user.id === userId ? { ...user, ...updatedUser } : user
),
}));
},
deleteUser: (userId) =>
set((state) => ({ users: state.users.filter((user) => user.id !== userId) })),
}));

Conclusion

In conclusion, our journey at Zet to bring our fintech solutions to every platform led us to the selection of three powerful libraries:

Nativewind (read part 1 of this series) for our design system, Zustand for client state management, and Tanstack Query for handling server state.

By combining the power of Nativewind, Zustand, and Tanstack Query, we have built a robust foundation for our universal app. These libraries empower us to deliver our fintech solutions seamlessly across all platforms, providing a superior user experience.

--

--