react hook useEffect to fetch data on button click (typescript)

Viewed 1608

I have a component and I want to fetch isbn data on button click using react hook useEffect, performing a get on the route ${basicUrl}/editorials/${isbn}, so i wrote this component:

import React, { Fragment, useState } from "react";
import "./Home.css";
import { V3_BASIC_URL } from "../../constants/endpoints";
import { useDataApi } from "../../store/effects/dataEffects";

import SearchIsbnElement from "../../components/SearchIsbnElement/SearchIsbnElement";
import IsbnPanelElement from "../../components/IsbnPanelElement/IsbnPanelElement";

function Home() {
  const [query, setQuery] = useState<string>("9788808677853");
  const [isValid, setIsValid] = useState<boolean>(true);

  const url = `${V3_BASIC_URL(
    process.env.REACT_APP_API_ENV
  )}/editorials/${query}`;
  const [{ isbn, isLoading, isError }, doFetch] = useDataApi(url, {
    isLoading: false,
    isError: false,
    isbn: undefined,
  });

  const buttonCallback = () => {
    doFetch(url);
  };
  const isbnRegexp = /^97\d{11}$/
  const validateQuery = (query: string): boolean => isbnRegexp.test(query)

  const inputCallback = (query: string) => {
    setQuery(query)
    setIsValid(validateQuery(query));
  };

  return (
    <div id="isbn-panel-home" className="Home">
      <SearchIsbnElement
        inputCallback={inputCallback}
        buttonCallback={buttonCallback}
        query={query}
        isValid={isValid}
      ></SearchIsbnElement>
      {isError && <div>Il servizio al momento non è disponibile, riprova più tardi</div>}
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        !isError && 
        <Fragment>
          <IsbnPanelElement isbn={isbn}></IsbnPanelElement> 
          <p>{isbn?.scheda_volume == null && 'isbn non trovato'}</p>
        </Fragment>
      )}
    </div>
  );
}

export default Home;

the useDataApi function uses the hook useEffect and returns state and setUrl action to set the new url on isbn value change. This is the useDataApi file:

import { useState, useEffect, useReducer } from "react";

import {
  dataFetchFailure,
  dataFetchInit,
  dataFetchSuccess,
} from "../actions/dataActions";
import { dataFetchReducer, ISBNState } from "../reducers/dataReducers";
import { get } from "../../tools/request";

type InitialState = {
  isLoading: boolean,
  isError: boolean,
  isbn: undefined,
}

export const useDataApi = (initialUrl: string, initialData: InitialState) : [ISBNState, (value: string) => void]  => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, initialData);

  useEffect(() => {
    let didCancel: boolean = false;

    const fetchData = async (): Promise<any> => {
      dispatch(dataFetchInit());
      const options = {
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json'
        },
        auth: {
          username: `${process.env.REACT_APP_API_AUTH_USER}`,
          password: `${process.env.REACT_APP_API_AUTH_PWD}`
        }
      }
      try {
        const {data} = await get(url, options);
        if (!didCancel) {
          dispatch(dataFetchSuccess(data));
        }
      } catch (error) {
        if (!didCancel) {
          dispatch(dataFetchFailure(error));
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  return [state, setUrl];
};

with this code fetching starts on page load, but i want to fetch data only on button click. How can I do this?

2 Answers

useEffect() is a hook to manipulate the component through the different lifecycle methods. In order to do something onClick you need to create a method for that:

const fetchData = async (): Promise<any> => {
  dispatch(dataFetchInit());
  const options = {
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json'
    },
    auth: {
      username: `${process.env.REACT_APP_API_AUTH_USER}`,
      password: `${process.env.REACT_APP_API_AUTH_PWD}`
    }
  }
  try {
    const {data} = await get(url, options);
    if (!didCancel) {
      dispatch(dataFetchSuccess(data));
    }
  } catch (error) {
    if (!didCancel) {
      dispatch(dataFetchFailure(error));
    }
  }
};

Just do that and you will be fine

Edit: the new version of useDataApi

export const useDataApi = (
  url: string,
  initialData: InitialState
): [ISBNState, (value: string) => void] => {
  const [state, dispatch] = useReducer(dataFetchReducer, initialData);

  const fetchData = useCallback(async (): Promise<any> => {
    dispatch(dataFetchInit());
    const options = {
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      auth: {
        username: `${process.env.REACT_APP_API_AUTH_USER}`,
        password: `${process.env.REACT_APP_API_AUTH_PWD}`,
      },
    };
    try {
      const { data } = await get(url, options);
      dispatch(dataFetchSuccess(data));
    } catch (error) {
      dispatch(dataFetchFailure(error));
    }
  }, [url]);

  return [state, fetchData];
};

The useDataApi hook returns [,doFetch], but the doFetch is actually setUrl so if you wanted that to work as expected you can let the initial value for the url be null or falsey and only allow a fetch inside the effect when the url is valid/truthy. When you click the button, thats when you setUrl and that's when the effect will allow a fetchData to occur because by then the value of url will be set.

export const useDataApi = (initialUrl: string, initialData: InitialState): [ISBNState, (value: string) => void] => {
  // make this default to null here, or where you intende to use this hook 
  const [url, setUrl] = useState(null);

  // custom hook body

  useEffect(() => {
    // effect body
    if (url) {
      fetchData();
    }
    // hook cleanup
  }, [url]);

  return [state, setUrl];
};

Although, the better solution is directly calling the function fetchData on the button click. One way you can do that is by modifying your useDataApi hook to return 'fetchData' directly allowing it to accept the url as an argument and removing the need for the const [url,setUrl] = useState(initialUrl) entirely

export const useDataApi = (initialUrl: string, initialData: InitialState): [ISBNState, (value: string) => void] => {
  const [state, dispatch] = useReducer(dataFetchReducer, initialData);

  const fetchData = useCallback(async (url): Promise<any> => {
    dispatch(dataFetchInit());
    const options = {
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      auth: {
        username: `${process.env.REACT_APP_API_AUTH_USER}`,
        password: `${process.env.REACT_APP_API_AUTH_PWD}`,
      },
    };
    try {
      const { data } = await get(url, options);
      if (!didCancel) {
        dispatch(dataFetchSuccess(data));
      }
    } catch (error) {
      if (!didCancel) {
        dispatch(dataFetchFailure(error));
      }
    }
  }, []);

  return [state, fetchData];
};

You can also drop initialUrl from the hook useDataApi

Related