usehooks-ts icon indicating copy to clipboard operation
usehooks-ts copied to clipboard

[Suggestion] useFetch, delayed for non-GET

Open nightness opened this issue 2 years ago • 0 comments

The code below is a bit customized for my needs, but it should be easy to follow... The point of this change to for non-get methods to have a delayed fetch call... so I implemented a setBody state that does the setting of the body and the fetch after the hook is instantiated. I'd PR but don't have the time to.

Usage (get):

  const { data, error } = useFetch('/spots', 'GET', true);

  useEffect(() => {
    console.log('data', data);
    console.log('error', error);
  }, [data, error]);

Usage (post):

  const { data, error, setBody } = useFetch('/features', 'POST', true);

  useEffect(() => {
    console.log('data', data);
    console.log('error', JSON.stringify(error));
  }, [data, error]);

...
   // Do the fetch, in a callback
   setBody({
      name: 'Testing',
    });

Code (JS in my case):

/*
    Based on https://usehooks-ts.com/react-hook/use-fetch
*/
/* eslint-disable import/no-cycle */
import Constants from 'expo-constants';
import { useContext, useEffect, useReducer, useRef, useState } from 'react';

import { FirebaseContext } from '../firebase';

const SERVER_URL = Constants.manifest?.extra?.SERVER_URL;

// interface State<T> {
//   data?: T
//   error?: Error
// }

// type Cache<T> = { [url: string]: T }

// discriminated union type
// type Action<T> =
//   | { type: 'loading' }
//   | { type: 'fetched'; payload: T }
//   | { type: 'error'; payload: Error }

export function useFetch(relativeUrl, method, authenticate = false) {
  const cache = useRef({});
  const { authToken } = useContext(FirebaseContext);
  const [body, setBody] = useState();

  const url = `${SERVER_URL}${relativeUrl}`;

  // Used to prevent state update if the component is unmounted
  const cancelRequest = useRef(false);

  const initialState = {
    error: undefined,
    data: undefined,
    setBody: (body) => {
      console.log('useFetch:setBody', body);
      setBody(body);
    },
  };

  // Keep state logic separated
  const fetchReducer = (state, action) => {
    switch (action.type) {
      case 'loading':
        return { ...initialState };
      case 'fetched':
        return { ...initialState, data: action.payload };
      case 'error':
        return { ...initialState, error: action.payload };
      default:
        return state;
    }
  };

  const [state, dispatch] = useReducer(fetchReducer, initialState);

  useEffect(() => {
    dispatch({ type: 'loading' });
  }, []);

  useEffect(() => {
    // Do nothing if the url is not given
    if (!url || state.data || state.error) return;

    cancelRequest.current = false;

    const fetchData = async () => {
      if (!url || state.data || state.error) return;

      dispatch({ type: 'loading' });

      // If a cache exists for this url, return it
      if (cache.current[url]) {
        dispatch({ type: 'fetched', payload: cache.current[url] });
        return;
      }

      try {
        const options = {
          method,
          headers: authenticate
            ? {
                Authorization: `Bearer ${authToken}`,
                'Content-Type': 'application/json',
              }
            : {
                'Content-Type': 'application/json',
              },
          body: body ? JSON.stringify(body) : undefined,
        };
        console.log('useFetch:options', options);
        const response = await fetch(url, options);
        if (!response.ok) {
          response
            .json()
            .then((error) => {
              console.log('Error:', error);
              throw new Error(error);
            })
            .catch((error) => {
              console.log('Error:', error);
              throw new Error(error);
            });
          throw new Error(response.statusText);
        }

        const data = await response.json();
        cache.current[url] = data;
        if (cancelRequest.current) return;

        dispatch({ type: 'fetched', payload: data });
      } catch (error) {
        if (cancelRequest.current) return;

        dispatch({ type: 'error', payload: error });
      }
    };

    if (method === 'GET' || body !== undefined) {
      console.log('fetchData...');
      fetchData();
    }

    // Use the cleanup function for avoiding a possibly...
    // ...state update after the component was unmounted
    return () => {
      cancelRequest.current = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [url, body]);

  return state;
}

nightness avatar Aug 17 '22 23:08 nightness