3

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 2

3

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];
};
Sign up to request clarification or add additional context in comments.

Comments

2

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

2 Comments

If I change the initialUrl to url since I do not initialUrl anymore, I don't need to use url param in fetchData. I need to set the missing dependency url in useCallback too
For the second code snippet i added, you don't need to add url to the callback, its not a dependency in that one because the url is coming from the function's argument.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.