Blog
Components structure in Umami codebase - Part 1.3

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. I study codebase architecture in large open-source projects.

Email: ramu.narasinga@gmail.com

I spent 200+ hours analyzing Supabase, shadcn/ui, LobeChat. Found the patterns that separate AI slop from production code. Stop refactoring AI slop. Start with proven patterns. Check out production-grade projects at 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