Back to Journal
Mobile/Frontend

Frontend State Management Best Practices for Startup Teams

Battle-tested best practices for Frontend State Management tailored to Startup teams, including anti-patterns to avoid and a ready-to-use checklist.

Muneer Puthiya Purayil 14 min read

Startup frontend state management should be simple, fast to implement, and easy to refactor as the product evolves. Premature state architecture is one of the biggest time sinks for early-stage engineering teams. These best practices help you ship fast without creating state management debt.

Core Principle: Start with Server State

The single highest-impact decision for startup state management is separating server state from client state. React Query or SWR handles 80% of what startups typically put in global stores:

typescript
1// Before: manual state management for API data
2const [users, setUsers] = useState<User[]>([]);
3const [loading, setLoading] = useState(false);
4const [error, setError] = useState<Error | null>(null);
5 
6useEffect(() => {
7 setLoading(true);
8 fetchUsers()
9 .then(setUsers)
10 .catch(setError)
11 .finally(() => setLoading(false));
12}, []);
13 
14// After: React Query handles everything
15const { data: users, isLoading, error } = useQuery({
16 queryKey: ["users"],
17 queryFn: fetchUsers,
18});
19 

React Query provides caching, background refetching, optimistic updates, pagination, and error retry — all patterns you'd otherwise build manually.

Best Practices

1. Use Zustand for Minimal Global State

Zustand is the right choice for startups: minimal API, TypeScript-first, no boilerplate:

typescript
1import { create } from "zustand";
2 
3interface AppState {
4 user: User | null;
5 theme: "light" | "dark";
6 sidebarOpen: boolean;
7 setUser: (user: User | null) => void;
8 toggleTheme: () => void;
9 toggleSidebar: () => void;
10}
11 
12export const useAppStore = create<AppState>((set) => ({
13 user: null,
14 theme: "light",
15 sidebarOpen: true,
16 setUser: (user) => set({ user }),
17 toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
18 toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
19}));
20 

This store handles the only truly global concerns: auth state, theme, and layout. Everything else is either server state (React Query) or component state (useState).

2. Colocate State with Components

State should live as close to where it's used as possible:

typescript
1// Good: state lives where it's needed
2function SearchFilters() {
3 const [query, setQuery] = useState("");
4 const [category, setCategory] = useState("all");
5 // State stays local — no need for global access
6}
7 
8// Bad: lifting state to global store unnecessarily
9const useStore = create((set) => ({
10 searchQuery: "",
11 searchCategory: "all",
12 setSearchQuery: (q) => set({ searchQuery: q }),
13}));
14 

3. URL State for Shareable State

Filters, pagination, and tab selections belong in the URL:

typescript
1import { useSearchParams } from "react-router-dom";
2 
3function ProductList() {
4 const [searchParams, setSearchParams] = useSearchParams();
5 const page = Number(searchParams.get("page") ?? "1");
6 const category = searchParams.get("category") ?? "all";
7 const sort = searchParams.get("sort") ?? "newest";
8 
9 const { data } = useQuery({
10 queryKey: ["products", { page, category, sort }],
11 queryFn: () => fetchProducts({ page, category, sort }),
12 });
13 
14 return (
15 <div>
16 <Filters
17 category={category}
18 sort={sort}
19 onChange={(filters) => setSearchParams(filters)}
20 />
21 <ProductGrid products={data?.items ?? []} />
22 <Pagination page={page} total={data?.total ?? 0} />
23 </div>
24 );
25}
26 

4. Don't Abstract Too Early

Startups should resist creating state management abstractions until patterns repeat at least three times:

typescript
1// Week 1-4: Just use hooks directly
2function Dashboard() {
3 const { data: stats } = useQuery({ queryKey: ["stats"], queryFn: fetchStats });
4 const { data: activity } = useQuery({ queryKey: ["activity"], queryFn: fetchActivity });
5 return <DashboardUI stats={stats} activity={activity} />;
6}
7 
8// Week 8+: Pattern repeats, NOW abstract
9function useDashboardData() {
10 const stats = useQuery({ queryKey: ["stats"], queryFn: fetchStats });
11 const activity = useQuery({ queryKey: ["activity"], queryFn: fetchActivity });
12 return { stats, activity };
13}
14 

5. Form State Stays in Forms

Never put form state in global stores:

typescript
1// Use react-hook-form or just local state
2import { useForm } from "react-hook-form";
3 
4function CreateProjectForm({ onSubmit }: { onSubmit: (data: ProjectInput) => void }) {
5 const { register, handleSubmit, formState: { errors } } = useForm<ProjectInput>();
6
7 return (
8 <form onSubmit={handleSubmit(onSubmit)}>
9 <input {...register("name", { required: true })} />
10 {errors.name && <span>Name is required</span>}
11 <button type="submit">Create</button>
12 </form>
13 );
14}
15 

Need a second opinion on your mobile/frontend architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Anti-Patterns to Avoid

  1. Redux on day one — Redux adds significant boilerplate. Use Zustand or just React Query + useState. Add structure later when you need it.
  2. Global state for API data — use React Query. Period. Manual API state management is the #1 source of bugs in startup frontends.
  3. State management for form validation — use react-hook-form or Formik. Don't build form state into your application store.
  4. Over-normalized data — normalization is for enterprise apps with complex entity relationships. Startups should keep data in the shape the API returns it.
  5. Premature state architecture — don't design state architecture before you have features. Let the state structure emerge from real needs.

Checklist

  • API data uses React Query or SWR, not manual state
  • Global store contains only auth, theme, and cross-cutting concerns
  • Component state uses useState for local UI concerns
  • URL state handles shareable state (filters, pagination, tabs)
  • Form state uses react-hook-form or local state, not global store
  • No state abstractions until a pattern repeats 3+ times
  • State is colocated with the components that use it
  • Total lines of state management code < 200

Conclusion

The best state management architecture for a startup is the one that lets you ship features fastest. In 2025, that means React Query for server state, Zustand for the minimal global state you actually need, and useState for everything else. Resist the urge to over-architect — you can always add structure later, but you can't get back the weeks spent building state management infrastructure you didn't need yet.

FAQ

Need expert help?

Building with mobile/frontend?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026