React Hooks Firebase - useEffect hook not returning any data - reactjs

I am trying to use the useEffect hook in React to listen for changes in a location in firestore.
Initially I didn't have an empty array as the second prop in the useEffect method and I didn't unsubscribe from the onSnapshot listener. I received the correct data in the projects variable after a short delay.
However, when I experienced extreme performance issues, I added in the unsubscribe and empty array which I should have put in earlier. Strangely, now no data is returned but the performance issues are gone.
What might be preventing the variable updating to reflect the data in firestore?
function useProjects(organisation) {
const [projects, setProjects] = useState({
docs: []
});
useEffect(() => {
if (!organisation.docs[0]) return;
const unsubscribe = firebase.firestore().collection('organisations').doc(organisation.docs[0].id).collection("projects").onSnapshot(snapshot => {
setProjects(snapshot);
});
return () => unsubscribe()
}, []);
return projects
};
const projects = useProjects(organisation);

You'll need a value in the dependency array for the useEffect hook. I'd probably suggest the values you are using in the useEffectHook. Otherwise with [] as the dependency array, the effect will only trigger once (on mount) and never again. The point of the dependency array is to tell react to re run the hook whenever a dependency changes.
Here's an example I'd suggest based on what's in the hook currently (using the id that you send to firebase in the call). I'm using optional chaining here as it makes the logic less verbose.
function useProjects(organisation) {
const [projects, setProjects] = useState({
docs: []
});
useEffect(() => {
if (!organisation.docs[0]) return;
const unsubscribe = firebase.firestore().collection('organisations').doc(organisation.docs[0].id).collection("projects").onSnapshot(snapshot => {
setProjects(snapshot);
});
return () => unsubscribe()
}, [organization.docs[0]?.id]);
return projects
};

Related

My custom React hook method "useFetch" is running 8 times when called

Hope anyone is able to help me with a custom react hook.
My custom react hook "useFetch" is running 8 times when called.
Can anyone see, why it is running 8 times when the custom "useFetch" hook is called?
I am a bit new to React, but it seems like I am using useEffect method wrong. Or maybe I need to use another method.
UseFetch hook method:
import React, { useState, useEffect } from "react";
export const useFetch = function (
options = {
IsPending: true,
},
data = {}
) {
// load data
const [loadData, setLoadData] = useState(null);
// pending
const [isPending, setIsPending] = useState(false);
// error
const [isError, setIsError] = useState(false);
useEffect(() => {
// method
const fetchData = async function () {
// try
try {
// set pending
setIsPending(true);
// response
const response = await fetch(data.url, data);
// handle errors
if (response.status !== 200 && response.status !== 201) {
// throw new error with returned error messages
throw new Error(`Unable to fetch. ${response.statusText}`);
}
// convert to json
const json = await response.json();
// set load data
setLoadData(json);
// set error
setIsError(false);
// set pending
setIsPending(false);
// catch errors
} catch (err) {
// set error
setIsError(`Error fetching data: ${err.message}`);
// set pending
setIsPending(false);
}
};
// invoke fetch data method
fetchData();
}, []);
// return
return {
loadData,
isPending,
isError,
};
};
export default useFetch;
Everytime you change a state in a hook, the component that has the hook in it will rerender, making it call the function again.
So let's start counting the renders/rerenders by the change of state:
Component mounted
setIsPending(true)
setLoadData(json)
setIsPending(false)
(depending if it's successful or not you might get more state changes, and therefore rerenders, and therefore hook being called again)
So 4 is not 8, so why are you getting 8?
I presume you are using React18, and React18 on development and StrictMode will call your useEffect hooks twice on mount: React Hooks: useEffect() is called twice even if an empty array is used as an argument
What can you do to avoid this?
First of all, check on the network tab how many times you are actually fetching the data, I presume is not more than 2.
But even so you probably don't want to fetch the data 2 times, even though this behaviour won't be on production and will only be on development. For this we can use the useEffect cleanup function + a ref.
const hasDataFetched = useRef(false);
useEffect(() => {
// check if data has been fetched
if (!hasDataFetched.current) {
const fetchData = async function () {
// fetch data logic in here
};
fetchData();
}
// cleanup function
return () => {
// set has data fetched to true
hasDataFetched.current = true;
};
}, []);
Or as you suggested, we can also add data to the dependency array. Adding a variable to a dependency array means the useEffect will only be triggered again, when the value of the variable inside the dependency array has changed.
(Noting that data is the argument you pass to the useFetch hook and not the actual data you get from the fetch, maybe think about renaming this property to something more clear).
useEffect(() => {
// check if data has been fetched
const fetchData = async function () {
// fetch data logic in here
};
fetchData();
}, [data]);
This will make it so, that only if loadData has not been fetched, then it will fetch it. This will make it so that you only have 4 rerenders and 1 fetch.
(There is a good guide on useEffect on the React18 Docs: https://beta.reactjs.org/learn/synchronizing-with-effects)
Every time you change the state within the hook, the parent component that calls the hooks will re-render, which will cause the hook to run again. Now, the empty array in your useEffect dependency should be preventing the logic of the hook from getting called again, but the hook itself will run.

Observable in hook dependency array causing infinite loop

I have a custom hook which receives an array of Observables as a parameter, then (in a useEffect hook) sets internal state any time the Observable emits a value.
export const myHook = (obs$: Observable<any>) => {
const [state, setState] = useState();
useEffect(() => {
obs$.subscribe((x) => setState(x));
}, [obs$]);
return state;
}
This causes an infinite loop, I'm guessing because some property on the obs$ object is changing as it resolves. It works with an empty dependency array, but for the sake of correctness I don't want to do that because it should recalculate if obs$ is genuinely changed outside of the scope of the custom hook (not sure why this would ever happen, but still).
Observables are objects. The following expression is false: {} === {} (similarly for == and other "similar" objects) (more info here).
React's useEffect hook uses === to compare dependencies to see if they've changed. Since each obs$ has different references, you'll need to find a way to ensure that the same exact object reference is passed to your hook. If the === comparator returns true, then the useEffect won't run again.
That begs the question: How do we ensure that the obs$ has the same reference unless it changes?
There are many ways, but useMemo and useCallback are tools that can help with this. Each of those have their own dependencies, but they shouldn't be used in your hook since you'll have the same problem of needing to depend on the same obs$ that changes with each function call.
If obs$ has an unsubscribe method, then I would unsubscribe when the component re-renders (or unmounts).
export const useHook = (obs$: Observable<any>) => { // hooks should start with the `use` prefix
const [state, setState] = useState();
useEffect(() => {
obs$.subscribe((x) => setState(x));
return () => obs$.unsubscribe() // return a cleanup function
});
return state;
}
In the React docs, there is a similar example.
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});

useEffect on infinite loop using async fetch function

I am trying to understand why the following useEffect is running in an infinite loop. I made the fetchSchedule helper function to call the getSchedule service (using Axios to query the API endpoint). Reason I did not define this function inside the useEffect hook is because I would like to alternatively also call it whenever the onStatus function is invoked (which toggles a Boolean PUT request on a separate endpoint).
The eslinter is requiring fetchSchedule be added to the array of dependencies, which seems to be triggering the infinite loop.
The way it should work is fetching the data from the database on first render, and then only each time either the value prop is updated or the onStatus button is toggled.
So far my research seems to point that this may have something to do with the way useEffect behaves with async functions and closures. I’m still trying to understand Hooks and evidently there’s something I’m not getting in my code…
import React, { useEffect, useCallback } from 'react';
import useStateRef from 'react-usestateref';
import { NavLink } from 'react-router-dom';
import { getSchedule, updateStatus } from '../../services/scheduleService';
import Status from './status';
// import Pagination from './pagination';
const List = ({ value }) => {
// eslint-disable-next-line
const [schedule, setSchedule, ref] = useStateRef([]);
// const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
}, [value, setSchedule]);
const onStatus = (id) => {
updateStatus(id);
fetchSchedule();
console.log('fetch', ref.current[0].completed);
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
return (...)
Update March 2021
After working with the repo owner for react-usestateref, the package now functions as originally intended and is safe to use as a replacement for useState as of version 1.0.5. The current implementation looks like this:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(state);
var dispatch = React.useCallback(function(val) {
ref.current = typeof val === "function" ?
val(ref.current) : val;
setState(ref.current);
}, []);
return [state, dispatch, ref];
};
You would be fine if it weren't for this react-usestateref import.
The hook returns a plain anonymous function for setting state which means that it will be recreated on every render - you cannot usefully include it in any dependency array as that too will be updated on every render. However, since the function is being returned from an unknown custom hook (and regardless, ESLint would correctly identify that it is not a proper setter function) you'll get warnings when you don't.
The 'problem' which it tries to solve is also going to introduce bad practice into your code - it's a pretty way to avoid properly handling dependencies which are there to make your code safer.
If you go back to a standard state hook I believe this code will work fine. Instead of trying to get a ref of the state in onStatus, make it async as well and return the data from fetchSchedule as well as setting it.
const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
return data;
}, [value]);
const onStatus = async (id) => {
updateStatus(id);
const data = await fetchSchedule();
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
Alternatively, although again I wouldn't really recommend using this, we could actually write a safe version of the useStateRef hook instead:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(defaultValue);
ref.current = state;
return [state, setState, ref];
}
A state setter function is always referentially identical throughout the lifespan of a component so this can be included in a dependency array without causing the effect/callback to be recreated.

Prevent infinite renders when updating state variable inside useEffect hook with data fetched using useQuery of graphql

Graphql provides useQuery hook to fetch data. It will get called whenever the component re-renders.
//mocking useQuery hook of graphql, which updates the data variable
const data = useQuery(false);
I am using useEffect hook to control how many times should "useQuery" be called.
What I want to do is whenever I receive the data from useQuery, I want to perform some operation on the data and set it to another state variable "stateOfValue" which is a nested object data. So this has to be done inside the useEffect hook.
Hence I need to add my stateOfValue and "data" (this has my API data) variable as a dependencies to the useEffect hook.
const [stateOfValue, setStateOfValue] = useState({
name: "jack",
options: []
});
const someOperation = (currentState) => {
return {
...currentState,
options: [1, 2, 3]
};
}
useEffect(() => {
if (data) {
let newValue = someOperation(stateOfValue);
setStateOfValue(newValue);
}
}, [data, stateOfValue]);
Basically I am adding all the variables which are being used inside my useEffect as a dependency because that is the right way to do according to Dan Abramov.
Now, according to react, state updates must be done without mutations to I am creating a new object every time I need to update the state. But with setting a new state variable object, my component gets re-rendered, causing an infinite renders.
How to go about implementing it in such a manner that I pass in all the variables to my dependency array of useEffect, and having it execute useEffect only once.
Please note: it works if I don't add stateOfValue variable to dependencies, but that would be lying to react.
Here is the reproduced link.
I think you misunderstood
what you want to be in dependencies array is [data, setStateOfValue] not [data, stateOfValue]. because you use setStateOfValue not stateOfValue inside useEffect
The proper one is:
const [stateOfValue, setStateOfValue] = useState({
name: "jack",
options: []
});
const someOperation = useCallback((prevValue) => {
return {
...prevValue,
options: [1, 2, 3]
};
},[])
useEffect(() => {
if (data) {
setStateOfValue(prevValue => {
let newValue = someOperation(prevValue);
return newValue
});
}
}, [data, setStateOfValue,someOperation]);
If you want to set state in an effect you can do the following:
const data = useQuery(query);
const [stateOfValue, setStateOfValue] = useState({});
const someOperation = useCallback(
() =>
setStateOfValue((current) => ({ ...current, data })),
[data]
);
useEffect(() => someOperation(), [someOperation]);
Every time data changes the function SomeOperation is re created and causes the effect to run. At some point data is loaded or there is an error and data is not re created again causing someOperation not to be created again and the effect not to run again.
First I'd question if you need to store stateOfValue as state. If not (eg it won't be edited by anything else) you could potentially use the useMemo hook instead
const myComputedValue = useMemo(() => someOperation(data), [data]);
Now myComputedValue will be the result of someOperation, but it will only re-run when data changes
If it's necessary to store it as state you might be able to use the onCompleted option in useQuery
const data = useQuery(query, {
onCompleted: response => {
let newValue = someOperation();
setStateOfValue(newValue);
}
)

React useEffect hook missing dependencies linter warnings

I am using the React useEffect hook to obtain API data on component load, with the useAxios hook. The code is as below (simplified):
const [formData, setFormData] = useState<FormData>();
const [{ , executeGet] = useAxios('', {
manual: true,
});
const getFormData = async () => {
let r = await executeGet({ url: `http://blahblahblah/`});
return r.data;
};
useEffect(() => {
const getData = async () => {
try {
let response = await getAPIData();
if (response) {
setFormData(response);
} catch (e) {
setFormError(true);
}
};
getData();
}, []);
This pattern is used frequently in the codebase, but I am getting the linter warning:
React Hook useEffect has missing dependencies: 'getFormData'. Either include them or remove the dependency array react-hooks/exhaustive-deps
I can suppress the warning successfully with:
// eslint-disable-line react-hooks/exhaustive-deps
but it feels wrong to do this!
I can add constants to the dependency list without a problem, however when I add the getFormData function, I get an infinite loop. I have read around the area a lot and understand why the dependencies are needed. I am not sure if the useEffect hook is the best way to obtain the data, or whether there is a way to fetch data.
The problem is that you are defining getFormData within the component. In each render, it is reassigned. As is, this would mean that your initial useEffect would only be bound to to first getFormData, not the one from the most recent render. This causes a warning because often this is not what you intend, particularly if your getFormData depended on state or props that could change.
The simplest solution in this case is to move the definition of your getFormData outside of your component, and use Axios directly instead of using a hook. That way it wouldn't need to be defined on every render anyways.
you should initiate getFormData function using useCallback hook and then put it in useEffect dependency list.
const getFormData = useCallback(async () => {
let r = await executeGet({ url: `http://blahblahblah/`});
return r.data;
}, [executeGet]);
you can read more about useCallback in reactjs site:
https://reactjs.org/docs/hooks-reference.html#usecallback

Resources