0
votes

My component fetches data by calling an hook-file which contains logic for requesting via API. By default it will call the API without any extra parameter. In GUI I also show an input where use can enter text. Each time he writes a letter I want to refetch data. But Im not really sure how to do this with react and hooks.

I declared "useEffect". And I see that the content of the input changes. But what more? I cannot call the hook-function from there because I then get this error:

"React Hook "useFetch" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks"

This is the code:

hooks.js

import { useState, useEffect } from "react";
function useFetch(url) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUrl() {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
        setLoading(false);
      }
    fetchUrl();
  }, [url]);
  return [data, loading];
}
export { useFetch };

mycomponent.js

import React, { useState, useEffect } from 'react';
import { useFetch } from "../hooks";    

const MyComponent = () => { 

useEffect(() => {
    console.log('rendered!');
    console.log('searchTerm!',searchTerm);
});

const [searchTerm, setSearchTerm] = useState('');
const [data, loading] = useFetch(
    "http://localhost:8000/endpoint?${searchTerm}"
  );
  return (
    <>
      <h1>Users</h1>
      <p>
        <input type="text" placeholder="Search" id="searchQuery" onChange={(e) => setSearchTerm(e.target.value)} />
      </p>
      {loading ? (
        "Loading..."
      ) : (
        <div>
          {data.users.map((obj) => (
            <div key={`${obj.id}`}>
              {`${obj.firstName}`} {`${obj.lastName}`}
            </div>
          ))}
        </div>
      )}
    </>
  );
}

export default MyComponent;
4
Did you try to export it as default?Yalung Tang
yes, and it still gives the same error.oderfla

4 Answers

1
votes

Create a function to handle your onChange event and call your fetch function from it. Something like this:

mycomponent.js

import React, { useState, useEffect } from 'react';
import { useFetch } from "../hooks";    

const MyComponent = () => { 

useEffect(() => {
    console.log('rendered!');
    console.log('searchTerm!',searchTerm);
});

const [searchTerm, setSearchTerm] = useState('');


const handleChange = e => {
   setSearchTerm(e.target.value)
   useFetch(
    "http://localhost:8000/endpoint?${searchTerm}"
  );
}

const [data, loading] = useFetch(
    "http://localhost:8000/endpoint?${searchTerm}"
  );
  return (
    <>
      <h1>Users</h1>
      <p>
        <input type="text" placeholder="Search" id="searchQuery" onChange={(e) => handleChange(e)} />
      </p>
      {loading ? (
        "Loading..."
      ) : (
        <div>
          {data.users.map((obj) => (
            <div key={`${obj.id}`}>
              {`${obj.firstName}`} {`${obj.lastName}`}
            </div>
          ))}
        </div>
      )}
    </>
  );
}

export default MyComponent;
1
votes

Your code works for me as per your requirement, type 1 or 2 in text box you will have different results.

So basically API get called once with default value of "searchTerm" and then it get called for each time by onChange.

try this at your local -

import React, { useState, useEffect } from "react";
function useFetch(url) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUrl() {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
      setLoading(false);
    }
    fetchUrl();
  }, [url]);
  return [data, loading];
}
export { useFetch };

const MyComponent = () => {
  useEffect(() => {
    console.log("rendered!");
    console.log("searchTerm!", searchTerm);
  });

  const [searchTerm, setSearchTerm] = useState("");
  const [data, loading] = useFetch(
    `https://reqres.in/api/users?page=${searchTerm}`
  );

  return (
    <>
      <h1>Users</h1>
      <p>
        <input
          type="text"
          placeholder="Search"
          id="searchQuery"
          onChange={e => setSearchTerm(e.target.value)}
        />
      </p>
      {loading ? (
        "Loading..."
      ) : (
        <div>
          {data.data.map(obj => (
            <div key={`${obj.id}`}>
              {`${obj.first_name}`} {`${obj.last_name}`}
            </div>
          ))}
        </div>
      )}
    </>
  );
};

export default MyComponent;
1
votes

The way your useFetch hook is setup it will only run once on load. You need to have it setup in a way you can trigger it from an effect function that runs only when searchTerm changes.

0
votes

this is how you handle searching in react properly. It is better to have default searchTerm defined when user lands on your page, because otherwise they will see empty page or seening "loading" text which is not a good user experience.

  const [data, setData] = useState([]);
  const [searchTerm, setSearchTerm] = useState("defaultTerm")

In the first render of page, we should be showing the results of "defaultTerm" search to the user. However, if you do not set up a guard, in each keystroke, your app is going to make api requests which will slow down your app.

To avoid fetching data in each keystroke, we set up "setTimeout" for maybe 500 ms. then each time user types in different search term we have to make sure we clean up previous setTimeout function, so our app will not have memory leak.

useEffect(() => {
    async function fetchUrl() {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
    }
   
   // this is during initial rendering. we have default term but no data yet
   if(searchTerm && !data){
        fetchUrl();
   }else{ 
        //setTimeout returns an id
        const timerId=setTimeout(()=>{
            if(searchTerm){
               fetchUrl}
         },500)

    // this where we do clean up
   return ()=>{clearTimeout(timerId)}
   }
   
  }, [url]);
  return [data, loading];
}

inside useEffect we are allowed to return only a function which is responsible for cleaning up. So right before we call useEffect again, we stop the last setTimeout.