useEffect with a dependency that changes too fast - reactjs

Here's the problem:
function Foo(){
let [events, setEvents] = useState([])
useEffect(() => {
let subscription = client.subscribe("eventType", event => {
setEvents([...events, event])
})
return () => subscription.unsubscribe()
}, [events])
}
So here if I make events a useEffect dependency it will unsubscribe / subscribe every time an event is added, and I will miss some. I can make events a ref, but then it won't re-render.
What's the right way to do this?
Edit: From the comments / answers I've gotten so far there seems to be no good way to deal with this in React. I have an ugly setInterval solution that I will post as an answer if nobody comes up with something better within 2 days.
EDIT 2: Here's a codesandbox showing the issue with setInterval. I'm looking for a solution that doesn't require events as a dependency (removing events dependency is not a solution, obviously.)

Instead of grabbing events from outside the useEffect, you can just pass a callback to setEvents that will receive the current value for events:
function Foo(){
let [events, setEvents] = useState([])
useEffect(() => {
let subscription = client.subscribe("eventType", event => {
setEvents((previousEvents) => [...previousEvents, event])
})
return () => subscription.unsubscribe()
}, []) // no need to add `events` to dependencies because we're getting it from `setEvent`'s callback
}

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])

How to access state inside custom event listener

I have spent time studying the details of other posts about this subject yet there is something I still do not understand. I am hoping I can receive some assistance to see this more clearly. Specifically, I would like to learn how to obtain the current state value inside a custom event handler. Here is some simplified code:
const modal_state_ref = React.useRef(modal_state);
React.useEffect(() => {
modal_state_ref.current = modal_state;
}, [modal_state]);
React.useEffect(() => {
const listener = () => {
console.log(`state in handler is ${modal_state_ref.current}`);
};
window.addEventListener('customEvent-modal-opening', listener);
return () => {
window.removeEventListener('customEvent-modal-opening', listener);
}
}, []);
I believe I understand about closures and how the value in the event handler is from a "snap shot" of the state at the time the event handler is created. I thought, however, that the ref, being an object, would always be reference to the location in memory, so always have the updated value. I'm at a lost for how to proceed.

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

React Hooks: Idiomatic way to ensure that useEffect runs only when the contents of an array argument to the custom hook change

I am creating a custom hook in React which sets up an event listener for a given set of events. A default set of events exists, and the consumer of this custom hook is not expected to customize these in the majority of use-cases. Generally, I want the event listeners to be added upon the mounting of a component and removed upon its un-mounting. However, adhering to the principles of hooks (and the eslint(react-hooks/exhaustive-deps) lint rule), I wish to gracefully handle changes to the list of events to watch. What is the most idiomatic way to achieve this with React hooks?
Assuming I would simply like to remove all event listeners and re-add them when the list of events changes, I could attempt the following:
const useEventWatcher = (
interval = 5000,
events = ['mousemove', 'keydown', 'wheel']
) => {
const timerIdRef = useRef();
useEffect(() => {
const resetInterval = () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(`${interval} milliseconds passed with no ${events.join(', ')} events!`);
}, interval)
};
events.forEach(event => window.addEventListener(event, resetInterval));
// Don't want to miss the first time the interval passes without
// the given events triggering (cannot run the effect after every render due to this!)
resetInterval();
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval]);
}
Unfortunately, this will not function as intended. Note that I would like to provide a default value for the events parameter. Doing that with the current approach means that events points to a different reference every time the custom hook runs, which means the effect runs every time as well (due to shallow dependency comparison). Ideally, I would like a way of having the effect depend on the contents of the array, rather than the reference. What is the best way of achieving this?
You can separate two side effects in two different useEffects.
You can run the initial resetInterval in the first useEffect on load.
You need to run it once, and you might use a dependency of [].
But then, you need to extract resetInterval outside the useEffect.
Another problem is that, now resetInterval is re-created during every render.
So you can wrap it in useCallback.
The first useEffect depends on resetInterval (which causes the useEffect run once on load, thus will call resetInterval)
Now you can subscribe all events in the second useEffect with dependencies on [events, interval, resetInterval] as suggested by "eslint(react-hooks/exhaustive-deps) lint rule".
The result would look like following &
You can check the working demo on CodeSandbox.
const useEventWatcher = (
interval = 2000,
events = ["mousemove", "keydown", "wheel"]
) => {
const timerIdRef = useRef();
const resetInterval = useCallback(() => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(
`${interval} seconds passed with no ${events.join(", ")} events!`
);
}, interval);
}, [events, interval]);
useEffect(() => {
resetInterval();
}, [resetInterval]);
useEffect(() => {
events.forEach(event => window.addEventListener(event, resetInterval));
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval, resetInterval]);
};
Check out the working page (I set the interval to 2 seconds for demo purpose)
When you move the mouse around, scroll wheels or press keys, the console log won't appear. Once those events are not fired, then you will see the console log messages.
So we need to include events into deps but for sure we don't want endless render loop.
Option #1: use JSON.stringify or similar to pass string as dependency not an array
function useEventWatcher(events = ['click'])
useEffect(() => {
}, [JSON.stringifiy(events.sort())])
However ESLint will still complain so either suppress it or use de-stringify:
const eventsStringified = JSON.stringify(events.sort());
useEffect(() => {
const eventsUnstringified = JSON.parse(eventsStringified);
}, [eventStringified]);
Option #2: move setting default value into useMemo. So default values will be referentially the same while events parameter is not passed(so it is undefined)
function useEventWatcher(events) {
const eventsOrDefault = useMemo(() => eventsPassed || ['click'], [events]);
useEffect(() => {
}, [eventsOrDefault]);
}

Resources