Metronic React
Getting Started

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:

ProviderPurposePath
Auth ProviderAuthentication with Supabasesrc/auth/providers/supabase-provider.tsx
Theme ProviderDark/light mode managementsrc/providers/theme-provider.tsx
I18n ProviderTranslations and RTL supportsrc/providers/i18n-provider.tsx
Settings ProviderApp configuration and preferencessrc/providers/settings-provider.tsx
Query ProviderData fetching and cachingsrc/providers/query-provider.tsx
Tooltips ProviderGlobal tooltip configurationsrc/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:

  1. Single Responsibility: Each provider should handle one aspect of your application
  2. Clear Interface: Expose a clean API through a custom hook
  3. Default Values: Provide sensible defaults for context values
  4. Error Handling: Include error checking in your custom hook
  5. Memoization: Use useMemo and useCallback for performance
  6. 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>
  );
}