React.js

Data Fetching Patterns 🔀

Make Requests and handle Errors

Example usage:

function App() {
  const [query, setQuery] = React.useState('')

  function handleSubmit(query) {
    setQuery(query)
  }

  return (
    <div >
      <Form query={query} onSubmit={handleSubmit} />
      <Info query={query} />
    </div>
  )
}

For more advanced versions, see also the extra credit:

  1. 💯 store the state in an object
  2. 💯 create an ErrorBoundary component
  3. 💯 re-mount the error boundary

from: 6. useEffect: HTTP requests | React Hooks 🎣

See also:

  1. Make HTTP Requests with React | egghead.io
  2. Handle HTTP Errors with React | egghead.io
function Info({query}) {
  const [status, setStatus] = React.useState('idle')
  const [data, setData] = React.useState(null)
  const [error, setError] = React.useState(null)

  React.useEffect(() => {
    if (!query) {
      return
    }
    setStatus('pending')
    fetch('https://example.com/?q='+query)
    .then(response => response.json())
    .then(
      data => {
        setData(data)
        setStatus('resolved')
      },
      error => {
        setError(error)
        setStatus('rejected')
      },
    )
  }, [query])

  if (status === 'idle') {
    return 'Submit a something'
  } else if (status === 'pending') {
    return <InfoFallback query={query} />
  } else if (status === 'rejected') {
    return (
      <div>
        There was an error:{' '}
        <pre style={{whiteSpace: 'normal'}}>{error.message}</pre>
      </div>
    )
  } else if (status === 'resolved') {
    return <DataView data={data} />
  }

  throw new Error('This should be impossible')
}

Alternative Make Request and handle Errors

This pattern does not only manage the state of the promise for fetching data, but also the fetching itself.

That makes it easier to use, but also less flexible.

Another difference is that this pattern only dispatches the most recent fetched data, while the other one dispatches all, e.g. an outdated fetch can overwrite a newer one.

A solution that fixes this in the other pattern with additional upgrades (e.g. using useReducer, useSafeDispatch, ...) can be found in the utililty: useAync(initialState)

It uses useReducer similiar to the exercise #2 of Advanced React Hooks

Example usage:

function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    'https://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );
 
  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `https://hn.algolia.com/api/v1/search?query=${query}`,
          );
 
          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
 
      {isError && <div>Something went wrong ...</div>}
 
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

from How to fetch data with React Hooks? - RWieruch with slide modifications:
My version replaced axios with native fetch and also assumes that the return data is in json format (response => response.json()).

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
 
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
 
  useEffect(() => {
    let didCancel = false;
 
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
 
      try {
        const result = await fetch(url).then(response => response.json());
 
        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };
 
    fetchData();
 
    return () => {
      didCancel = true;
    };
  }, [url]);
 
  return [state, setUrl];
};

Render as you fetch

A pattern to be used with <React.Suspense/>

The createResource utility is intended to be used with this pattern.

Example usage:

function App() {
  const [pokemonResource, setPokemonResource] = React.useState(null)
  // ...
  setPokemonResource(
    createResource(fetch(pokemonName).then((response) => response.json()))
  );

  // ...
  return (
    <React.Suspense
      fallback={<PokemonInfoFallback name={pokemonName} />}
    >
      <PokemonInfo pokemonResource={pokemonResource} />
    </React.Suspense>
  )
}

from: 2. Render as you fetch | React Suspense 🔀

See also:

import {createResource} from './utils'

function PokemonInfo({pokemonResource}) {
  const pokemon = pokemonResource.read()
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}