When building a React application with TypeScript and Tailwind CSS, you eventually need to share state across components that are far apart in your component tree. You usually have to choose between the built-in React Context API and Zustand, a popular external state management library.
Picking the wrong tool early leads to messy prop drilling or frustrating re-render performance issues. Let's look at how both approaches work and when you should use each one.
- How React Context handles shared state natively
- How Zustand simplifies global state without boilerplate
- Clear code examples for both tools
- Common performance mistakes to avoid
- A definitive recommendation for your next project
Using React Context for Built-in State
React Context is built directly into React, meaning you do not need to install any external dependencies. It works by creating a provider component that wraps your app and passes data down to any consumer component.
Here is how you create and use a theme context in a TypeScript application:
import { createContext, useContext, useState, ReactNode } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}The key takeaway here is that React Context requires zero extra libraries, but it forces you to write custom provider wrappers and type guards.
Using Zustand for Simple Global State
Zustand is a lightweight state management library that uses hooks. It lets you store your data outside of the React component tree and pull only the pieces of state you need.
Here is how you build the exact same theme switcher using Zustand:
import { create } from 'zustand';
interface ThemeState {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const useThemeStore = create<ThemeState>((set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
}));The key takeaway here is that Zustand removes provider boilerplate and lets you access your state from anywhere, even outside of React components.
Common mistakes to watch out for
A frequent mistake with React Context is putting fast-changing state—like mouse coordinates or input text—into a global context. Because every component consuming that context re-renders on any change, this can cause severe lag in larger applications.
Another mistake with Zustand is selecting the entire store inside a component instead of a specific slice of data. Always select only the properties you need, like const theme = useThemeStore(state => state.theme), to prevent unnecessary component renders.
If you are building a small app or just managing static data like user authentication or UI themes, stick with React Context. For anything involving frequent updates, multiple stores, or deep component trees, install Zustand to save yourself headaches.
Open your current project, identify a piece of state causing prop-drilling, and try refactoring it into a Zustand store this afternoon.