Skip to content

Instantly share code, notes, and snippets.

@lvl99
Created August 29, 2019 22:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lvl99/3893b8b8f475964c2f5cb6144a41ab0c to your computer and use it in GitHub Desktop.
Save lvl99/3893b8b8f475964c2f5cb6144a41ab0c to your computer and use it in GitHub Desktop.
useCallbackRef - React hook for triggering changes after ref has been received from DOM
import React, { useState, useRef } from "react";
import { render, cleanup } from "react-testing-library";
import useCallbackRef from "./useCallbackRef";
function TestInputRef<T = HTMLInputElement>({
id = "test",
onSet,
onChange
}: {
id?: string;
onSet?: (newInput: T | null) => void;
onChange?: (newInput?: T | null, prevInput?: T | null) => void;
}) {
const [count, setCount] = useState(0);
const hasRef = useRef(false);
const [ref, setRef] = useCallbackRef<T>((newInput, prevInput) => {
setCount(count + 1);
if (newInput && !prevInput) {
hasRef.current = newInput === ref.current;
if (onSet) {
onSet(newInput);
}
} else {
if (onChange) {
onChange(newInput, prevInput);
}
}
});
return (
<div data-testid={id}>
<input data-testid="ref-input" ref={setRef} type="hidden" />
<span data-testid="has-ref">{String(hasRef.current)}</span>
<span data-testid="count">{count}</span>
</div>
);
}
beforeEach(cleanup);
it("should assign the ref using the callbackRef", () => {
const check = {
inputRef: null
};
const onSet = jest.fn(newInput => {
check.inputRef = newInput;
});
const onChange = jest.fn((newInput, prevInput) => {});
const { getByTestId } = render(
<TestInputRef onSet={onSet} onChange={onChange} />
);
expect(onSet).toHaveBeenCalled();
expect(check.inputRef).toBeInstanceOf(HTMLInputElement);
expect(onSet).toHaveBeenCalledWith(check.inputRef);
expect(onChange).not.toHaveBeenCalled();
expect(getByTestId("has-ref")).toHaveTextContent("true");
expect(getByTestId("count")).toHaveTextContent("1");
});
/**
* This allows using the hook pattern of a "callback ref".
*
* Use this when you want to do side-effects after obtaining
* a ref from somewhere in the DOM because `useEffect` is not
* ideal in that situation.
*
* See: https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780
*
* ```
* function MyComponent() {
* const [ref, setRef] = useCallbackRef((newInput, prevInput) => {
* if (newInput && !prevInput) {
* // This is where you can put your effects after
* // receiving the input for the first time.
* } else {
* // If the input ever changes you can clean
* // up things related to the previous input here.
* }
* });
*
* useEffect(() => {
* return () => {
* // If you need to clean-up on unmount do it here.
* // The following assumes your ref has a method of `destroy()`:
* ref.current.destroy();
* }
* });
*
* return <div ref={setRef} />
* }
* ```
*/
import { useRef, useCallback } from "react";
export type OnInput<T = any> = (
newInput?: T | null,
prevInput?: T | null
) => void;
export default function useCallbackRef<T = any>(
cb: OnInput<T>
): [React.RefObject<T | null>, React.Ref<T>] {
const ref = useRef<T | null>(null);
const setRef = useCallback<
(newInput: T | null) => React.MutableRefObject<T | null>
>(newInput => {
if (ref.current !== newInput) {
const prevInput = ref.current;
ref.current = newInput;
if (!!cb) {
cb(newInput, prevInput);
}
}
return ref;
}, []);
return [ref, setRef];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment