Skip to content

Audiu/react-useSavedCallback

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 

Repository files navigation

react-useSavedCallback

npm version

Invoke a callback that is saved from the latest render allowing easier integration with legacy callback based external JavaScript dependencies

Usage

Install with npm or yarn

npm i react-usesavedcallback
yarn add react-usesavedcallback

Use this function instead of useCallback when you are interfacing with external JavaScript (trivial example is setInterval).

const [a, setA] = useState(1);

const func = useSavedCallback(() => {
    setA(a + 1);
    console.log(a);
}, [a]);

useEffect(() => {
    const interval = setInterval(func, 1000);

    return () => clearInterval(interval);
}, []);

Background

Working with callbacks from 3rd party dependencies and React Hooks can be a little bit confusing.

The classic example is setInterval built in.

Example:

const [a, setA] = useState(1);

useEffect(() => {
    const interval = setInterval(() => {
        setA(a + 1);
        console.log(a);
    }, 1000);

    return () => clearInterval(interval);
}, []);

The callback passed into setInterval will be the rendered function at that point in time. This means a will not change as it was collected at the point of render (when useEffect fired);

It would be nice if you could use the useCallback function to update:

const [a, setA] = useState(1);

const func = useCallback(() => {
    setA(a + 1);
    console.log(a);
}, [a]);

useEffect(() => {
    const interval = setInterval(func, 1000);

    return () => clearInterval(interval);
}, []);

The problem however is still the same. func is pointing towards the function returned by useCallback during that initial render. Of course, useCallback is great for only re-creating complex functions if a dependency changes, but it will return the function created at that point in time. It doesn't provide us a method to dynamically update the callback.

There are a few solutions to this problem, including this cool library: https://github.com/Aminadav/react-useStateRef

However, even though you can use this to access the state via the ref to get the current value, it doesn't help you when you want to propagate up the chain.

Consider you have a prop passed in, which is a callback function from this child component:

const { parentFunc } = props;

const [a, setA] = useState(1);

const func = useCallback(() => {
    setA(a + 1);
    console.log(a);

    parentFunc(a);
}, [a]);

useEffect(() => {
    const interval = setInterval(func, 1000);

    return () => clearInterval(interval);
}, []);

This callback will point to parentFunc at the point in time of this render, and therefore you will also "see" the state of the parent component at the point in time that the render was done. This means you would have to access all your state via state refs all the way up the chain and life gets messy.

Anyway, wouldn't it be great to just move the ref of the callback? Well, we can! Mix the magic of useStateRef and this blog: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ and we end up with a super simple generic hook to move the reference to the callback, instead of the reference to the state. Also, a touch of syntatic suger means we don't have to worry about .current.

Enter this hook - this is the entirety of it:

import { useCallback, useEffect, useRef } from 'react';

const useSavedCallback = (callbackFunc, deps) => {
    const savedCallback = useRef();

    const callback = useCallback(callbackFunc, deps);

    useEffect(() => {
        savedCallback.current = callback;
    }, [callback]);

    return (...params) => savedCallback.current(...params);
};

export default useSavedCallback;

All we are doing is updating the ref to the callback whenever it changes.

Just use useSavedCallback instead of useCallback

const [a, setA] = useState(1);

const func = useSavedCallback(() => {
    setA(a + 1);
    console.log(a);
}, [a]);

useEffect(() => {
    const interval = setInterval(func, 1000);

    return () => clearInterval(interval);
}, []);

This code will now behave as expected.

Thanks to this awesome blog for making this clear to me - and it has helped loads with interfacing with external callback style JavaScript from within React: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published