Skip to content

State Management Patterns

Abhinav Rastogi edited this page Mar 3, 2025 · 3 revisions

Managing State

In this document, we use the Branch Selector component as a case study to explore challenges with shared state/data management across multiple pages. We analyze three different approaches — Centralized Store or Per-API State Management, Per-Page State Management (View-Model), and an Encapsulated Component (Render Prop) — detailing their trade-offs and best use cases.

While the discussion revolves around the Branch Selector, the insights apply to any scenario where a shared data exists across pages.

Centralized Store Approach (Current Implementation) – The branch selector component retrieves data from a centralized store that is shared across multiple pages (data-model approach). Each page updates this global store, ensuring consistency but leading to synchronization issues and redundant API calls. In cases where a page utilizes incorrect logic to update the store or simply forgets to refresh stale data, it may lead to inconsistencies across pages, resulting in outdated selections, unexpected UI behavior, or unnecessary re-renders.

View-Model Approach (Per-Page State Management) – Implements the industry-standard MVVM pattern; Instead of relying on a shared store, each page independently manages its own branch selector state and business logic. The page sets up its own store and passes the relevant data down to the branch selector component, reducing synchronization issues but requiring more repeated setup across pages.

Encapsulated Component Approach (Render Prop Pattern) – The branch selector is designed as a self-contained component that encapsulates all API calls and business logic. Pages can simply import and use it as a render prop, keeping logic centralized while ensuring each instance remains independent and in sync.

TLDR? Jump to recommendations

1. Centralized Store Approach

In this approach, all pages use a global store to manage the branch selector state.

// branchSelectorStore.ts (Centralized Store using Zustand or similar)
import { create } from 'zustand';

export const useBranchSelectorStore = create((set) => ({
  selectedBranchOrTag: null,
  branchList: [],
  tagList: [],
  setSelectedBranchOrTag: (branchOrTag) => set({ selectedBranchOrTag: branchOrTag }),
  setBranchList: (branches) => set({ branchList: branches }),
  setTagList: (tags) => set({ tagList: tags }),
}));
// PageA.tsx (A page using the Branch Selector)
import { useBranchSelectorStore } from './branchSelectorStore';

const PageA = () => {
  const { selectedBranchOrTag, branchList, tagList, setSelectedBranchOrTag } = useBranchSelectorStore();
  
  return (
    <div>
      <h1>Page A</h1>
      <BranchSelector 
        selectedBranchOrTag, 
        ...
      />
    </div>
  );
};

Issues:

If PageA updates the store, PageB sees the same update, even though it might not need it.

  • Leads to unnecessary re-renders across pages.
  • API calls may still be needed for every instance of the component to prevent desynchronization.
  • We may have stale data in the store. Harder to debug. May lead to race conditions if the multiple sources update store asynchronously. eg. BranchList page uses the same store, but it only updates the branchList in the store for some repo. Now the branchList and tagList can be out of sync, storing data for different repos.

2. View-Model Approach (Per-Page State Management)

Each page manages its own state and passes it down to the component. This avoids synchronization issues but introduces code duplication. In case of the branch-selector this is a non-trivial amount of code and API calls being duplicated across many pages.

For simple views/pages, state can be managed in React.State itself. For complex views, state can be managed using stores provided by libraries likes Zustand.

// PageA.tsx (Using Own State Instead of Global Store)

const PageA = () => {
  // if the state is expected to be complex, we can use a store instead  
  const [selectedBranchOrTag, setSelectedBranchOrTag] = useState(null);
  const [branchList, setBranchList] = useState([]);
  const [tagList, setTagList] = useState([]);

  useEffect(() => {
    // Fetch branches and tags (API call simulation)
    fetchBranches().then(setBranchList);
    fetchTags().then(setTagList);
  }, []);

  return (
    <div>
      <h1>Page A</h1>
      <BranchSelector 
        selectedBranchOrTag={selectedBranchOrTag} 
        setSelectedBranchOrTag={setSelectedBranchOrTag}
        branchList={branchList} 
      />
    </div>
  );
};

Issues:

  • As every page manages its own state, it can lead to code duplication for the rare shared pieces (eg. Branch Selector)

3. Encapsulated Component Approach (Render Prop Pattern)

The branch selector encapsulates all API calls and logic and is used as a self-contained component. Pages just import and use it, avoiding duplication.

// BranchSelector.tsx (Encapsulated Component)

const BranchSelector = ({ handleBranchSelect }) => {
  const [branchList, setBranchList] = useState([]);
  const [tagList, setTagList] = useState([]);

  useEffect(() => {
    // Fetch API data once per instance
    fetchBranches().then(setBranchList);
    fetchTags().then(setTagList);
  }, []);

  return (
    <BranchSelectorView 
      branchList, 
      handleBranchSelect
    />
    );
};
// PageA.tsx (Using BranchSelector as a Render Prop)

import BranchSelector from './branch-selector'

const PageA = () => {
  const handleBranchSelect = {...}

  return (
    <div>
      <h1>Page A</h1>
      <BranchSelector handleBranchSelect />
    </div>
  );
};

Issues:

  • Less control over state management for specific page needs
  • Could require prop drilling if the reused component is deep in the view hierarchy

Comparison & Recommendation

Approach Pros Cons Best Use Case
Centralized Store Approach - Global consistency
- Avoids API duplication (in theory).
- Synchronization issues, hard to figure out if we have stale data.
- Can lead to unnecessary re-renders, requiring manual optimization via useMemo, etc.
- Defeats the purpose of a store if API calls are still needed to prevent desync.
- Harder to debug as multiple places are updating and reading from the store.
- When multiple pages must always reflect the same data AND there is a single source of truth for the data which does not need to be updated across pages.
- Genuine use cases for this are rare.
- Eg. Current user info, which is fetched once per session and displayed on all pages.
View-Model Approach (Per-Page State Management) - Each page manages its own state, avoiding sync issues.
- Greater flexibility as each page controls its own data.
- Can be implemented trivially in local React.State, only requiring solutions like Zustand for complex views with multiple data sources.
- Potential for increased code duplication as each page makes its own API calls, which may be a non-trivial amount of repeated code.
- Potential for inconsistent business logic across different pages.
- Potential for frequent re-renders, but this can be avoided via Zustand stores for complex screens.
- When different pages require their own state and should not interfere with each other.
- This model should meet 80-90% of your needs where each page just manages its own state and related business logic.
- Eg. Tag List can just store an array of Tags in local state.
RepoSummary can create a Zustand store to combine data from 5-6 APIs and selectively render parts of the screen.
Encapsulated Component Approach (Render Prop Pattern) - Self-contained logic simplifies reuse.
- No risk of global state sync issues.
- Each page can use it without worrying about managing state.
- Reduces boilerplate setup on each page.
- Reduces re-renders on each page.
- Less control over fine-tuning state management for specific page needs.
- Might introduce prop drilling if many props are required.
- Views are not “pure” anymore as they can require parts of it to be passed in via render props.
- When the same logic needs to be reused across pages while maintaining independence.
- Eg. Very effective in specialized use cases such as branch-selector, connectors, etc.

Clone this wiki locally