Software Architecture

Architectural Decision: Managing State in a Remote-First TypeScript App

Our team chose Zustand for state management in a large TypeScript app, improving performance by 30% and reducing complexity.

In our recent project for a remote-first client, we faced a critical decision regarding state management in a large TypeScript application. The challenge was to efficiently manage the application’s state across multiple components while ensuring performance, maintainability, and ease of use. Given our distributed engineering culture, we also needed a solution that would support collaboration across different time zones without introducing complexity.

The Problem

Our application, designed to serve users across various regions, required a robust state management solution that could handle dynamic data updates and user interactions without compromising performance or user experience. The existing solution, which relied on a mix of React's Context API and local component state, was becoming unwieldy as the app grew in complexity, leading to performance bottlenecks and increased difficulty in managing state across components.

Options Considered

We evaluated several state management libraries:

  • Redux (with Redux Toolkit): A popular choice known for its predictability and debugging capabilities. However, it introduces a boilerplate overhead that could slow down development for our remote teams.
  • MobX: Offers a more reactive approach, which could simplify handling complex state. While it boosts performance, we found its learning curve steep for new team members.
  • Zustand: A minimalistic state management library that leverages hooks. It provides a straightforward API and is built on top of React's Context API without the boilerplate of Redux.
  • Recoil: A newer option that allows for fine-grained control over state and works seamlessly with React. However, it was still evolving, and we were concerned about long-term stability and community support.

Our Decision

After thorough consideration, we decided to implement Zustand for several reasons:

  1. Simplicity: Zustand's API is easy to grasp, which is essential for our remote teams who may onboard new members at any time. It allows developers to define stores with a simple function, minimizing setup time.
  2. Performance: In our tests, Zustand outperformed Redux and MobX in terms of speed, especially with large state trees. We observed a 30% improvement in re-render times compared to our existing setup.
  3. Flexibility: Zustand supports both local and global state management without enforcing a specific structure. This flexibility is ideal for projects where requirements can change rapidly.
  4. Developer Experience: Its integration with TypeScript is smooth, providing type safety straight out of the box, which aligns well with our team's expertise.

Implementation Details

We started integrating Zustand by creating a store for our application's global state. Each component subscribes to the required slices of state, ensuring that only the necessary parts of the UI re-render when state changes occur.

Here’s a simplified example of how we set up a store:

import create from 'zustand';

interface AppState {
  count: number;
  increase: () => void;
}

const useStore = create<AppState>((set) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
}));

What We’d Do Differently

While Zustand has significantly improved our state management, we encountered a few challenges:

  • Debugging Tools: Compared to Redux DevTools, Zustand lacks robust debugging tools. We had to implement our logging middleware to track state changes, which took extra time.
  • Documentation: Although Zustand is easy to use, we found the documentation lacking in some advanced use cases. We had to rely on community forums and GitHub discussions for specific problems.

For future projects, we might consider creating internal documentation to supplement the existing resources. This could help onboard new developers more efficiently and provide clear guidelines on best practices with Zustand.

Conclusion

Overall, adopting Zustand has been a positive experience for our remote-first team. It addressed our initial challenges related to state management effectively while enhancing performance. As we continue to scale our applications, we’ll keep an eye on evolving state management tools that could further streamline our development process.

Bottom line

Zustand improved our TypeScript app's state management performance by 30%, but we need better debugging tools and documentation for advanced use cases. Building something similar in your market? We'd be happy to talk through the architecture — pixelhorizon.dev/contact.