DEV Community

Ramu Narasinga
Ramu Narasinga

Posted on • Originally published at thinkthroo.com

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 
  });
...
Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

Cache strategy

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

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

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 };
}
Enter fullscreen mode Exit fullscreen mode
  • 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

Top comments (0)