Skip to content

Commit

Permalink
Import useStableCallback hook from Via
Browse files Browse the repository at this point in the history
This hook is useful for wrapping callbacks to avoid unnecessary re-renders due
to callback props changing. See hypothesis/via#1129.
  • Loading branch information
robertknight committed Nov 9, 2023
1 parent 489f5e3 commit 941ccac
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 0 deletions.
39 changes: 39 additions & 0 deletions src/hooks/test/use-stable-callback-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// These tests use Preact directly, rather than via Enzyme, to simplify debugging.
import { render } from 'preact';

import { useStableCallback } from '../use-stable-callback';

describe('useStableCallback', () => {
let container;
let stableCallbackValues;

beforeEach(() => {
container = document.createElement('div');
stableCallbackValues = [];
});

function Widget({ callback }) {
const stableCallback = useStableCallback(callback);
stableCallbackValues.push(stableCallback);
return <button onClick={stableCallback}>Test</button>;
}

it('returns a wrapper with a stable identity', () => {
render(<Widget callback={() => {}} />, container);
render(<Widget callback={() => {}} />, container);

assert.equal(stableCallbackValues.length, 2);
assert.equal(stableCallbackValues[0], stableCallbackValues[1]);
});

it('returned wrapper forwards to the latest callback', () => {
const stub = sinon.stub();
render(<Widget callback={() => {}} />, container);
render(<Widget callback={stub} />, container);

assert.equal(stableCallbackValues.length, 2);
stableCallbackValues.at(-1)('foo', 'bar', 'baz');

assert.calledWith(stub, 'foo', 'bar', 'baz');
});
});
23 changes: 23 additions & 0 deletions src/hooks/use-stable-callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useRef } from 'preact/hooks';

/**
* Return a function which wraps a callback to give it a stable value.
*
* The wrapper has a stable value across renders, but always forwards to the
* callback from the most recent render. This is useful if you want to use a
* callback inside a `useEffect` or `useMemo` hook without re-running the effect
* or re-computing the memoed value when the callback changes.
*/
export function useStableCallback<R, A extends any[], F extends (...a: A) => R>(
callback: F,
): F {
const wrapper = useRef({
callback,
call: (...args: A) => wrapper.current.callback(...args),
});

// On each render, save the last callback value.
wrapper.current.callback = callback;

return wrapper.current.call as F;
}

0 comments on commit 941ccac

Please sign in to comment.