user2176499
user2176499

Reputation: 393

Calling custom hook in event handler

I have a custom hook named useFetchMyApi wrapping a fetch call to API endpoint. The function hook accepts a parameter and it is included in the post body. The data array output is dependent on the hook parameter.

On the UI, the App component calls useFetchMyApi once. A button click handler will call subsequent useFetchMyApi to update a list on the UI.

My issues:

  1. I can't call useFetchMyApi in the event handler because it violates the rules of hook.
  2. How do I refresh the list of items dependent on the response of useFetchMyApi?
import React, { useState, useEffect, useRef } from "react";
import "./styles.css";

const useFetchMyApi = (location = "") => {
  const [items, setItems] = useState([]);
  useEffect(() => {
    // let's stimulate the API call with a 2 seconds response time
    setTimeout(() => setItems([location, Math.random()]), 5000);
  }, [location]);
  return { items };
};

export default function App() {
  const locationSelect = useRef(null);
  const { items } = useFetchMyApi("mylocation1");
  const onButtonClicked = (event) => {
    const selectedLocation = locationSelect.current.value;
    // Call api and refresh items by location
    // Fix here??
    //useFetchMyApi(selectedLocation);
  };

  return (
    <div className="App">
      <select ref={locationSelect}>
        <option value="location1">Location 1</option>
        <option value="location2">Location 2</option>
      </select>
      <button onClick={onButtonClicked}>Refresh</button>
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  );
}

https://codesandbox.io/s/stupefied-water-1o6mz?file=/src/App.js:0-1055

Upvotes: 11

Views: 11757

Answers (1)

Drew Reese
Drew Reese

Reputation: 202618

Issues

  1. I can't call useFetchMyApi in the event handler because it violates the rules of hooks.

Correct, react hooks can only be called from the body of functional components and other react hooks. They can not be conditionally invoked, such as in an asynchronous callback, or in loops.

  1. How do I refresh the list of items dependent on the response of useFetchMyApi?

You've nothing in App to trigger a rerender to trigger the effect in order to trigger the effect's callback.

Solution

You should leverage the useEffect hook's dependency in your custom useFetchMyApi hook to refresh the list of items. Add some local component state to represent the "currently" selected location. Use your button's onClick callback to update the state and trigger a rerender which then will rerun your custom hook.

const useFetchMyApi = (location = "") => {
  const [items, setItems] = useState([]);
  useEffect(() => {
    // let's stimulate the API call with a 2 seconds response time
    setTimeout(() => setItems([location, Math.random()]), 2000);
  }, [location]); // <-- effect callback triggers when location updates
  return { items };
};

function App() {
  const locationSelect = useRef(null);
  const [location, setLocation] = useState("mylocation1"); // <-- add state
  const { items } = useFetchMyApi(location); // <-- pass location value to hook

  const onButtonClicked = () => {
    const selectedLocation = locationSelect.current.value;
    setLocation(selectedLocation); // <-- update state, trigger rerender
  };

  return (
    <div className="App">
      <select ref={locationSelect}>
        <option value="location1">Location 1</option>
        <option value="location2">Location 2</option>
      </select>
      <button onClick={onButtonClicked}>Refresh</button>
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  );
}

Edit calling-custom-hook-in-event-handler

Alternative Solution

An alternative solution would be to rewrite your custom useFetchMyApi hook to also return a callback to be invoked on-demand in a click handler to do the fetch and update the list. Here is a simple conversion. Notice now the returned fetch function consumes the location, not the hook call.

const useFetchMyApi = () => {
  const [items, setItems] = useState([]);

  // custom fetch function consumes location
  const locationFetch = (location = "") => {
    // let's stimulate the API call with a 2 seconds response time
    setTimeout(() => setItems([location, Math.random()]), 2000);
  };
  
  return [items, locationFetch]; // <-- return state and fetch function
};

export default function App() {
  const locationSelect = useRef(null);
  const [items, locationFetch] = useFetchMyApi(); // <-- destructure state and fetch function

  useEffect(() => {
    locationFetch("mylocation1");
  }, []); // <-- initial mounting fetch call

  const onButtonClicked = () => {
    const selectedLocation = locationSelect.current.value;
    locationFetch(selectedLocation); // <-- invoke fetch function
  };

  return (
    <div className="App">
      <select ref={locationSelect}>
        <option value="location1">Location 1</option>
        <option value="location2">Location 2</option>
      </select>
      <button onClick={onButtonClicked}>Refresh</button>
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  );
}

Edit calling-custom-hook-in-event-handler (forked)

Upvotes: 18

Related Questions