Blog
State Management in Umami codebase - Part 1.4

State Management in Umami codebase - Part 1.4

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

This article focuses only on the state management in Umami codebase.

Why learn this pattern?

Let’s be honest, your useState code is probably a mess. Mine was too.

Look, we’ve all written this:

const [websites, setWebsites] = useState([]);
const addWebsite = async (data) => {
  const newWebsite = await api.post('/websites', data);
  setWebsites([...websites, newWebsite]); 
};

We all start the same way, useState for everything. Throw in Context API when passing props gets annoying. Works fine until your app grows.

Then you’re debugging why:

  • Your app is slow because state is everywhere

  • The UI shows old data after mutations

  • Clicking fast causes weird race conditions

  • You have to manually refresh data in 10 places

Production apps like Umami don’t have these problems because they use different patterns.

Let me show you what I learned from studying their code.

Prerequisites

  1. State management in Umami codebase — Part 1.0

  2. State management in Umami codebase — Part 1.1

  3. State management in Umami codebase — Part 1.2

Approach

The approach we take is simple:

  1. Pick a route, for example, https://cloud.umami.is/analytics/us/websites

  2. Locate this route in Umami codebase

  3. Review how the state is managed

  4. Repeat this process for 3+ pages to establish a common pattern and identify exceptions

In Part 1.1, we reviewed the state management in the websites route. 

In Part 1.2, we reviewed the state management involved in creating a website. 

In the Part 1.3 , we reviewed how the state is managed in creating a goal. 

In this article, we review the state in listing the goals.

What is a Goal in Umami?

Umami Goals is a crucial tool in providing valuable insights into how well your website is meeting its objectives. With clear metrics and visualizations, our goals insights translates user behavior into actionable steps towards improvement.

Learn more about Goal in Umami.

I reviewed the code around listing goals and the following are used:

  1. GoalsPage

  2. useReportsQuery

GoalsPage

GoalsPage is defined as shown below:

'use client';
import { Column, Grid } from '@umami/react-zen';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
import { SectionHeader } from '@/components/common/SectionHeader';
import { useDateRange, useReportsQuery } from '@/components/hooks';
import { Goal } from './Goal';
import { GoalAddButton } from './GoalAddButton';

export function GoalsPage({ websiteId }: { websiteId: string }) {
  const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });
  const {
    dateRange: { startDate, endDate },
  } = useDateRange();

  return (
    <Column gap>
      <WebsiteControls websiteId={websiteId} />
      <SectionHeader>
        <GoalAddButton websiteId={websiteId} />
      </SectionHeader>
      <LoadingPanel data={data} isLoading={isLoading} error={error}>
        {data && (
          <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
            {data.data.map((report: any) => (
              <Panel key={report.id}>
                <Goal {...report} startDate={startDate} endDate={endDate} />
              </Panel>
            ))}
          </Grid>
        )}
      </LoadingPanel>
    </Column>
  );
}

This component displays all goals for a website.

const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });

This single line handles:

  • Fetching all goals for the website

  • Loading state

  • Error handling

  • Automatic refetching when cache is invalidated

Do you recall from Part 1.3 when we called touch('reports:goal') after creating a goal? This query listens for that and refetches automatically.

The useState version would look like this:

// The manual way:
const [goals, setGoals] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

const fetchGoals = async () => {
  setIsLoading(true);
  try {
    const response = await api.get(`/reports?websiteId=${websiteId}&type=goal`);
    setGoals(response.data);
  } catch (err) {
    setError(err);
  } finally {
    setIsLoading(false);
  }
};

useEffect(() => {
  fetchGoals();
}, [websiteId]);

// And you'd need to manually call fetchGoals() after creating a goal

With useReportsQuery, you skip all of that.

The below component renders the goals

<LoadingPanel data={data} isLoading={isLoading} error={error}>
  {data && (
    <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
      {data.data.map((report: any) => (
        <Panel key={report.id}>
          <Goal {...report} startDate={startDate} endDate={endDate} />
        </Panel>
      ))}
    </Grid>
  )}
</LoadingPanel>

useReportsQuery

useReportsQuery is defined as shown below:

import type { ReactQueryOptions } from '@/lib/types';
import { useApi } from '../useApi';
import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery';

export function useReportsQuery(
  { websiteId, type }: { websiteId: string; type?: string },
  options?: ReactQueryOptions,
) {
  const { modified } = useModified(`reports:${type}`);
  const { get } = useApi();

  return usePagedQuery({
    queryKey: ['reports', { websiteId, type, modified }],
    queryFn: async () => get('/reports', { websiteId, type }),
    enabled: !!websiteId && !!type,
    ...options,
  });
}

When you call touch('reports:goal') from the mutation, useModified updates this modified timestamp. 

queryKey: ['reports', { websiteId, type, modified }]

The modified timestamp is part of the cache key. When it changes, React Query sees it as a new query and automatically refetches.

We call the touch in GoalEditForm:

// In GoalEditForm after successful mutation:
touch('reports:goal');

The Complete Picture

Let’s trace the entire flow from Part 1.3 to now:

Creating a Goal:

  1. User submits form in GoalEditForm

  2. Mutation succeeds

  3. Calls touch('reports:goal')

  4. useModified('reports:goal') timestamp updates

  5. useReportsQuery cache key changes

  6. Query refetches automatically

  7. GoalsPage shows new goal

Listing Goals:

  1. GoalsPage calls useReportsQuery({ websiteId, type: 'goal' })

  2. Hook checks useModified('reports:goal') for timestamp

  3. Includes timestamp in query key

  4. Fetches data with get('/reports', { websiteId, type })

  5. Returns { data, isLoading, error }

The modified timestamp is the bridge between mutations and queries.

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/blob/master/src/app/(main)/websites/%5BwebsiteId%5D/(reports)/goals/GoalsPage.tsx

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

  3. https://github.com/umami-software/umami/blob/master/src/components/hooks/queries/useReportsQuery.ts#L6

We use cookies
We use cookies to ensure you get the best experience on our website. For more information on how we use cookies, please see our cookie policy.

By clicking "Accept", you agree to our use of cookies.

Learn more