Skip to main content

State management

One of the most common questions for new engineers on postman-app is: "Where does this data live, and why?" The app uses multiple state management approaches depending on the type of data. This page explains each one and when to use it.


The core insight: not all state is the same

There are two fundamentally different kinds of state in any application:

Server state — Data that lives on the backend. Collections, workspaces, environments, user info. The frontend is just a cache of this data. It can go stale. Multiple users can change it simultaneously. It needs to be fetched, synchronized, and eventually discarded.

Client state — Data that only exists in the app. Which tab is selected. Whether a modal is open. What text is in an input. This state doesn't come from a server and doesn't need to be synchronized with anyone else.

Confusing these two is the source of most state management bugs. Trying to manually manage server state with client-state tools leads to stale data, cache inconsistency, and over-fetching. Postman-app now uses dedicated tools for each.


React Query — for server state (use this first)

React Query (also called TanStack Query) is the standard for all data that comes from the backend.

// data/workspace-data/src/queries/useWorkspaceQuery.ts
import { useQuery } from '@tanstack/react-query';
import { gatewayClient } from '@postman-app/gateway-service';

export const useWorkspaceQuery = (workspaceId: string) => {
return useQuery({
queryKey: ['workspace', workspaceId],
queryFn: () => gatewayClient.get(`/workspaces/${workspaceId}`),
staleTime: 5 * 60 * 1000, // consider fresh for 5 minutes
});
};

What React Query gives you for free:

  • Caching — the same data fetched by 10 components results in 1 network request
  • Background revalidation — stale data is shown immediately, then refreshed in the background
  • Loading and error statesisLoading, isError, data without any boilerplate
  • Automatic retries — failed requests retry automatically with backoff
  • Optimistic updates — mutations update the cache instantly, before the server confirms

When to use React Query: any time you're fetching data from the backend. Collections, workspaces, user profile, environments, API definitions — all of these are server state. Use useQuery for reads, useMutation for writes.


Zustand — for shared client state

Zustand is the standard for client state that needs to be shared between multiple components.

// data/active-workspace-data/src/store.ts
import { create } from 'zustand';

interface ActiveWorkspaceStore {
workspaceId: string | null;
setWorkspaceId: (id: string) => void;
}

export const useActiveWorkspaceStore = create<ActiveWorkspaceStore>((set) => ({
workspaceId: null,
setWorkspaceId: (id) => set({ workspaceId: id }),
}));
// In any component, anywhere in the tree:
const { workspaceId } = useActiveWorkspaceStore();

When to use Zustand: client-only state that multiple components need to read or write, and that doesn't come from the server. Which workspace is active. What the current theme is. Global UI state like "is the sidebar collapsed."

Zustand is intentionally minimal — no boilerplate, no reducers, no actions. Just a store with state and functions to update it.


React state — for local UI state

For state that is purely local to one component and doesn't need to be shared, use React's built-in useState and useReducer.

// A modal's open/close state — no other component cares about this
const [isModalOpen, setIsModalOpen] = useState(false);

// A form's input values — local to this form
const [formValues, setFormValues] = useState({ name: '', description: '' });

When to use React state: anything that only one component cares about. Don't put it in a Zustand store "just in case" — that leads to bloated global state. Keep it local until you actually need to share it.


MobX — the legacy (don't use for new code)

Older packages in postman-app use MobX for state management. MobX uses reactive observables — when you modify a store's property, any component that reads that property automatically re-renders.

// Example of MobX pattern (legacy — don't write new code like this)
import { makeAutoObservable } from 'mobx';

class WorkspaceStore {
workspaceId: string | null = null;

constructor() {
makeAutoObservable(this);
}

setWorkspaceId(id: string) {
this.workspaceId = id;
}
}

export const workspaceStore = new WorkspaceStore();

You will see MobX throughout src/renderer/ and in some older data/ packages. Do not use MobX in new code. Zustand is the replacement for client state, React Query for server state. If you're extracting code from src/renderer/ that uses MobX, migrate it to Zustand + React Query as you go.


The decision tree

Is this data from the backend (API call required)?
YES → React Query
- useQuery() for reads
- useMutation() for writes

Is this data shared between multiple components?
YES → Zustand
- One store per domain/feature area
- Keep stores small and focused

Is this data only used by one component?
YES → React useState or useReducer

Am I looking at existing code using MobX?
→ Don't extend it. Migrate when touching the code.

Where state lives in the layer structure

State management follows the layer hierarchy:

  • data/ packages — own the Zustand stores and React Query hooks for their domain. This is the canonical location for shared state. No UI code here.
  • ui-features/ — read from data/ stores and queries. May have local React state for their own UI concerns.
  • views/ — mostly read from the stores and ui-features. Rarely have their own state.

The rule: state flows down, never up. A data/ package doesn't know which ui-feature reads from it. A ui-feature doesn't know which view renders it.


References

  • data/workspace-data/README.md — example of React Query + Zustand patterns
  • platform-libs/react-query-utils/README.md — base utilities and patterns for React Query
  • State Management & Data Fetching skill — AGENTS.md /state-management-and-data-fetching