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:
Create a new React hook using your React hook scaffold:
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; }
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.
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); } }
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> ); };
Run the dev server to see this composition rendered in your workspace UI:
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) ); });
Run the following to run your tests:
Create a new component:
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> ); }