Building a React dashboard in 2026 looks nothing like it did in 2023. React 19, Vite 8, shadcn/ui, and TanStack Query are the new default stack - Create React App was officially deprecated in February 2025, Material UI was overtaken by shadcn in 2025, and TanStack Query became the standard for server state. If you're following an older React dashboard tutorial, most of it is now wrong.
This guide is the distilled playbook for shipping a production React dashboard in 2026: React 19 + Vite + TypeScript, data fetching with TanStack Query, client state with Zustand, charts with Recharts, JWT auth with role-based access, code splitting, and WebSocket-driven real-time updates.
Key Takeaways
- The 2026 React dashboard stack: Vite 8 + React 19 + shadcn/ui + TanStack Query + Zustand + Recharts. Next.js 15 App Router if you need SSR.
- Templates vs. custom: A React dashboard template (shadcn blocks, TailAdmin, Zenith) ships in 2–3 days. Building from scratch takes 2–6 weeks but gives full control over data model and UX.
- State management: TanStack Query for server state (API data), Zustand for client state (filters, sidebar, dark mode). Avoid Redux unless you specifically need its middleware.
- Charts: Recharts for common types (line, bar, pie, area). ECharts if you need Sankey, geo maps, or pivot tables - and 10+ React chart libraries compared here.
- Multi-tenant adds 4–8 weeks: single-user dashboards are a 2–4 week build; multi-tenant with row-level security is substantially harder - plan the scope accordingly before writing your first query.
Prerequisites: Node.js 20+ (Vite 8 requires 20.19+ or 22.12+), npm 10+, and basic React and TypeScript familiarity. Every code block below is copy-paste runnable - follow the nine steps in order and you'll end up with a working dashboard on localhost:5173.
Finished code: github.com/databrainhq/dbn-demos-updated/tree/main/react-tutorial-scratch - clone, npm install, npm run dev, done. A Vite middleware serves mock dashboard metrics so you can skip the backend for now.
React Dashboard Templates vs. Building From Scratch
Before you write any code, the first decision: use a React dashboard template, or build from scratch. The honest trade-off:
| Approach | Time to ship | Cost | Flexibility | Best for |
|---|---|---|---|---|
| Template (shadcn blocks, TailAdmin, Modernize) | 2–3 days | Free to ~$80 one-time | Limited - you inherit the template's structure | Internal admin panels, MVPs, quick prototypes |
| Custom build (this guide) | 2–6 weeks | Developer time only | Full control | Customer-facing dashboards, unique UX |
| Embedded analytics platform (Metabase, Cube, and others) | 1–5 days | $85+/mo (varies widely by vendor) | Configurable, vendor-bounded | Multi-tenant SaaS, many chart types, RLS out-of-box |
The best React dashboard templates in 2026
If you're going the template route, these are the credible options:
- shadcn/ui Dashboard Blocks - free, official, modern. Full dashboard blocks (sidebar, stats, charts, tables) you copy into your project and own. This is what most teams start with in 2026.
- TailAdmin React - free + pro tiers, Tailwind-based, solid starting point for admin panels.
- Material Tailwind Dashboard React - MIT-licensed, classic Material UI aesthetic. Free.
- Zenith Dashboard (Next.js 16) - premium (~$60), React 19 + TypeScript + shadcn, 50+ pages, live theme customizer.
- Modernize - MUI-based, premium (~$50), 13+ apps + 75+ page templates.
Templates work well when the dashboard is incidental to your product (e.g., an admin panel for internal users). They fall short when the dashboard is a customer-facing feature - at that point, template rigidity becomes a constraint and you're better off building from scratch.
When to build custom (this guide): your dashboard is a customer-facing product feature, you need unique UX that no template gives you, or you're learning React 19 and want the practice.
Step 1: Set Up React 19 with Vite and TypeScript
Vite is the standard build tool for React in 2026 - the React team officially recommends it as the primary alternative after deprecating Create React App in February 2025. Vite 8 (released March 2026) ships with Rolldown - a Rust-based bundler that replaced Rollup - delivering significantly faster builds.
npm create vite@latest my-dashboard -- --template react-ts
cd my-dashboard
npm installWhy React 19 matters for dashboards
React 19 shipped features specifically useful for data-heavy applications like dashboards:
- React Compiler - Stable since October 2025, the compiler automatically memoizes components and hooks - eliminating the need for manual
useMemo/useCallback. New Vite, Next.js, and Expo projects ship with it enabled by default. Meta reports up to 12% faster initial loads and 2.5x faster interactions in production. For dashboards with dozens of components re-rendering on filter changes, this is a significant win. use()API - Read promises and context directly in render, simplifying data-fetching patterns.- Server Components - Render data-heavy components on the server, reducing JavaScript shipped to the client. Server Components work best with frameworks like Next.js - for client-side Vite apps, you won't use these directly.
- Actions and
useOptimistic- Handle form submissions (filter changes, date range selections) with automatic pending states.
For client-side React dashboards built with Vite, you'll benefit from the React Compiler (enabled by default in new projects), the use() API, and Actions. Server Components require a framework like Next.js.
Step 2: Install Your Component Library and Data Layer
shadcn/ui for dashboard components
shadcn/ui has become the fastest-growing component library for React dashboards. The State of React 2024 survey showed it doubling from 20% to 42% usage in a single year, with 80% positivity - the highest of any library. By 2025, it was on the verge of overtaking MUI for the top spot. Its popularity is partly driven by AI coding tools (Cursor, v0, Lovable) that default to generating shadcn/ui code. It provides composable components you own and can customize.
npx shadcn@latest init
npx shadcn@latest add card button table tabs separator skeleton badgeTanStack Query for server state
Every React dashboard fetches data from APIs. TanStack Query (formerly React Query) handles caching, background refetching, stale-while-revalidate, and error states - problems you'd otherwise solve manually with useState + useEffect.
npm install @tanstack/react-queryZustand for client state
For client-side state like sidebar open/closed, selected filters, and dark mode preference, Zustand provides a minimal, performant store without the boilerplate of Redux.
npm install zustandRecharts for data visualization
For custom-built charts, Recharts is the most popular React charting library. It's composable, responsive, and works well with TypeScript. For a full comparison against alternatives (ECharts, Nivo, Chart.js), see our guide on React chart libraries.
npm install rechartsStep 3: Build the Dashboard Layout
Create a responsive dashboard shell with a sidebar, header, and main content area using CSS Grid and shadcn/ui components.
src/components/DashboardLayout.tsx:
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface DashboardLayoutProps {
children: React.ReactNode;
}
const navItems = [
{ id: 'overview', label: 'Overview', icon: '📊' },
{ id: 'analytics', label: 'Analytics', icon: '📈' },
{ id: 'customers', label: 'Customers', icon: '👥' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
];
export function DashboardLayout({ children }: DashboardLayoutProps) {
const [activeItem, setActiveItem] = useState('overview');
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="grid min-h-screen grid-cols-[auto_1fr]">
<aside
className={cn(
'border-r bg-card transition-all duration-300',
sidebarOpen ? 'w-60' : 'w-16'
)}
>
<div className="flex h-14 items-center border-b px-4">
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen(!sidebarOpen)}
>
{sidebarOpen ? '◀' : '▶'}
</Button>
{sidebarOpen && (
<span className="ml-2 font-semibold">Dashboard</span>
)}
</div>
<nav className="flex flex-col gap-1 p-2">
{navItems.map((item) => (
<Button
key={item.id}
variant={activeItem === item.id ? 'secondary' : 'ghost'}
className="justify-start"
onClick={() => setActiveItem(item.id)}
>
<span className="text-lg">{item.icon}</span>
{sidebarOpen && <span className="ml-2">{item.label}</span>}
</Button>
))}
</nav>
</aside>
<main className="flex flex-col">
<header className="flex h-14 items-center justify-between border-b px-6">
<h1 className="text-lg font-semibold capitalize">{activeItem}</h1>
</header>
<div className="flex-1 p-6">{children}</div>
</main>
</div>
);
}Step 4: Fetch and Display Data with TanStack Query
This is where React dashboards differ from typical React apps. Dashboard data is server state - it comes from APIs, changes frequently, and multiple components often need the same data.
src/hooks/useDashboardData.ts:
import { useQuery } from '@tanstack/react-query';
interface DashboardMetrics {
totalRevenue: number;
activeUsers: number;
conversionRate: number;
avgOrderValue: number;
revenueByMonth: { month: string; revenue: number }[];
}
async function fetchDashboardMetrics(): Promise<DashboardMetrics> {
const response = await fetch('/api/dashboard/metrics');
if (!response.ok) throw new Error('Failed to fetch metrics');
return response.json();
}
export function useDashboardMetrics() {
return useQuery({
queryKey: ['dashboard', 'metrics'],
queryFn: fetchDashboardMetrics,
staleTime: 30_000,
refetchInterval: 60_000,
});
}src/components/MetricCards.tsx:
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { useDashboardMetrics } from '@/hooks/useDashboardData';
export function MetricCards() {
const { data, isLoading, error } = useDashboardMetrics();
if (error) return <div className="text-destructive">Failed to load metrics</div>;
const metrics = [
{ title: 'Total Revenue', value: data?.totalRevenue, format: (v: number) => `$${v.toLocaleString()}` },
{ title: 'Active Users', value: data?.activeUsers, format: (v: number) => v.toLocaleString() },
{ title: 'Conversion Rate', value: data?.conversionRate, format: (v: number) => `${v}%` },
{ title: 'Avg Order Value', value: data?.avgOrderValue, format: (v: number) => `$${v.toFixed(2)}` },
];
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{metrics.map((metric) => (
<Card key={metric.title}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{metric.title}
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-24" />
) : (
<p className="text-2xl font-bold">
{metric.format(metric.value!)}
</p>
)}
</CardContent>
</Card>
))}
</div>
);
}Notice that MetricCards and the RevenueChart below both call useDashboardMetrics(). TanStack Query deduplicates these - only one network request is made, and both components share the same cached data. This is the key architectural pattern for React dashboards: colocate data fetching with the component that needs it, and let the query layer handle deduplication and caching.
Also note the use of <Skeleton> instead of a spinner. Skeleton screens reduce perceived load time and prevent layout shift - both important for Core Web Vitals.
Step 5: Add Charts with Recharts
src/components/RevenueChart.tsx:
import {
LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer,
} from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useDashboardMetrics } from '@/hooks/useDashboardData';
export function RevenueChart() {
const { data, isLoading } = useDashboardMetrics();
return (
<Card>
<CardHeader>
<CardTitle>Revenue Over Time</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="h-[300px] animate-pulse rounded bg-muted" />
) : (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data?.revenueByMonth}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="revenue"
stroke="hsl(var(--primary))"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
);
}When Recharts Isn't Enough
Recharts covers the common chart types - line, bar, pie, area, scatter - really well. But production React dashboards often outgrow it: Sankey diagrams, geo maps, Gantt charts, gauges, waterfall charts, pivot tables. Each new chart type is either a multi-day D3 project or a dependency bump to something heavier like ECharts.
If you're evaluating options, see our breakdown of React chart libraries. And if your dashboard already needs five or six chart types and you're reaching for library docs every week, it's worth reading the embedded analytics in React guide to understand the trade-off before committing to building every chart type yourself.
Step 6: Authentication and Role-Based Access Control
Production React dashboards need authentication. Here's a minimal JWT pattern with React Router.
src/hooks/useAuth.ts:
import { create } from 'zustand';
interface User {
id: string;
email: string;
role: 'admin' | 'manager' | 'viewer';
}
interface AuthState {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
export const useAuth = create<AuthState>((set) => ({
user: null,
token: null,
login: async (email, password) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
set({ user: data.user, token: data.token });
},
logout: () => set({ user: null, token: null }),
}));src/components/PermissionGate.tsx:
import { useAuth } from '@/hooks/useAuth';
interface PermissionGateProps {
allowedRoles: string[];
children: React.ReactNode;
fallback?: React.ReactNode;
}
export function PermissionGate({
allowedRoles,
children,
fallback = null,
}: PermissionGateProps) {
const { user } = useAuth();
if (!user || !allowedRoles.includes(user.role)) return <>{fallback}</>;
return <>{children}</>;
}Usage:
<PermissionGate allowedRoles={['admin', 'manager']}>
<DangerousSettingsPanel />
</PermissionGate>Step 7: Performance Optimization
Dashboards are the most performance-sensitive pages in any application. Multiple charts, real-time data, and complex layouts compound to create performance challenges.
Code splitting by route
Lazy-load dashboard pages so users only download the JavaScript for the page they're viewing:
import { lazy, Suspense } from 'react';
const OverviewPage = lazy(() => import('./pages/Overview'));
const AnalyticsPage = lazy(() => import('./pages/Analytics'));
const CustomersPage = lazy(() => import('./pages/Customers'));
function App() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<Routes>
<Route path="/" element={<OverviewPage />} />
<Route path="/analytics" element={<AnalyticsPage />} />
<Route path="/customers" element={<CustomersPage />} />
</Routes>
</Suspense>
);
}Core Web Vitals targets
| Metric | Target | Why It Matters |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | Google ranking signal; users perceive >2.5s as slow |
| INP (Interaction to Next Paint) | < 200ms | Filter changes and drill-downs must feel instant |
| CLS (Cumulative Layout Shift) | < 0.1 | Charts loading shouldn't push content around |
Practical tips for React dashboards:
- Use
<Skeleton>components to reserve space before data loads (prevents CLS) - Set explicit
heighton chart containers (prevents layout shift) - Use TanStack Query's
staleTimeto serve cached data instantly while refetching in the background - The React Compiler handles memoization automatically - if you're on an older project without it, profile and add
useMemo/useCallbackwhere needed
Step 8: Real-Time Data Updates
For React dashboards that need live data (operations dashboards, trading screens, monitoring), add WebSocket support:
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export function useRealtimeMetrics() {
const queryClient = useQueryClient();
useEffect(() => {
const ws = new WebSocket('wss://your-api.com/ws/metrics');
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
queryClient.setQueryData(['dashboard', 'metrics'], (old: any) => ({
...old,
...update,
}));
};
return () => ws.close();
}, [queryClient]);
}This pattern updates TanStack Query's cache directly, so every component using useDashboardMetrics() re-renders with the new data automatically - no prop drilling, no manual state management.
Step 9: Putting It All Together
src/App.tsx:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { DashboardLayout } from '@/components/DashboardLayout';
import { MetricCards } from '@/components/MetricCards';
import { RevenueChart } from '@/components/RevenueChart';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<DashboardLayout>
<div className="space-y-6">
<MetricCards />
<div className="grid gap-6 lg:grid-cols-2">
<RevenueChart />
{/* Add more chart components here */}
</div>
</div>
</DashboardLayout>
</QueryClientProvider>
);
}
export default App;Run it:
npm run devVisit http://localhost:5173 to see your React dashboard.
What you should see
The browser renders a two-column layout: a collapsible sidebar on the left with four nav items (Overview, Analytics, Customers, Settings), and the dashboard content on the right. At the top of the content area, four KPI cards show skeleton placeholders briefly while TanStack Query fetches /api/dashboard/metrics, then render real numbers - total revenue, active users, conversion rate, average order value. Below the cards, a revenue line chart spans the full width with 12 months of data.
Click the sidebar collapse button and it shrinks to an icons-only rail. Switch nav items and the header title updates but the data doesn't refetch - TanStack Query serves it from cache, refreshing in the background every 60 seconds (see refetchInterval: 60_000 in useDashboardData.ts). That's the caching and deduplication behaviour from Step 4 working as intended.
The finished repo at react-tutorial-scratch renders the same result, with a Vite middleware serving mock metrics so there's no backend to stand up.
What Actually Breaks: Lessons From Production Dashboards
Everything above is the happy path. Here are the seven most common failure modes in production React dashboards - usually surfacing after launch, usually from the same set of unchecked assumptions. None of these are obvious from a tutorial, but all of them are fixable cheaply if you know to look.
1. The filter bar that re-fetches 15 charts on every keystroke
Teams wire up a search input to a Zustand store, every chart subscribes, every keystroke re-queries the backend. At three charts it's imperceptible. At 15 it melts the API and the user's CPU. Fix: debounce the filter state update (300ms is a safe default), and make sure your queryKey only changes once the debounced value lands - not on the intermediate keystrokes.
2. Mock data that was 10× smaller than production
Development uses a fixture with 500 rows. Production hits 50,000. TanStack Query looks instant in dev because the mock responds in 5ms; production responds in 400ms and the UI feels broken. Fix: seed your dev database with realistic row counts from day one. If you can't, generate a 50k-row fixture with faker and use it as your baseline. This catches slow queries, unindexed joins, and chart-rendering bottlenecks while they're still cheap to fix.
3. The timezone bug every dashboard ships with
Your server stores timestamps in UTC. The user is in IST. The charts show "Monday" when the user was looking at their Tuesday morning. This bug ships in virtually every first-version dashboard - the only question is which timezone got picked silently. Fix: decide explicitly and document it: display in the user's browser timezone, the organization's configured timezone, or UTC. Whichever you pick, show it in the UI ("All times in PT") so the user isn't guessing.
4. Charts that render fine at 500 rows and die at 50,000
Recharts renders SVG paths. At small row counts this is beautiful - composable, animatable, responsive. Past roughly 10,000 data points, SVG path complexity explodes, the browser's main thread stalls, and INP goes past 1,000ms. Fix: for high-cardinality series, either switch to a canvas-based chart library (Chart.js, ECharts with renderer: 'canvas'), or pre-aggregate on the server before sending. Never send 50k points to the browser when the user can only see ~1,000 pixels of chart width.
5. The PM ask that turns into a sprint: "can we add one more filter?"
Adding a filter looks like two hours of work. In practice, it propagates through every queryKey, every chart component, every saved view, and every exported report. Multi-select vs. single-select doubles the complexity. Dependent filters (filter B only valid values given filter A) quadruple it. Fix: treat filters as a first-class architectural concern from day one. Build a generic filter engine that holds filter state in one place, derives queryKeys automatically, and serializes to URL params for sharing. The upfront cost is a week; the savings after the fourth filter request are infinite.
6. The bundle that's 400 KB heavier than it needs to be
Default imports pull in entire libraries. import * as Recharts from 'recharts' is 135 KB gzipped; selective imports land closer to 50 KB. Same pattern for lodash (280 KB → 8 KB), date-fns (76 KB → 12 KB), and most chart libraries. Fix: run npm run build -- --report (or rollup-plugin-visualizer) before shipping and look at the top 10 chunks. Anything unexpectedly large - usually moment, full-lodash, or icon packs - gets a selective-import rewrite. A 20-minute audit usually saves 200–400 KB.
7. The "can we export this to CSV?" feature that ate a month
It looks trivial. Ship a hidden <a download> with a CSV blob, done. Then the PM asks for: filtered exports (state propagation), styled PDFs (server-side rendering), scheduled email reports (queue + templating + deliverability), and access control ("viewers can't export"). Each one is a week. Fix: scope exports honestly at the start. If the product needs more than flat CSV, budget 3–4 weeks, not 3–4 days - and factor in the permanent maintenance tax of every new delivery format, schedule, and permission rule the team will accumulate over the product's lifetime.
React Dashboard Examples: Three Common Patterns
Not every React dashboard looks the same. The code above is the foundation - here are the three most common shapes built on top of it, and what changes for each:
1. Analytics dashboard (SaaS-facing)
What it looks like: time-series charts, cohort tables, funnels, filter bars. Used by customer success, growth, product teams. Example: Mixpanel-style dashboards inside a SaaS product.
Key additions to the base:
- Date range picker - Zustand store for the current range, propagated into every
useQueryviaqueryKey - Cross-filtering - clicking a bar filters every other chart on the page. Getting this right takes weeks past a toy example: query-key invalidation, URL state sync, filter-dependency graphs
- CSV export - straightforward to build with a hidden
<a download>link generating the CSV client-side
2. Admin dashboard (internal tools)
What it looks like: user lists, permission management, billing, settings. Used by your team, not your customers. A React admin dashboard is typically CRUD-heavy with occasional charts.
Key additions:
- Data tables with sort/filter/pagination - use
@tanstack/react-tablev8, not Recharts - Form-heavy workflows - React 19 Actions +
useOptimisticmake this much cleaner than pre-19 - Bulk actions - row selection + server batch endpoints
For admin-heavy apps, consider a template like shadcn/ui dashboard blocks or TailAdmin - the base layout is the same but you get pre-built tables and form patterns.
3. Operations / monitoring dashboard
What it looks like: real-time metrics, alerts, status indicators, geo maps. Used by DevOps, on-call teams, trading desks. Every chart is live-updating.
Key additions:
- WebSocket integration - see Step 8 above
- Alert threshold rules - stored server-side, evaluated either server-side or client-side depending on scale
- Geo visualization - Recharts doesn't do maps; reach for ECharts (
echarts-for-react) or react-leaflet - Dense layouts - use
react-grid-layoutfor drag-and-drop resizable widgets
Each pattern has a different maintenance profile. Analytics dashboards are the hardest to scale (data volume + chart variety); admin dashboards are the easiest (CRUD is a solved problem); operations dashboards live or die by the realtime infrastructure.
React Dashboard Chart Libraries Compared
If you're building charts from scratch, here's how the major libraries compare for a React dashboard use case:
| Library | Bundle Size (gzipped) | Chart Types | TypeScript | Best For |
|---|---|---|---|---|
| Recharts | ~135 KB (full), ~50 KB with selective imports | 11 | Yes | Simple dashboards, React-first API |
| Chart.js | ~67 KB | 8 | Via @types/chart.js | Lightweight, canvas-based |
| Apache ECharts | ~273 KB (full), tree-shakeable to ~100 KB | 30+ | Yes | Complex visualizations, geo maps |
| D3 | ~80 KB (full), modular imports can reduce to ~30 KB | Unlimited | Via @types/d3 | Full control, custom visualizations |
| Nivo | ~120 KB (per chart) | 27 | Yes | Server-side rendering, motion |
For the full decision tree on which chart library to pick, see our deep-dive on 10 Best React Chart Libraries for Interactive Dashboards.
Deployment Checklist
Before shipping your React dashboard to production:
- [ ] Replace demo API endpoints with production URLs
- [ ] Store tokens and API keys in environment variables (never commit
.env) - [ ] Enable code splitting for each dashboard page
- [ ] Set explicit heights on chart containers (prevents CLS)
- [ ] Test on mobile devices (responsive sidebar, touch interactions)
- [ ] Verify Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1
- [ ] Add error boundaries around chart components
Next Steps
- Clone the starter: react-tutorial-scratch - the exact code from this guide, runnable with
npm install && npm run dev - Pick a chart library: 10 Best React Chart Libraries for Interactive Dashboards
- Grab a template instead: shadcn/ui Dashboard Blocks - free, official, matches the stack in this guide
- Not the right path for you? If your use case is multi-tenant analytics with RLS, drill-downs, and exports out of the box, embedded analytics in React covers when that trade-off makes sense.
Covers React 19, Vite 8, shadcn/ui, TanStack Query v5, Recharts. Last updated April 2026.
Rahul Pattamatta is co-founder of Databrain, an embedded analytics platform for SaaS.



.png)
.png)



