Provider System
How to use Metronic React's provider ecosystem to access global features
Provider System
Metronic React uses a provider-based architecture to manage global state and features across your application. This approach offers several advantages:
- Centralized State Management: Providers create a central source of truth for application data
- Component Decoupling: Components can access shared state without prop drilling
- Contextual Features: Features like authentication and theme are available throughout your app
- Testability: Easier mocking of global state during testing
Core Providers
Metronic React includes several core providers that work together to create a cohesive system:
Provider | Purpose | Path |
---|---|---|
Auth Provider | Authentication with Supabase | src/auth/providers/supabase-provider.tsx |
Theme Provider | Dark/light mode management | src/providers/theme-provider.tsx |
I18n Provider | Translations and RTL support | src/providers/i18n-provider.tsx |
Settings Provider | App configuration and preferences | src/providers/settings-provider.tsx |
Query Provider | Data fetching and caching | src/providers/query-provider.tsx |
Tooltips Provider | Global tooltip configuration | src/providers/tooltips-provider.tsx |
Provider Integration
All providers are integrated at the application root in App.tsx
, creating a nested structure that makes all functionality available throughout the component tree. The order of provider nesting is important - providers that depend on other providers must be wrapped by them.
// Actual App.tsx structure
<QueryClientProvider client={queryClient}>
<AuthProvider>
<SettingsProvider>
<ThemeProvider>
<I18nProvider>
<HelmetProvider>
<TooltipsProvider>
<QueryProvider>
<BrowserRouter basename={BASE_URL}>
<AppRouting />
<Toaster />
</BrowserRouter>
</QueryProvider>
</TooltipsProvider>
</HelmetProvider>
</I18nProvider>
</ThemeProvider>
</SettingsProvider>
</AuthProvider>
</QueryClientProvider>
Auth Provider
The Auth Provider manages user authentication using Supabase, providing a unified interface for all authentication operations and user state.
When to Use
- When building pages that require authentication
- For implementing login, registration, or password reset flows
- To access current user information throughout your application
- When implementing role-based access control (admin vs regular users)
Usage
import { useAuthContext } from '@/auth/auth-context';
function ProfileSection() {
const { currentUser, isAdmin, logout } = useAuthContext();
if (!currentUser) return <div>Please sign in</div>;
return (
<div className="p-4">
<p>Welcome, {currentUser.fullname || currentUser.email}</p>
{isAdmin && <span className="badge">Admin</span>}
<button onClick={logout}>Sign Out</button>
</div>
);
}
Key Features
- Authentication State: Access current user data and auth status
- Login/Register: Handle user authentication and registration
- Profile Management: Update user information
- Password Operations: Reset password and verify email
- Access Control: Check user permissions with
isAdmin
- Session Persistence: Automatically restores authentication between page loads
Code Examples
Login:
const { login } = useAuthContext();
await login(email, password);
Register:
const { register } = useAuthContext();
await register(email, password, passwordConfirmation);
Update profile:
const { updateProfile } = useAuthContext();
await updateProfile({ first_name: "John", last_name: "Doe" });
Social login:
import { SupabaseAdapter } from '@/auth/adapters/supabase-adapter';
SupabaseAdapter.signInWithOAuth('google');
Theme Provider
The Theme Provider manages dark/light mode preferences, providing a consistent theming experience across your application. It's built on next-themes
and integrates with Tailwind CSS for seamless theme switching.
When to Use
- When implementing dark/light mode toggling
- For components that need to adapt to the current theme
- To access the user's system theme preferences
- When creating theme-aware UI elements
Usage
import { useTheme } from 'next-themes';
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
);
}
Key Features
- Theme Management: Switch between light, dark, and system themes
- Theme Detection: Access the system's preferred theme
- Persistent Preferences: Theme choice is saved in localStorage
- No Flash: Prevents light mode flash when the page loads
- CSS Variables: Works with Tailwind's dark mode via
class
strategy
Code Examples
Get resolved theme:
const { resolvedTheme } = useTheme();
const textColor = resolvedTheme === 'dark' ? 'text-white' : 'text-black';
Theme selector:
const { setTheme } = useTheme();
<select onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
I18n Provider
The I18n Provider manages translations and RTL direction, supporting multiple languages and right-to-left text direction. It's built on react-intl
for message formatting and translation management.
When to Use
- When building applications that need multi-language support
- For components that display user-facing text
- When implementing right-to-left (RTL) language support
- To allow users to switch between languages
Usage
import { useLanguage } from '@/providers/i18n-provider';
import { FormattedMessage } from 'react-intl';
function Header() {
const { currenLanguage, changeLanguage, isRTL } = useLanguage();
return (
<header className={isRTL() ? 'rtl' : 'ltr'}>
<h1><FormattedMessage id="welcome.title" /></h1>
<select
value={currenLanguage.code}
onChange={(e) => {
const lang = I18N_LANGUAGES.find(l => l.code === e.target.value);
if (lang) changeLanguage(lang);
}}
>
{I18N_LANGUAGES.map(lang => (
<option key={lang.code} value={lang.code}>{lang.label}</option>
))}
</select>
</header>
);
}
Key Features
- Translations: Use
<FormattedMessage>
for localized text - RTL Support: Detect and apply right-to-left direction
- Language Switching: Change the active language
- Persistent Preferences: Language choice is saved in localStorage
- Message Formatting: Support for variables, plurals, and date formatting
- URL Parameter: Language can be set via
?lang=
URL parameter
Code Examples
Simple message:
<FormattedMessage id="common.save" />
Message with values:
<FormattedMessage
id="welcome.user"
values={{ name: user.name }}
/>
Check RTL:
const { isRTL } = useLanguage();
<div className={isRTL() ? 'flex-row-reverse' : 'flex-row'}>
Settings Provider
The Settings Provider manages application settings and preferences, providing a unified interface for reading and writing configuration options. It persists settings to localStorage for a consistent user experience.
When to Use
- When building configurable UI components
- For user preferences like layout options
- When features need persistent configuration
- To manage default settings with user overrides
Usage
import { useSettings } from '@/providers/settings-provider';
function LayoutSettings() {
const { getOption, storeOption } = useSettings();
const isCollapsed = getOption('sidebar.collapsed');
return (
<div>
<label>
<input
type="checkbox"
checked={isCollapsed}
onChange={() => storeOption('sidebar.collapsed', !isCollapsed)}
/>
Collapse Sidebar
</label>
</div>
);
}
Key Features
- Get Settings: Access any setting with path notation
- Update Settings: Change settings temporarily or persistently
- Automatic Storage: Settings are saved to localStorage
- Nested Settings: Access deeply nested settings with dot notation
- Type Safety: TypeScript support for settings retrieval
- Default Values: Fallback to defaults defined in
settings.config.ts
Code Examples
Get setting:
const { getOption } = useSettings();
const headerFixed = getOption('layout.header.fixed');
Temporary change (not persisted):
const { setOption } = useSettings();
setOption('layout.sidebar.minimized', true);
Persistent change (saved to localStorage):
const { storeOption } = useSettings();
storeOption('theme.darkMode', true);
Query Provider
The Query Provider sets up React Query for data fetching, providing powerful tools for server state management. It handles caching, refetching, and error handling automatically.
When to Use
- When fetching data from APIs
- For components that need to display server data
- When implementing data mutations (create/update/delete)
- To efficiently cache and manage API responses
Usage
import { useQuery } from '@tanstack/react-query';
import { fetchProducts } from '@/api/products';
function ProductList() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
Key Features
- Data Fetching: Easily fetch and cache API data
- Loading States: Access loading and error states
- Automatic Refetching: Data refreshes when components remount
- Mutations: Update data with optimistic updates
- Caching: Automatic caching of query results
- Prefetching: Load data before it's needed
- Error Handling: Integrated error handling with toast notifications
Code Examples
Basic query:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
});
Query with parameters:
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId
});
Basic mutation:
const { mutate } = useMutation({
mutationFn: updateUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
// Call the mutation
mutate(updatedUser);
Route and Location Management
Instead of a dedicated Pathname Provider, Metronic React uses React Router's built-in hooks for route and location management. This provides all the functionality needed for tracking the current route and handling navigation.
When to Use
- When building navigation components like breadcrumbs
- For implementing "back" functionality
- When components need to react to route changes
- To track user navigation for analytics
Usage
import { useLocation, useNavigate } from 'react-router-dom';
function BreadcrumbNav() {
const { pathname } = useLocation();
const navigate = useNavigate();
return (
<nav>
<span>Current: {pathname}</span>
<button onClick={() => navigate(-1)}>
Go Back
</button>
</nav>
);
}
Key Features
- Current Path: Access the current route path using
useLocation
- Navigation: Programmatically navigate with
useNavigate
- URL Parameters: Access URL parameters with
useParams
- Route Matching: Match patterns against current URL with
useMatch
- Search Parameters: Get and set URL search params with
useSearchParams
Code Examples
Get path parameters:
import { useParams } from 'react-router-dom';
const { userId } = useParams();
Navigate programmatically:
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
// Navigate to a new page
navigate('/dashboard');
// Go back one step
navigate(-1);
Check if path matches:
import { useMatch } from 'react-router-dom';
const isUserRoute = useMatch('/users/*');
Tooltips Provider
The Tooltips Provider configures tooltips throughout the application, providing consistent tooltip behavior and styling. It's built on Radix UI for accessible tooltip implementation.
When to Use
- When implementing tooltips for UI elements
- For components that need hover explanations
- When building accessible interface elements
- To provide additional context for icons or buttons
Usage
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
function ActionButton() {
return (
<Tooltip>
<TooltipTrigger asChild>
<button>Action</button>
</TooltipTrigger>
<TooltipContent>
<p>Performs an action</p>
</TooltipContent>
</Tooltip>
);
}
Key Features
- Consistent Tooltips: Apply tooltips to any element
- Accessibility: Follows accessibility best practices
- Customization: Style tooltips to match your design
- Portal Rendering: Tooltips render in a portal to avoid clipping
- Positioning: Smart positioning to stay within viewport
Creating Custom Providers
You can create your own provider following this pattern:
// 1. Create context and hook
import { createContext, useContext, useState } from 'react';
const CounterContext = createContext({ count: 0, increment: () => {} });
export const useCounter = () => useContext(CounterContext);
// 2. Create provider component
export function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<CounterContext.Provider value={{ count, increment }}>
{children}
</CounterContext.Provider>
);
}
// 3. Use in components
function Counter() {
const { count, increment } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
Provider Design Principles
When creating custom providers, follow these best practices:
- Single Responsibility: Each provider should handle one aspect of your application
- Clear Interface: Expose a clean API through a custom hook
- Default Values: Provide sensible defaults for context values
- Error Handling: Include error checking in your custom hook
- Memoization: Use
useMemo
anduseCallback
for performance - Composition: Create specialized providers on top of more generic ones
Integration Pattern
Add your custom provider to the provider hierarchy in App.tsx
:
// In App.tsx
import { CounterProvider } from './providers/counter-provider';
function App() {
return (
<ExistingProviders>
<CounterProvider>
<AppRoutes />
</CounterProvider>
</ExistingProviders>
);
}