DEV Community

Ramu Narasinga
Ramu Narasinga

Posted on

Error handling 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 error handling strategies used in Umami codebase.

Prerequisite

  1. Error handling in Umami codebase — Part 1.0

Approach

In part 1.0, I mentioned that Bulletproof React mentions three ways of error handling:

  1. API errors

  2. In app errors

  3. Error tracking

In this article, I want to provide examples and references to how Umami handled in-app errors. The approach we take is simple:

  1. Pick common/DataGrid.tsx component that handles the error

  2. Review how the error is handled

You see Umami uses Tanstack Query. So the way in app errors are handled is that it involves destructing the query result and then use the error variable.

  const { data, error, isLoading, isFetching } = query;
Enter fullscreen mode Exit fullscreen mode

In this part 1.0, we review the common/DataGrid.tsx.

DataGrid.tsx

This common/DataGrid.tsx component is used in quite some pages. Following are some examples showing how this DataGrid component is reused

Websites

In WebsitesDataTable.tsx, you will find the following code:

import Link from 'next/link';
import { DataGrid } from '@/components/common/DataGrid';
import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';
import { Favicon } from '@/index';
import { Icon, Row } from '@umami/react-zen';
import { WebsitesTable } from './WebsitesTable';

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 });
  const { renderUrl } = useNavigation();

  const renderLink = (row: any) => (
    <Row alignItems="center" gap="3">
      <Icon size="md" color="muted">
        <Favicon domain={row.domain} />
      </Icon>
      <Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
    </Row>
  );

  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

Here queryResult is entirely passed down as a prop to DataGrid.

Links

Similarly in the LinksDataTable.tsx, you will find the following code:

import { DataGrid } from '@/components/common/DataGrid';
import { useLinksQuery, useNavigation } from '@/components/hooks';
import { LinksTable } from './LinksTable';

export function LinksDataTable() {
  const { teamId } = useNavigation();
  const query = useLinksQuery({ teamId });

  return (
    <DataGrid 
      query={query} 
      allowSearch={true} 
      autoFocus={false} 
      allowPaging={true}
    >
      {({ data }) => <LinksTable data={data} />}
    </DataGrid>
  );
}
Enter fullscreen mode Exit fullscreen mode

Do you see the pattern here? query results is passed down as a prop entirely to the DataGrid component.

DataGrid

Let’s take a look at this DataGrid component. It has about 107 LOC at the time of writing this article. 

Query result is destructured as shown below:

export function DataGrid({
  query,
  searchDelay = 600,
  allowSearch,
  allowPaging = true,
  autoFocus,
  renderActions,
  renderEmpty = () => <Empty />,
  children,
}: DataGridProps) {
  const { formatMessage, labels } = useMessages();
  const { data, error, isLoading, isFetching } = query;
Enter fullscreen mode Exit fullscreen mode

Then these variables are passed down as props to the component, LoadingPanel.

LoadingPanel

<LoadingPanel
  data={data}
  isLoading={isLoading}
  isFetching={isFetching}
  error={error}
  renderEmpty={renderEmpty}
>
  {data && (
    <>
      <Column>
        {isValidElement(child)
          ? cloneElement(child as ReactElement<any>, { displayMode })
          : child}
      </Column>
      {showPager && (
        <Row marginTop="6">
          <Pager
            page={data.page}
            pageSize={data.pageSize}
            count={data.count}
            onPageChange={handlePageChange}
          />
        </Row>
      )}
    </>
  )}
</LoadingPanel>
Enter fullscreen mode Exit fullscreen mode

In the LoadingPanel, you will find the following code:

// Show loading spinner only if no data exists
if (isLoading || isFetching) {
  return (
    <Column position="relative" height="100%" width="100%" {...props}>
      <Loading icon={loadingIcon} placement={loadingPlacement} />
    </Column>
  );
}

// Show error
if (error) {
  return <ErrorMessage />;
}
Enter fullscreen mode Exit fullscreen mode

There is other code snippets but I am more interested in learning how the In-App errors are handled. So if there is an error, ErrorMessage component is rendered and this is based on the destructured error value from the query result.

ErrorMessage

You will find the following code in ErrorMessage.tsx.

import { Icon, Row, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { AlertTriangle } from '@/components/icons';

export function ErrorMessage() {
  const { formatMessage, messages } = useMessages();

  return (
    <Row alignItems="center" justifyContent="center" gap>
      <Icon>
        <AlertTriangle />
      </Icon>
      <Text>{formatMessage(messages.error)}</Text>
    </Row>
  );
}
Enter fullscreen mode Exit fullscreen mode

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. umami/src/components/common/ErrorMessage.tsx#L5

  2. https://tanstack.com/query/latest

  3. umami/blob/master/src/components/common/DataGrid.tsx#L41

  4. umami/src/app/(main)/websites/WebsitesDataTable.tsx#L35

  5. umami/src/app/(main)/links/LinksDataTable.tsx

  6. umami/components/common/LoadingPanel.tsx#L43

Top comments (0)