Skip to main content

useDLE() - [D]ata [L]oading [E]rror

High performance async data rendering without overfetching. With fetch meta data.

In case you cannot use suspense, useDLE() is just like useSuspense() but returns [D]ata [L]oading [E]rror values.

useDLE() is reactive to data mutations; rerendering only when necessary.

Usage

import { Entity, createResource } from '@data-client/rest';

export class Profile extends Entity {
  id: number | undefined = undefined;
  avatar = '';
  fullName = '';
  bio = '';

  pk() {
    return this.id?.toString();
  }
  static key = 'Profile';
}

export const ProfileResource = createResource({
  path: '/profiles/:id',
  schema: Profile,
});
import { useDLE } from '@data-client/react';
import { ProfileResource } from './ProfileResource';

function ProfileList(): JSX.Element {
  const { data, loading, error } = useDLE(ProfileResource.getList);
  if (error) return <div>Error {`${error.status}`}</div>;
  if (loading || !data) return <>loading...</>;
  return (
    <div>
      {data.map(profile => (
        <div className="listItem" key={profile.pk()}>
          <Avatar src={profile.avatar} />
          <div>
            <h4>{profile.fullName}</h4>
            <p>{profile.bio}</p>
          </div>
        </div>
      ))}
    </div>
  );
}
render(<ProfileList />);
🔴 Live Preview
Store

Behavior

Expiry StatusFetchDataLoadingErrorConditions
Invalidyes1undefinedtruefalsenot in store, deletion, invalidation, invalidIfStale
Staleyes1denormalizedfalsefalse(first-render, arg change) & expiry < now
Validnodenormalizedfalsemaybe2fetch completion
noundefinedfalsefalsenull used as second argument
note
  1. Identical fetches are automatically deduplicated
  2. Hard errors to be caught by Error Boundaries
React Native

When using React Navigation, useDLE() will trigger fetches on focus if the data is considered stale.

Conditional Dependencies

Use null as the second argument to any RDC hook means "do nothing."

// todo could be undefined if id is undefined
const todo = useDLE(TodoResource.get, id ? { id } : null);

Types

function useDLE(
endpoint: ReadEndpoint,
...args: Parameters<typeof endpoint> | [null]
): {
data: Denormalize<typeof endpoint.schema>;
loading: boolean;
error: Error | undefined;
};

Examples

Detail

import { Entity, createResource } from '@data-client/rest';

export class Profile extends Entity {
  id: number | undefined = undefined;
  avatar = '';
  fullName = '';
  bio = '';

  pk() {
    return this.id?.toString();
  }
  static key = 'Profile';
}

export const ProfileResource = createResource({
  path: '/profiles/:id',
  schema: Profile,
});
import { useDLE } from '@data-client/react';
import { ProfileResource } from './ProfileResource';

function ProfileDetail(): JSX.Element {
  const {
    data: profile,
    loading,
    error,
  } = useDLE(ProfileResource.get, { id: 1 });
  if (error) return <div>Error {`${error.status}`}</div>;
  if (loading || !profile) return <>loading...</>;
  return (
    <div className="listItem">
      <Avatar src={profile.avatar} />
      <div>
        <h4>{profile.fullName}</h4>
        <p>{profile.bio}</p>
      </div>
    </div>
  );
}
render(<ProfileDetail />);
🔴 Live Preview
Store

Conditional

null will avoid binding and fetching data

Resources
import { Entity, createResource } from '@data-client/rest';

export class Post extends Entity {
  id = 0;
  userId = 0;
  title = '';
  body = '';

  pk() {
    return this.id?.toString();
  }
  static key = 'Post';
}
export const PostResource = createResource({
  path: '/posts/:id',
  schema: Post,
});

export class User extends Entity {
  id = 0;
  name = '';
  username = '';
  email = '';
  phone = '';
  website = '';

  get profileImage() {
    return `https://i.pravatar.cc/64?img=${this.id + 4}`;
  }

  pk() {
    return `${this.id}`;
  }
  static key = 'User';
}
export const UserResource = createResource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/users/:id',
  schema: User,
});
PostWithAuthor
import { PostResource, UserResource } from './Resources';

export default function PostWithAuthor({ id }: { id: string }) {
  const postDLE = useDLE(PostResource.get, { id });
  if (postDLE.error) return <div>Error {`${postDLE.error.status}`}</div>;
  if (postDLE.loading || !postDLE.data) return <>loading...</>;
  const authorDLE = useDLE(
    UserResource.get,
    postDLE.data.userId
      ? {
          id: postDLE.data.userId,
        }
      : null,
  );
  if (authorDLE.error) return <div>Error {`${authorDLE.error.status}`}</div>;
  if (authorDLE.loading || !authorDLE.data) return <>loading...</>;

  return <div>{authorDLE.data.username}</div>
}

Embedded data

When entities are stored in nested structures, that structure will remain.

api/Post
export class PaginatedPost extends Entity {
  id = '';
  title = '';
  content = '';

  pk() {
    return this.id;
  }
  static key = 'PaginatedPost';
}

export const getPosts = new RestEndpoint({
  path: '/post',
  searchParams: { page: '' },
  schema: {
    results: new schema.Collection([PaginatedPost]),
    nextPage: '',
    lastPage: '',
  },
});
ArticleList
import { useDLE } from '@data-client/react';
import { getPosts } from './api/Post';

export default function ArticleList({ page }: { page: string }) {
  const { data, loading, error } = useDLE(getPosts, { page });
  if (error) return <div>Error {`${error.status}`}</div>;
  if (loading || !data) return <>loading...</>;
  const { results: posts, nextPage, lastPage } = data;
  return (
    <div>
      {posts.map(post => (
        <div key={post.pk()}>{post.title}</div>
      ))}
    </div>
  );
}

Github Reactions

useDLE() allows us to declaratively fetch reactions on any issue page the moment we navigate to it. This allows us to not block the issues page from showing if the reactions are not completed loading.

It's usually better to wrap cases like this in new Suspense Boundaries. However, our component library ant design does not allow this.

More Demos