useEffect lazy created cleanup function - reactjs

I'm trying to create hook that is is using an effect in which side effect function returns the cleanup callback. However I want to call it only when component is unmounted, not on the rerender.
Normal approach when you call useEffect with empty deps array won't work here as the cleanup function is created only once, on the first call of the hook. But my clean up is created later, so there is no way to change it.
function useListener(data) {
const [response, updateResponse] = useState(null);
useEffect(
() => {
if (data) {
const removeListener = callRequest(data, resp => {
updateResponse(resp);
});
return removeListener;
}
},
[data]
);
return response;
}
This comes down to a following problem: In normal class component, the willComponentUnmount could make a decision based on a current component state but in case of useEffect, state is passed via closure to the cleanup and there is no way to pass the information later if the state has changed

You can use useRef to save and update your callback function
The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. more
function useListener(data) {
const [response, updateResponse] = useState(null);
const cleanUpCallbackRef = useRef(() => {});
useEffect(
() => {
if (data) {
cleanUpCallbackRef.current = callRequest(data, resp => {
updateResponse(resp);
});
}
},
[data]
);
useEffect(() => {
return () => {
cleanUpCallbackRef.current();
}
}, []);
return response;
}
I create a simple example here

Related

React - set state doesn't change in callback function

I'm not able to read current state inside refreshWarehouseCallback function. Why?
My component:
export function Schedules({ tsmService, push, pubsub }: Props) {
const [myState, setMyState] = useState<any>(initialState);
useEffect(() => {
service
.getWarehouses()
.then((warehouses) =>
getCurrentWarehouseData(warehouses) // inside of this function I can without problems set myState
)
.catch(() => catchError());
const pushToken = push.subscribe('public/ttt/#');
const pubSubToken = pubsub.subscribe(
'push:ttt.*',
refreshWarehouseCallback // HERE IS PROBLEM, when I try to read current state from this function I get old data, state changed in other functions cannot be read in thi function
);
return () => {
pubsub.unsubscribe(pubSubToken);
push.unsubscribe(pushToken);
};
}, []);
...
function refreshWarehouseCallback(eventName: string, content: any) {
const {warehouseId} = myState; // undefined!!!
case pushEvents.ramp.updated: {
}
}
return (
<Views
warehouses={myState.warehouses}
allRamps={myState.allRamps}
currentWarehouse={myState.currentWarehouse}
pending={myState.pending}
error={myState.error}
/>
I have to use useRef to store current state additionally to be able to rerender the whole component.
My question is - is there any other solution without useRef? Where is the problem? Calback function doesn't work with useState hook?
Your pub/sub pattern does not inherit React's states. Whenever subscribe is triggered, and your callback function is initialized, that callback will not get any new values from myState.
To be able to use React's states, you can wrap refreshWarehouseCallback into another function like below
//`my state` is passed into the first function (the function wrapper)
//the inner function is your original function
const refreshWarehouseCallback =
(myState) => (eventName: string, content: any) => {
const { warehouseId } = myState;
//your other logic
};
And then you can add another useEffect to update subscribe after state changes (in this case, myState updates)
//a new state to store the updated pub/sub after every clean-up
const [pubSubToken, setPubSubToken] = useState();
useEffect(() => {
//clean up when your state updates
if (pubSubToken) {
pubsub.unsubscribe(pubSubToken);
}
const updatedPubSubToken = pubsub.subscribe(
"push:ttt.*",
refreshWarehouseCallback(myState) //execute the function wrapper to pass `myState` down to your original callback function
);
//update new pub/sub token
setPubSubToken(updatedPubSubToken);
return () => {
pubsub.unsubscribe(updatedPubSubToken);
};
//add `myState` as a dependency
}, [myState]);
//you can combine this with your previous useEffect
useEffect(() => {
const pushToken = push.subscribe("public/ttt/#");
return () => {
pubsub.unsubscribe(pushToken);
};
}, []);

React useEffect to have different behavior on first load and subsequent updates

I am using React with typescript and I want to convert my class component to a functional component, but my class component has two different componentDidMount and comonentDidUpdate behaviors:
componentDidMount() {
this.props.turnResetOff();
}
componentDidUpdate() {
if (this.props.resetForm) {
this.resetChangeForm();
this.props.turnResetOff();
}
}
I just want my form to reset every time it loads except the first time, because I have a menu drop-down that allows clearing the form but I want the data to not reset on mount.
I tried using this: componentDidMount equivalent on a React function/Hooks component?
const turnResetOff = props.turnResetOff;// a function
const resetForm = props.resetForm;
const setPersonId = props.setPersonId;// a function
useEffect(() => {
turnResetOff();
}, [turnResetOff]);
useEffect(() => {
const resetChangeForm = () => {/*definition*/};
if (resetForm) {
resetChangeForm();
turnResetOff();
}
}, [resetForm, turnResetOff, setPersonId]);
However, this causes an infinite re-render. Even if I useCallback for turnResetOff:
turnResetOff={useCallback(() => {
if (shouldReset) {
setShouldReset(false);
}
}, [shouldReset])}
I also tried using useRef to count the number of times this has been rendered, with the same result (infinite rerender - this is a simplified version even).
const [shouldReset, setShouldReset] = useState<boolean>(false);
const mountedTrackerRef = useRef(false);
useEffect(() => {
if (mountedTrackerRef.current === false) {
console.log("mounted now!");
mountedTrackerRef.current = true;
// props.turnResetOff();
setShouldReset(false);
} else {
console.log("mounted already... updating");
// if (props.resetForm) {
if (shouldReset) {
// resetChangeForm();
// props.turnResetOff();
setShouldReset(false);
}
}
}, [mountedTrackerRef, shouldReset]);
When you call useEffect() you can return a clean-up function. That cleanup function gets called when the component is unmounted. So, perhaps what you want to do is this: when you are called with turnResetOff then call it, and return a function that calls turnResetOff. The return function will be called when the component unmounts, so next time the component mounts it won't reset.
Something to along these lines:
useEffect(
() => {
turnResetOff()
return () => {setShouldReset(false)}
}
,[turnResetOff, setShouldReset])
Using the logic you have in the class component, the fellowing should give you identical behavior in a functional component
const turnResetOff = props.turnResetOff;// a function
const resetForm = props.resetForm;
const setPersonId = props.setPersonId;// a function
// useEffect with an empty dependency array is identical to ComponentDidMount event
useEffect(() => {
turnResetOff();
}, []);
// Just need resetForm as dependency since only resetForm was checked in the componentDidUpdate
useEffect(() => {
const resetChangeForm = () => {/*definition*/};
if (resetForm) {
resetChangeForm();
turnResetOff();
}
}, [resetForm]);

I am trying to use callback in hook but can not get latest context value in the callback

const Demo = () => {
const { name } = useContext(AppContext);
function emiterCallback(val) {
console.log('value==', name);
if (name !== val) {
setContextState({ name: val });
}
}
useEffect(() => {
window.eventEmitter.on('CHANGED', emiterCallback);
return () => {
window.eventEmitter.removeListener('CHANGED', emiterCallback);
};
}, []);
}
in class component
this.emiterCallback = this.emiterCallback.bind(this) can solve my question, but how to use it in hook ?
The problem you have here is due to the fact that useEffect with an empty array dependency only runs once - when the component mounts. This means that the emiterCallback it assigns as the event function is the very first one that's made on the first render. Since you just declare emiterCallback in the body of the function, it gets remade every single re-render, so after a single re-render, it will be a different one to the event one you assigned when the component mounted. Try something like this:
import React, { useCallback, useContext, useEffect } from 'react';
...
const Demo = () => {
const { name } = useContext(AppContext);
// Assign it to a memoized function that will recalculate as needed when the context value changes
const emiterCallback = useCallback((val) => {
console.log('value==', name);
if (name !== val) {
setContextState({ name: val });
}
}, [name]);
// Adding the function as a dependency means the .on function should be updated as needed
useEffect(() => {
window.eventEmitter.on('CHANGED', emiterCallback);
return () => {
window.eventEmitter.removeListener('CHANGED', emiterCallback);
};
}, [emiterCallback]);
}
This code isn't tested but you get the idea
use useCallback to memorize the effect no need for bind since there is no this as it is not a class,
Here read more about it -
How can I bind function with hooks in React?

Invalid hook call when trying to fetch data using useCallback

I'm trying to call useState inside an async function like:
const [searchParams, setSearchParams] = useState({});
const fetchData = () => useCallback(
() => {
if (!isEmpty(searchParams)) {
setIsLoading(true); // this is a state hook
fetchData(searchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
},
[],
);
There are two states I am trying to set inside this fetchData function (setIsLoading and setIds), but whenever this function is executed am getting the error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
What is this Rule of hooks I am breaking here?
Is there any way around to set these states from the function?
PS: I only used the useCallback hook here for calling this function with lodash/debounce
Edit: The function is called inside useEffect like:
const debouncedSearch = debounce(fetchSearchData, 1000); // Is this the right way to use debounce? I think this is created every render.
const handleFilter = (filterParams) => {
setSearchParams(filterParams);
};
useEffect(() => {
console.log('effect', searchParams); // {name: 'asd'}
debouncedSearch(searchParams); // Tried without passing arguments here as it is available in state.
// But new searchParams are not showing in the `fetchData`. so had to pass from here.
}, [searchParams]);
The hook rule you are breaking concerns useCallback because you are returning it as the result of your fetchData;
useCallback should be called on top level; not in a callback, like this:
const fetchData = useCallback(
() => {
if (!isEmpty(searchParams)) {
setIsLoading(true); // this is a state hook
fetchData(searchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
},
[],
);
The code you wrote is equivalent to
const fetchData = () => { return React.useCallback(...
or even
function fetchData() { return React.useCallback(...
To read more about why you can't do this, I highly recommend this blog post.
edit:
To use the debounced searchParams, you don't need to debounce the function that does the call, but rather debounce the searched value. (and you don't actually the fetchData function that calls React.useCallback at all, just use it directly in your useEffect)
I recommend using this useDebounce hook to debounce your search query
const [searchParams, setSearchParams] = React.useState('');
const debouncedSearchParams = useDebounce(searchParams, 300);// let's say you debounce using a delay of 300ms
React.useEffect(() => {
if (!isEmpty(debouncedSearchQuery)) {
setIsLoading(true); // this is a state hook
fetchData(debouncedSearchParams)
.then((ids) => {
setIds(ids); // Setting the id state here
}).catch(() => setIsLoading(false));
}
}, [debouncedSearchParams]); // only call this effect again if the debounced value changes

How can I check if the component is unmounted in a functional component?

A callback function sets the component state. But sometimes subscription which supplies the data need to end. Because callback is executed asynchronously, it's not aware if the subscription ends just after making the service call (which executes the callback function).
Then I see following error in the console:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
Is there a way to access the component state, even if I am in the callback function?
This would be the steps:
subscribe with parameters
unsubscribe
component is unmounted
subscribed service executes the callback function
callback functio sets state in an unmounted component and it gives error above
You can use a ref like this:
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => { mounted.current = false; };
}, []);
Then in your callback you can check if mounted.current === false and avoid setting the state
Here is some pseudo code how you can use useEffect to see if a component is mounted.
It uses useEffect to listen to someService when it receives a message it checks if the component is mounted (cleanup function is also called when component unmounts) and if it is it uses setServiceMessage that was created by useState to set messages received by the service:
import { useState, useEffect } from 'react';
import someService from 'some-service';
export default props => {
const userId = props.userId;
const [serviceMessage, setServiceMessage] = useState([]);
useEffect(
() => {
const mounted = { current: true };
someService.listen(
//listen to messages for this user
userId,
//callback when message is received
message => {
//only set message when component is mounted
if (mounted.current) {
setServiceMessage(serviceMessage.concat(message));
}
});
//returning cleanup function
return () => {
//do not listen to the service anymore
someService.stopListen(userId);
//set mounted to false if userId changed then mounted
// will immediately be set to true again and someService
// will listen to another user's messages but if the
// component is unmounted then mounted.current will
// continue to be false
mounted.current = false;
};
},//<-- the function passed to useEffects
//the function passed to useEffect will be called
//every time props.userId changes, you can pass multiple
//values here like [userId,otherValue] and then the function
//will be called whenever one of the values changes
//note that when this is an object then {hi:1} is not {hi:1}
//referential equality is checked so create this with memoization
//if this is an object created by mapStateToProps or a function
[userId]
);
};
This hook (insired from Mohamed answer) solves the problem in a more elegant maner:
function useMounted() {
const mounted = useMemo(() => ({ current: true }), []);
useEffect(() => {
return () => { mounted.current = false}
}, [mounted]);
return mounted;
}
(Updated to use useMemo instead of useRef for readability).
You can return a function from useEffect, which will be fired when a functional component unmount.
Please read this
import React, { useEffect } from 'react';
const ComponentExample = () => {
useEffect(() => {
// Anything in here is fired on component mount.
return () => {
// Anything in here is fired on component unmount.
}
}, [])
}
I found the accepted answer to this question hard to read, and React provides their own documentation on just this question. Their example is:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
I've created a component I call <Fade> that will fade in/out any children its given. Note that it relies on bootstrap's .fade and .show classes, though these could easily be implemented without bootstrap.
const Fade: React.FC<{}> = ({ children }) => {
const [ className, setClassName ] = useState('fade')
const [ newChildren, setNewChildren ] = useState(children)
useEffect(() => {
setClassName('fade')
const timerId = setTimeout(() => {
setClassName('fade show')
setNewChildren(children)
}, TIMEOUT_DURATION)
return () => {
clearTimeout(timerId)
}
}, [children])
return <Container fluid className={className + ' p-0'}>{newChildren}</Container>
}
It all boils down to one rule: unsubscribe from your asynchronous tasks in the cleanup function returned from useEffect.

Resources