Blog
State management in Umami codebase - Part 1.1

State management in Umami codebase - Part 1.1

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.

Prerequisite

  1. State management in Umami codebase — Part 1.0

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. We repeat this process for 3+ pages to establish a common pattern, see if there’s any exceptions.

In this part 1.1, we review the websites route and see what library is used to manage state, here it is the list of websites. We will find out what libraries Umami uses, how the files are structured, how the data flows to manage its state.

I reviewed the /websites route and found that the following files give us a clear picture about state management.

  1. WebsitesDataTable.tsx

  2. WebsitesTable.tsx

We will first review the code and then the underlying pattern. When you visit /websites on Umami, you see list of websites.

Our goal is to find out how this list of websites is stored. Is there a component state or application state or server cache state? let’s see.

WebsitesDataTable.tsx

You will find the following code in WebsitesDataTable.tsx 

...
export function WebsitesDataTable({
  userId,
  teamId,
  allowEdit = true,
  allowView = true,
  showActions = true,
}: {
  userId?: string;
  teamId?: string;
  allowEdit?: boolean;
  allowView?: boolean;
  showActions?: boolean;
}) {
  const { user } = useLoginQuery();
  const queryResult = useUserWebsitesQuery({ 
    userId: userId || user?.id, teamId 
  });
...

Here the queryResults is assigned the data fetched from the server. More info about useUserWebsitesQuery can be found in the API layer series.

And this queryResult is fed to the DataGrid and WebsiteTable components as shown below:

...
return (
    <DataGrid query={queryResult} allowSearch allowPaging>
      {({ data }) => (
        <WebsitesTable
          data={data}
          showActions={showActions}
          allowEdit={allowEdit}
          allowView={allowView}
          renderLink={renderLink}
        />
      )}
    </DataGrid>
...

WebsitesTable

You will find the following code in WebsitesTable.

import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen';
import type { ReactNode } from 'react';
import { LinkButton } from '@/components/common/LinkButton';
import { useMessages, useNavigation } from '@/components/hooks';
import { SquarePen } from '@/components/icons';

export interface WebsitesTableProps extends DataTableProps {
  showActions?: boolean;
  allowEdit?: boolean;
  allowView?: boolean;
  renderLink?: (row: any) => ReactNode;
}

export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTableProps) {
  const { formatMessage, labels } = useMessages();
  const { renderUrl } = useNavigation();

  return (
    <DataTable {...props}>
      <DataColumn id="name" label={formatMessage(labels.name)}>
        {renderLink}
      </DataColumn>
      <DataColumn id="domain" label={formatMessage(labels.domain)} />
      {showActions && (
        <DataColumn id="action" label=" " align="end">
          {(row: any) => {
            const websiteId = row.id;

            return (
              <LinkButton href={renderUrl(`/websites/${websiteId}/settings`)} variant="quiet">
                <Icon>
                  <SquarePen />
                </Icon>
              </LinkButton>
            );
          }}
        </DataColumn>
      )}
    </DataTable>
  );
}

This just renders the data passed via props.

Strategy used

We did not see any references to useState or libraries like Zustand, instead we saw useUserWebsitesQuery.ts

and this useUserWebsitesQuery.ts 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 useUserWebsitesQuery(
  { userId, teamId }: { userId?: string; teamId?: string },
  params?: Record<string, any>,
  options?: ReactQueryOptions,
) {
  const { get } = useApi();
  const { modified } = useModified(`websites`);

  return usePagedQuery({
    queryKey: ['websites', { userId, teamId, modified, ...params }],
    queryFn: pageParams => {
      return get(
        teamId
          ? `/teams/${teamId}/websites`
          : userId
            ? `/users/${userId}/websites`
            : '/me/websites',
        {
          ...pageParams,
          ...params,
        },
      );
    },
    ...options,
  });
}

This returns usePagedQuery and this is defined as shown below

import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import type { PageResult } from '@/lib/types';
import { useApi } from './useApi';
import { useNavigation } from './useNavigation';

export function usePagedQuery<TData = any, TError = Error>({
  queryKey,
  queryFn,
  ...options
}: Omit<
  UseQueryOptions<PageResult<TData>, TError, PageResult<TData>, readonly unknown[]>,
  'queryFn' | 'queryKey'
> & {
  queryKey: readonly unknown[];
  queryFn: (params?: object) => Promise<PageResult<TData>> | PageResult<TData>;
}): UseQueryResult<PageResult<TData>, TError> {
  const {
    query: { page, search },
  } = useNavigation();
  const { useQuery } = useApi();

  return useQuery<PageResult<TData>, TError>({
    queryKey: [...queryKey, page, search] as const,
    queryFn: () => queryFn({ page, search }),
    ...options,
  });
}

Cache strategy

We finally see a reference to Tanstack React Query as the import shown below:

import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';

Learn more about useQuery

There are no additional options passed and this is using React Query’s default client-side cache.

Default React Query Behavior:

  1. staleTime: 0ms (data immediately considered stale)

  2. cacheTime/gcTime: 5 minutes (cached data kept for 5 minutes)

  3. refetchOnWindowFocus: true

  4. refetchOnMount: true if data is stale

Cache Invalidation Strategy:

Notice the useModified hook in useUserWebsitesQuery:

The modified timestamp is included in the queryKey. When websites data changes, modified updates, creating a new cache key, effectively invalidating the old cache.

useModified

useModified is defined as shown below:

import { create } from 'zustand';

const store = create(() => ({}));

export function touch(key: string) {
  store.setState({ [key]: Date.now() });
}

export function useModified(key?: string) {
  const modified = store(state => state?.[key]);

  return { modified, touch };
}
  • When touch('websites') is called, it updates the Zustand state

  • The useModified('websites') hook subscribes to that state

  • When the state changes, modified gets a new timestamp

  • React Query sees the queryKey has changed: ['websites', { ..., modified: OLD_TIME }]['websites', { ..., modified: NEW_TIME }]

  • React Query treats this as a completely different query and refetches

This way you dont need to import queryClient.invalidateQueries() everywhere.

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/WebsitesDataTable.tsx

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

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

  4. https://github.com/umami-software/umami/blob/master/src/components/hooks/usePagedQuery.ts#L6

  5. https://tanstack.com/query/latest/docs/framework/react/reference/useQuery

  6. https://github.com/umami-software/umami/blob/master/src/app/(main)/websites/%5BwebsiteId%5D/settings/WebsiteData.tsx#L40

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