Restful Data Fetching

We recommend implementing the fetch logic and state management, as an independent component, separated from any specific UI component. This will make this component, as well as its future dependent UI components, more reusable and maintainable.

Run the following to generate a workspace with the components in this tutorial:

$bit
Copiedcopy

Create a new React hook using your React hook scaffold:

$bit
Copiedcopy

Implement your custom hook:

import { useEffect, useState } from 'react';
/* this component will be created in the next step */
import { Brewery } from '@learnbit-react/data-fetching.entities.brewery';

export function useBreweries(): Brewery[] | undefined {
  const [breweries, setBreweries] = useState<Brewery[]>();

  useEffect(() => {
    const getBreweries = async () => {
      const response = await fetch(
        'https://api.openbrewerydb.org/breweries?by_city=san_francisco'
      );
      const plainBreweries = await response.json();
      if (!plainBreweries?.length) return;
      setBreweries(
        plainBreweries.map((plainBrewery: any) =>
          Brewery.fromObject(plainBrewery)
        )
      );
    };
    getBreweries();
  }, []);

  return breweries;
}
CopiedCopy

See example component.

Create an entity component

Create an entity component to define the structure of data being fetched, and provide the methods required for the manipulation of that data. This component serves a a source-of-truth for any component handling that type of data, client-side and server-side.

$bit
Copiedcopy

Implement the entity class:

export type BreweryType = {
  name: string;
  [x: string]: any;
};

export class Brewery {
  constructor(readonly name: BreweryType['name']) {}

  getNameSize(): number {
    return this.name.length;
  }

  static fromJson(jsonStr: string) {
    const object = JSON.parse(jsonStr);
    return new Brewery(object.name);
  }

  static fromObject(plainBrewery: BreweryType) {
    return new Brewery(plainBrewery.name);
  }
}
CopiedCopy

See example component.

Testing

Head over to your use-breweries.compositions.tsx file, to create a composition that will help you test your hook manually (and later on, programmatically).

import { useBreweries } from './use-breweries';

export const BasicUseBreweries = () => {
  const breweries = useBreweries();
  if (!breweries) return <div>no breweries found</div>;
  return (
    <ul>
      {breweries.map((brewery, index) => (
        <li key={index}>{brewery.name}</li>
      ))}
    </ul>
  );
};
CopiedCopy

Run the dev server to see this composition rendered in your workspace UI:

$bit
Copiedcopy

Head over to your use-breweries.spec.tsx file to create a programmatic test. Use the previously created composition to reduce the necessary setup needed for your test.

import { render, waitFor } from '@testing-library/react';
import { BasicUseBreweries } from './use-breweries.compositions';

it('fetches and renders data', async () => {
  const { getAllByRole } = render(<BasicUseBreweries />);
  await waitFor(() =>
    expect(getAllByRole('listitem', { hidden: true }).length).toBeGreaterThan(0)
  );
});
CopiedCopy

Run the following to run your tests:

$bit
Copiedcopy

Use the custom hook as a dependency

Create a new component:

$bit
Copiedcopy

Implement the component. Use the previously created hook to implement data fetching and state management:

import { useBreweries } from '@learnbit-react/data-fetching.ui.hooks.use-breweries';

export function BreweryList() {
  const breweries = useBreweries();

  if (!breweries) return <div>no breweries found</div>;

  return (
    <ul>
      {breweries.map((brewery, index) => (
        <li key={index}>
          {brewery.name} | the name's length is {brewery.getNameSize()}
        </li>
      ))}
    </ul>
  );
}
CopiedCopy

See example component.