Is it good practice to chain useEffect hooks? - reactjs

Since there is no setState callback using react hooks I have to use an useEffect hook with the state in its dependency array as a callback function for setting the state.
So I often find myself chaining some useEffects to set states, wait until they were set, and then do some actions with it.
This seems not as the correct way for me.
Here is an Example:
const url = useLocation();
useEffect(() => {
// Do some stuff related to URL
// ...
// after processing the url params i set a config object as a state
setConfig();
}, [url.search]);
useEffect(() => {
// Fetch some data with the config
// ...
// after data fetching is done
setData([]);
}, [config]);
useEffect(() => {
// process my fetched data
}, [data]);
Is this the correct way to do it? Or is there a better approach?

Related

React useEffect dependency array and implementation

I'm new to react hooks and have run into a situation which I solved, but not sure if this is a proper implementation of useEffect. I have a form with some fields (answer, question etc) with some validation, but without the implementation of useEffect below, my validation was one step behind due to the async nature of setting state. After adding useEffect and the state items to the useEffect dependency array that was fixed. But the side effect of adding items to that array was re-rendering, and thus fetchData running each time the state changed. Each time fetch data finished it wiped out the changed state of any items I was changing in the form.
My solution was a "mounted" state variable which is set to true once the fetch occurs. Then if mounted was true, I don't fetch again. This solution seems to have fixed the re-fetching issue as well as the state being one step behind. Is this a proper pattern to use or is there a better/more preferred way?
const [mounted, setMounted] = useState(false)
useEffect(() => {// reactive state
// if params.id that means we need to edit an existing faq
if(params.id && !mounted){
async function fetchData() {
await fetchFaqs();
setMounted(true);
}
fetchData();
}
checkIfFormIsValid();
}, [answer, question, section, sort, checkIfFormIsValid]);
You could just use separate useEffects like this:
// add params.id to dependency array
useEffect(() => {
if (params.id) {
async function fetchData() {
await fetchFaqs();
}
fetchData();
}
}, [params.id])
useEffect(() => {
checkIfFormIsValid();
}, [answer, question, section, sort, checkIfFormIsValid])

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.

Infinite loop on componentdidupdate with useEffect

I'm using redux and trying to fetch data when my component did update.
I'm using useEffect hook to track posts update and getting the state with useSelector.
I'm having issues as the component is making infinite fetching requests instead of a single request.
Anyone knows how I can stop it from making infinite requests
and make a single request if posts updated?
my code:
const posts = useSelector((state) => state.posts.posts);
useEffect(() => {
dispatch(getPosts(page));
}, [posts]);
image showing infinite fetching requests being made
From useEffect documentation
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
So, dispatch(getPosts(page)) will be called on component mount as well when any of the dependency provided get changed, this will make an API request and fetch the posts of this page. Which will eventually update the state.posts.posts once the API is successful. As, the same state.posts.posts is given as dependency to the useEffect hook this will trigger the function to get executed again and the cycle goes on.
For example if you want to make the API call and fetch new posts when there's a change in the page you should provide page as dependency instead of posts as shown below
const posts = useSelector((state) => state.posts.posts);
useEffect(() => {
dispatch(getPosts(page));
}, [page]);
const posts = useSelector((state) => state.posts.posts);
useEffect(() => {
dispatch(getPosts(page));
}, []);
const updateNeeded = useSelector((state) => state.posts.updateNeeded);
useEffect(() => {
if (updateNeeded) {
dispatch(getPosts(page));
}
}, [updateNeeded]);
Change updateNeeded to true by a dispatch action when you want to fetch a new update, and when the update is fetched dispatch an action which will make this flag to false again.

Outdated react state in pusher .bind method

I use pusher-js to receive data from the backend.
I configure it in useEffect like this:
useEffect(() => {
const pusher = new Pusher('my_app_id', {
cluster: 'us3',
});
const channel = pusher.subscribe('messages');
channel.bind('send-message', (data) => {
});
}, []);
In the callback of the .bind method, I want to access the react state. The problem is, that if it gets updated, this callback still has the old version.
channel.bind('send-message', (data) => {
// here is my outdated state
});
How can I acces new, updated state inside this callback?
Thanks in advance
Use another useEffect with the updated state in your dependency array of the useEffect,as soon as the state gets updated that useEffect will. be triggered and inside it you can access the updated state.
I was stuck on the same problem for quite a long time. The way I finally solved this is to store the channel and rebind the event every time the state (which I want to access in the bind callback) changed. Here is a code snippet to help you understand better.
VERY IMPORTANT - Do not forget to unbind the event from the channel before rebinding it. As rebinding without unbinding prior binds would just create additional listeners for the event and all the listeners will fire when the event occurs and it will be a mess. Learnt it the hard way :")
Don't know if this is the best method but worked for me.
const [pusherChannel, setPusherChannel] = useState(null);
const [data, setData] = useState(null);
// TRIGGERED ON MOUNT
useEffect(() => {
const pusher = new Pusher(APP_KEY, {
cluster: APP_CLUSTER
});
const channel = pusher.subscribe(CHANNEL_NAME);
setPusherChannel(channel);
// PREVIOUSLY
// channel.bind(EVENT_NAME, (pusherData) => {
// ...
// Accessing "data" here would give the state used
// during binding the event
//});
}, []);
// TRIGGERED ON CHANGE IN "data"
useEffect(() => {
console.log("Updated data : ", data);
if(pusherChannel && pusherChannel.bind){
console.log("Unbinding Event");
pusherChannel.unbind(EVENT_NAME);
console.log("Rebinding Event");
pusherChannel.bind(EVENT_NAME, (pusherData) => {
// USE UPDATED "data" here
}
}
}, [pusherChannel, data]);
Reference -
Binding Events
Unbinding Events

React Hooks Firebase - useEffect hook not returning any data

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
};

Resources