When building scalable React apps, Context API is your best friend for passing data deeply without prop-drilling. But creating your own Context Provider the right way—especially in TypeScript—can feel intimidating.
This guide walks you through creating a custom context provider in both JavaScript and TypeScript, helping you manage global state like themes, user auth, or modals cleanly and effectively.
📦 What is React Context?
React's Context API provides a way to share values like auth status, app settings, or language preferences between components without having to explicitly pass props.
Typical use cases:
Dark/light theme
User authentication
Global modals or notifications
Language/locale settings
✅ JavaScript Example: Theme Context
1. Create ThemeContext.js
import React, { createContext, useState, useContext } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () =>
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
2. Wrap your app in ThemeProvider
// App.js
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
}
3. Use the context
import { useTheme } from './ThemeContext';
const MyComponent = () => {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
};
🧾 TypeScript Example: User Context
1. Create UserContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';
type User = {
name: string;
email: string;
};
type UserContextType = {
user: User | null;
login: (user: User) => void;
logout: () => void;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const login = (user: User) => setUser(user);
const logout = () => setUser(null);
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be used within a UserProvider');
return context;
};
2. Use in your app
// App.tsx
import { UserProvider } from './UserContext';
function App() {
return (
<UserProvider>
<Dashboard />
</UserProvider>
);
}
3. Access in a component
import { useUser } from './UserContext';
const Dashboard = () => {
const { user, login, logout } = useUser();
return (
<div>
{user ? (
<>
<p>Welcome, {user.name}</p>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={() => login({ name: 'Alice', email: 'a@example.com' })}>
Login
</button>
)}
</div>
);
};
💡 Best Practices
✅ Provide a custom hook (
useXyz
) for safer usage✅ Use TypeScript to prevent runtime errors in consuming components
🚫 Don’t overuse context—prefer props or state lifting for local data
🔁 Wrap state logic in context only when truly global