Skip to content

Instantly share code, notes, and snippets.

@KEIII
Last active September 7, 2021 19:20
Show Gist options
  • Save KEIII/165c9687e5a6b7de058a9e21bb41d5ce to your computer and use it in GitHub Desktop.
Save KEIII/165c9687e5a6b7de058a9e21bb41d5ce to your computer and use it in GitHub Desktop.
React hook useFactory()
import { useLayoutEffect, useRef, useState } from 'react';
export type Dispose = () => void | undefined;
export type Disposable<T> = {
instance: T;
dispose: Dispose;
};
export type Factory<Arguments extends Tuple, Instance> = (
...props: Arguments
) => Disposable<Instance>;
export type Tuple = Array<unknown> &
(
| [void]
| {
length: number;
every: (cb: (v: unknown, k: number) => boolean) => boolean;
[k: number]: unknown;
}
);
const shallowCompare = <T extends Tuple>(a: T, b: T) => {
return a.length === b.length && a.every((v, k) => v === b[k]);
};
/**
* Returns an instance from calling f(...args).
* Actually it's like useMemo() but support cleanup function.
*/
export const useFactory = <Arguments extends Tuple, Instance>(
f: Factory<Arguments, Instance>,
args: Arguments,
): Instance => {
const intoState = () => [args, f(...args)] as const;
const [[prevArgs, { instance, dispose }], setState] = useState(intoState);
// Creating a new instance on `args` changes
// eslint-disable-next-line react-hooks/exhaustive-deps
useLayoutEffect(
() => {
if (shallowCompare(args, prevArgs)) return;
dispose?.();
setState(intoState);
},
// Call each render
);
// Dispose on component destroy
// Use ref to call actual dispose function
{
const disposeRef = useRef<Dispose>();
disposeRef.current = dispose;
useLayoutEffect(() => {
return () => disposeRef.current?.();
}, []);
}
return instance;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment