Invite-Only Early Access — Think Throo GitHub App is currently invite-only. Request access here.
2025November

Components structure in Umami codebase - Part 1.3

Inspired by BulletProof React, I applied its codebase architecture concepts to the Umami codebase.

This article focuses only on the components structure in Umami codebase.

Prerequisite

  1. Components structure in Umami codebase — Part 1.0

  2. Components structure in Umami codebase — Part 1.1

  3. Components structure in Umami codebase — Part 1.2

In the parts 1.0–1.2, we reviewed the /websites route components structure. In this part 1.3, we will review the components structure in /websites/[websiteId]

/websites/[websiteId] has so many pages, you can tell looking at its sidebar.

In this article, we review the:

  1. [websiteId]/page.tsx

  2. WebsitePage.tsx

  3. WebsiteChart.tsx

[websiteId]/page.tsx

You will find the following code in [websiteId]/page.tsx.

import { WebsitePage } from './WebsitePage';
import { Metadata } from 'next';
 
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
  const { websiteId } = await params;
 
  return <WebsitePage websiteId={websiteId} />;
}
 
export const metadata: Metadata = {
  title: 'Websites',
};

We saw the same pattern in the websites page.tsx, Page has the bare minimum defined and lets the WebsitePage define the details.

WebsitePage.tsx

You will find the below code in WebsitePage.tsx.

'use client';
import { Column } from '@umami/react-zen';
import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from './WebsiteChart';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { WebsitePanels } from './WebsitePanels';
import { WebsiteControls } from './WebsiteControls';
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
 
export function WebsitePage({ websiteId }: { websiteId: string }) {
  return (
    <Column gap>
      <WebsiteControls websiteId={websiteId} />
      <WebsiteMetricsBar websiteId={websiteId} showChange={true} />
      <Panel minHeight="520px">
        <WebsiteChart websiteId={websiteId} />
      </Panel>
      <WebsitePanels websiteId={websiteId} />
      <ExpandedViewModal websiteId={websiteId} />
    </Column>
  );
}

All these components are co-located within websites/[websiteId] folder.

WebsiteChart.tsx

WebsiteChart.tsx is defined as shown below:

import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useDateRange, useTimezone } from '@/components/hooks';
import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery';
import { PageviewsChart } from '@/components/metrics/PageviewsChart';
import { useMemo } from 'react';
 
export function WebsiteChart({
  websiteId,
  compareMode,
}: {
  websiteId: string;
  compareMode?: boolean;
}) {
  const { timezone } = useTimezone();
  const { dateRange, dateCompare } = useDateRange({ timezone: timezone });
  const { startDate, endDate, unit, value } = dateRange;
  const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({
    websiteId,
    compare: compareMode ? dateCompare?.compare : undefined,
  });
  const { pageviews, sessions, compare } = (data || {}) as any;
 
  const chartData = useMemo(() => {
    if (data) {
      const result = {
        pageviews,
        sessions,
      };
 
      if (compare) {
        result['compare'] = {
          pageviews: result.pageviews.map(({ x }, i) => ({
            x,
            y: compare.pageviews[i]?.y,
            d: compare.pageviews[i]?.x,
          })),
          sessions: result.sessions.map(({ x }, i) => ({
            x,
            y: compare.sessions[i]?.y,
            d: compare.sessions[i]?.x,
          })),
        };
      }
 
      return result;
    }
    return { pageviews: [], sessions: [] };
  }, [data, startDate, endDate, unit]);
 
  return (
    <LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}>
      <PageviewsChart
        key={value}
        data={chartData}
        minDate={startDate}
        maxDate={endDate}
        unit={unit}
      />
    </LoadingPanel>
  );
}

We see components are imported from 3 places:

  1. components/common

  2. components/metrics

Then hooks are imported from components/hooks. Do you see the pattern here?

These are located in websites/[websiteId] folder

Page -> WebsitePage -> WebsiteChart

However, the components and hooks used in WebsiteChart come from src/components folder.

About me:

Hey, my name is Ramu Narasinga. Email: ramu.narasinga@gmail.com

Tired of AI-generated code that works but nobody understands? 

I spent 3+ years studying OSS codebases and wrote 350+ articles on what makes them production-grade. I built an open source tool that reviews your PR against your existing codebase patterns.

Your codebase. Your patterns. Enforced. 

Get started for free —thinkthroo.com

References:

  1. https://github.com/umami-software/umami/tree/master/src/app/(main)/websites/%5BwebsiteId%5D

  2. https://github.com/umami-software/umami/blob/master/src/app/(main)/websites/%5BwebsiteId%5D/page.tsx

  3. https://github.com/umami-software/umami/blob/master/src/app/(main)/websites/page.tsx

  4. https://github.com/umami-software/umami/blob/master/src/app/(main)/websites/%5BwebsiteId%5D/WebsiteChart.tsx