Home > Back-end >  My useFetch hook is giving me too many re-renders
My useFetch hook is giving me too many re-renders

Time:03-09

I'm trying to abstract my fetch out to a hook in my expo react native app. The fetch must be able to do POST method. I started out by trying to use and then modify the useHook() effect found at https://usehooks-ts.com/react-hook/use-fetch. It caused too many re-renders. So in searching Stack Overflow I found this article that recommended that I do it as a function. Since, I anticipate almost every fetch request is going to be a POST and done with a submit, that looked like a good idea. But it is still giving me too many re-renders. Then I found this article that said to do it with a useCallBack. Still no success...

Here is the hook I'm trying to use:

import { useCallback, useState } from "react";
import { FetchQuery } from "../interfaces/FetchQuery";

interface State<T> {
  data?: T;
  error: Error | string | null;
  fetchData: any;
}
const useFetch2 = <T = unknown,>(): State<T> => {
  const [data, setData] = useState<T>();
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async (query: FetchQuery) => {
    const { url, options } = query;

    if (!url) return;

    const response = await fetch(url, options);

    const data = await response.json();

    if (data.error) {
      setData(undefined);
      setError(data.error);
    } else {
      setData(data);
      setError(null);
    }
  }, []);

  return { fetchData, data, error };
};

export default useFetch2;

This is the component calling the code (right now it assigns all of the data to the error field so I can validate the data coming back):

export default function AdminLogin() {
  // screen controls
  const [err, setErr] = useState<Error | string | undefined>();

  // form controls
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  // fetch information
  // const [fetchQuery, setFetchQuery] = useState<FetchQuery>({});
  const { fetchData, data, error } = useFetch2<User>();

  if (error) {
    setErr(error);
  }
  if (data?.user) {
    setErr(JSON.stringify(data.user));
  }

  const handleSignIn = async () => {       
    const options = {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        "Cache-control": "no-cache",
      },
      body: JSON.stringify({ email, password }),
    };
    const url = BASE_API   "users/login";
    // setFetchQuery({ url, options }); <-- attempted to use this with useEffect

    fetchData({ url, options });
  };

  // useEffect(() => {
  //   fetchData();
  // }, [fetchQuery]);

  return (
    <View style={styles.container}>
      <Text style={styles.msg}>Maps can only be edited by Administers.</Text>

      <View style={styles.controlGroup}>
        <Text style={styles.UnPw}>Email</Text>
        <TextInput
          style={styles.txInput}
          keyboardType="email-address"
          placeholder="Email"
          value={email}
          onChangeText={(value) => setEmail(value)}
        />
      </View>

      <View style={styles.controlGroup}>
        <Text style={styles.UnPw}>Password</Text>
        <TextInput
          style={styles.txInput}
          maxLength={18}
          placeholder="Password"
          value={password}
          onChangeText={(value) => setPassword(value)}
          secureTextEntry
        />
      </View>
      <TouchableOpacity onPress={handleSignIn}>
        <Text>Sign in</Text>
      </TouchableOpacity>
      {/* {status === "loading" && <Text>Loading...</Text>} */}
      {err && (
        <>
          <Text style={styles.errText}>Error:</Text>
          <Text style={styles.errText}>{err}</Text>
        </>
      )}
    </View>
  );
}
    const styles = StyleSheet.create({});

Adding some console.log commands does show that states are continually updating after the fetch command, but I don't understand why nor how to fix it.

CodePudding user response:

You should not update state directly in the component. This may cause the state to be updated on every render, which forces re-render and creates an infinite render loop. Instead, state should be updated only in callbacks or useEffect.

Changes you have to make are convert following

  if (error) {
    setErr(error);
  }
  if (data?.user) {
    setErr(JSON.stringify(data.user));
  }

to

useEffect(() => {
    if (error) {
      setErr(error);
    }
    if (data?.user) {
      setErr(JSON.stringify(data.user));
    }
}, [error, data?.user])

Or, even better would be to remove err state and convert it to const variable like

  // remove following
  // const [err, setErr] = useState<Error | string | undefined>();

  // ..some code
  const { fetchData, data, error } = useFetch2<User>();
  // err is not required to be a state
  const err = error || JSON.strigify(data?.user);

CodePudding user response:

You should remove this code which causes infinite re-renders:

if (error) {
     setErr(error);
   }
   if (data?.user) {
     setErr(JSON.stringify(data.user));
   }

You should put it to useEffect, something like this:

useEffect(() => {
  setErr(error);
}, [error])

useEffect(() => {
  setErr(JSON.stringify(data.user));
}, [data?.user])

Because that code updates state on every render.

  • Related