Cleanup after a async function in useEffect - reactjs

I'm migrating a React project to TypeScript but I'm struggling to type out this function and, subsequently the useEffect. I know that registerListener has to return a Promise but how do I access the value of that promise in useEffect?
const registerListener = async () => {
const listener = await someAsyncAction();
return { listener };
};
React.useEffect(() => {
const { listener } = registerListener();
return () => {
removeListener(listener);
};
}, []);

Have you tried to declare a return type on registerListener?
const registerListener = async (): Promise<{ listener: someAsyncActionReturnType }> => {
const listener = await someAsyncAction();
return { listener };
};

Related

React prevent re render

How should I prevent a re render when I click on onClose to close the modal.
It looks like the dispatch function: dispatch(setActiveStep(0) cause some troubles.
export default function ImportOrderModal(props: ImportOrderModalProps) {
const { open, onClose } = props
const {
orderImport: { activeStep }
} = useAppSelector((state: RootState) => state)
const steps = useImportOrderConfig()
const dispatch = useDispatch()
React.useEffect(() => {
getOrdersList()
}, [])
const onCloseModal = () => {
onClose()
// Force a re render because of activeStep value
dispatch(setActiveStep(0))
}
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
return (
<Modal open={open} onClose={onCloseModal}>
<Stepper steps={steps} currentStepNumber={activeStep} />
<FormSteps />
</Modal>
)
}
This block is outside of your useEffect()
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
This will cause rendering troubles.
if you're using an older version of React (<17) that doesn't enforce <React.StrictMode> you can get away with rewriting that as:
useEffect(() => {
getOrderList
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
}, [dispatch]);
if you're using a newer version of React (>18) you will have to cleanup your asynchronous call in the cleanup function of your useEffect().
useEffect(() => {
// This has to be passed down to your fetch/axios call
const controller = new AbortController();
getOrderList(controller)
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
return () => {
// This will abort any ongoing async call
controller.abort();
}
}, [dispatch]);
For this to make sense I will probably have to write an example of the api call for you as well, if you don't mind I'll use axios for the example but it essentially works the same-ish with .fetch.
const getOrderList = async (controller) => {
try {
const { data } = await axios.get("url", { signal: controller.signal });
return data.orders;
} catch (e) {
throw e;
}
}

How to call a Firestore unsubscribe function from another function in a functional component?

I have the following function that runs upon a click. It basically starts a Firestore listener to grab messages. It also has an unsubscribe function declared which I am trying to call from another function:
const getMessages = (uid) => {
const ref = firebase.firestore().collection('Chats').doc(uid).collection('Messages');
const query = ref.where("uid", "==", uid).orderBy('timestamp', 'desc').limit(25);
const unsubFromMessages = query.onSnapshot((snapshot) => {
if (snapshot.empty) {
console.log('No matching documents.');
}
snapshot.docChanges().forEach((change) => {
if (change.type === 'removed') {
console.log(change.doc.data().content)
} else if (change.type === 'added') {
setMessages(prevFiles => ([...prevFiles, {
id: change.doc.id, body: change.doc.data()
}]))
// setTimeout( this.scrollToBottom(), 2000)
}
});
}, (error) => {console.log(error)});
}
As you can see inside of it, I declare a function to unsubscribe from the Firestore listener (const unsubFromMessages = query.onSnapshot). I want to be able to call this "unsubFromMessages" function upon another button click from another function which basically closes a chat.
Here's that closeChat function:
const closeChat = () => {
setMessages([]);
unsubFromMessages();
}
Unfortunately, the closeChat function can not access the unsubFromMessages function to unsubscribe from the Firestore listener. I get the following error:
Line 177:5: 'unsubFromMessages' is not defined no-undef
I know how to do it in a class component where I would simply declare the function as this.unsubFromMessages = ... and then call it from any other function but I can not figure out how to do it in a functional component. Please advise.
You could store the unsubFromMessages callback in a React ref and access it in the other click hander.
const unsubFromMessagesRef = React.useRef();
...
const getMessages = (uid) => {
...
const unsubFromMessages = query.onSnapshot((snapshot) => { ..... };
unsubFromMessagesRef.current = unsubFromMessages;
...
}
...
const closeChat = () => {
setMessages([]);
unsubFromMessagesRef.current && unsubFromMessagesRef.current();
}
Don't forget to unsubscribe when the component unmounts:
useEffect(() => {
return () => {
unsubFromMessagesRef.current && unsubFromMessagesRef.current()
};
}, []);

How to translate componentDidMound and componentWillUnmount to UseEffect (React Hooks) with Firebase/Firestore?

How would one go about using the useEffect hook to replace both componentDidMount and componentWillUnmount while working with Firebase? I can't find a solution to this 'unsubscribe' function.
unsubscribe = null;
componentDidMount = async () => {
this.unsubscribe = firestore.collection('posts').onSnapshot(snapshot => {
const posts = snapshot.docs.map(...)
this.setState({ posts })
})
}
componentWillUnmount = () => {
this.unsubscribe()
}
Here's what I tried:
useEffect(() => {
async function getSnapshot() {
const unsubscribe = firestore.collection('posts').onSnapshot(snapshot => {
const posts = snapshot.docs.map(...)
setPosts(posts)
}
getSnapshot()
//return something to clear it? I don't have access to 'unsubscribe'
}, [])
You are actually pretty close with your answer. You weren't using await in your function, so there was no point in using it.
useEffect(() => {
const unsubscribe = firestore.collection('posts').onSnapshot((snapshot) => {
const posts = snapshot.docs.map(...)
setPosts(posts);
});
return () => {
unsubscribe();
};
}, []);
If you did need to use async, you can just utilize the closure to get unsubscribe out of the async function.
useEffect(() => {
let unsubscribe;
async function getSnapshot() {
unsubscribe = firestore.collection('posts').onSnapshot((snapshot) => {
const posts = snapshot.docs.map(...)
setPosts(posts);
});
}
getSnapshot();
return () => {
unsubscribe();
};
}, []);
you're probably going to run into trouble using async inside useEffect, check out https://www.npmjs.com/package/use-async-effect
useAsyncEffect( async() => {
const unsubscribe = await firestore.collection('posts').onSnapshot(snapshot => {
const posts = snapshot.docs.map(...)
setPosts(posts)
}
return () => {
console.log("unmount")
unsubscribe()
};
}, [])
EDIT: actually it seems from the docs that you don't need async at all there:
have you tried this format?
useEffect(
() => {
const unsubscribe = firebase
.firestore()
.collection('recipes')
.doc(id)
.collection('ingredients')
.onSnapshot( snapshot => { const ingredients = [] snapshot.forEach(doc => { ingredients.push(doc) }) setLoading(false) setIngredients(ingredients) }, err => { setError(err) } )
return () => unsubscribe()
},
[id]
)

Cancel Axios post request outside useEffect hook

GET requests canceling fine in this example:
export default function Post (props) {
const _cancelToken = axios.CancelToken.source()
useEffect(() => {
const _loadAsyncData = async () => {
await axios.get('/post'), { cancelToken: _cancelToken.token })
}
_loadAsyncData()
return () => {
_cancelToken.cancel()
}
}, [])
return ()
}
But when I need save form via POST request, my code looks like:
export default function Form (props) {
const _cancelToken = axios.CancelToken.source()
const _zz = { qq: 'QQ' }
const handleCreate = async e => {
e.preventDefault()
_zz.qq = 'ZZ'
await axios.post('/form'), {}, { cancelToken: _cancelToken.token })
}
useEffect(() => {
return () => {
console.log(_zz.qq)
_cancelToken.cancel()
}
}, [])
return ()
}
Request not cancel and my _zz.qq always 'QQ' instead 'ZZ'. It's working fine without hooks, but I like hooks and want to use hooks for new components.
I want to cancel request when componentWillUnmount.
This is because you're losing the changes between renders. During the handleCreate call the variable changes only for that render. When the useEffect is run on a subsequent render/unmounting, you're resetting _zz to { qq: 'QQ' }. In order to get around this you need to use references.
export default function Form (props) {
const cancelToken = useRef(null)
const zz = useRef({ qq: 'QQ' })
const handleCreate = async e => {
e.preventDefault()
cancelToken.current = axios.CancelToken.source()
zz.current = { qq: 'ZZ' }
await axios.post('/form'), {}, { cancelToken: cancelToken.current.token })
}
useEffect(() => {
return () => {
console.log(zz.current) //this should now be {qq : 'ZZ'}
if (cancelToken.current) {
cancelToken.current.cancel()
}
}
}, [])
return null
}

How to cleanup async tasks created outside useEffect

I created a custom hook useFetch that returns a fetch function that I can use in other components. It uses a promise to fetch some data inside. My goal is to clean up the pending promise, if the component, that uses this custom hook gets unmounted.
How would I do it? I tried something using useRef, but without success yet. Still getting the Can't perform a React state update on an unmounted component. warning.
const useFetch = (url) => {
const [isFetching, setIsFetching] = useState(false)
const handler = useRef(null)
useEffect(() => () => {
if (handler.current !== null) {
handler.current.cancel()
}
}, [])
return (options) => {
handler.current = window.fetch(url, options)
setIsFetching(true)
return handler.current.then(() => {
handler.current = null
setIsFetching(false)
})
}
}
export default () => {
const fetchData = useFetch('www.tld')
useEffect(() => {
fetchData({}).then(() => console.log('done'))
}, [])
return null
}
Notice that the promise in this example is cancelable via .cancel() (so thats not a problem here).
Return cancel() as bound callback from your hook. Then it would be up to consumer to stop it:
const useFetch(url) {
const [isFetching, setIsFetching] = useState(false)
const handler = useRef(null)
function run(options) {
handler.current = window.fetch(url, options)
setIsFetching(true)
...
}
function cancel() {
if(handler.current) {
handler.current.cancel()
}
}
return {run, cancel}
}
...
function OtherComponent({userId}) {
const [userData, setUserData] = useState(null);
const {run, cancel} = useFetch(`/user/${userId}`);
useEffect(() => {
run(options).then(setUserData);
return cancel; // it's up to consumer code to stop request
}, [userId]);
}

Resources