From 1bfe0636643a519bcbc3e620d9e384b4870a5a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oriol=20Colomer=20Aragon=C3=A9s?= Date: Thu, 24 Oct 2019 16:27:38 +0200 Subject: [PATCH] feat: add ensuredForwardRef and useEnsuredForwardedRef --- README.md | 5 ++ docs/useEnsuredForwardedRef.md | 63 +++++++++++++++ .../useEnsuredForwardedRef.story.tsx | 79 +++++++++++++++++++ src/__tests__/useEnsuredForwardedRef.test.tsx | 53 +++++++++++++ src/index.ts | 1 + src/useEnsuredForwardedRef.ts | 33 ++++++++ 6 files changed, 234 insertions(+) create mode 100644 docs/useEnsuredForwardedRef.md create mode 100644 src/__stories__/useEnsuredForwardedRef.story.tsx create mode 100644 src/__tests__/useEnsuredForwardedRef.test.tsx create mode 100644 src/useEnsuredForwardedRef.ts diff --git a/README.md b/README.md index 76fd92c187..d93ee9a387 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,11 @@ - [`useStateValidator`](./docs/useStateValidator.md) — tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo) - [`useMultiStateValidator`](./docs/useMultiStateValidator.md) — alike the `useStateValidator`, but tracks multiple states at a time. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemultistatevalidator--demo) - [`useMediatedState`](./docs/useMediatedState.md) — like the regular `useState` but with mediation by custom function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemediatedstate--demo) +
+
+- [**Miscellaneous**]() + - [`useEnsuredForwardedRef`](./docs/useEnsuredForwardedRef.md) and [`ensuredForwardRef`](./docs/useEnsuredForwardedRef.md) — use a React.forwardedRef safely. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-useensuredforwardedref--demo) +

diff --git a/docs/useEnsuredForwardedRef.md b/docs/useEnsuredForwardedRef.md new file mode 100644 index 0000000000..b094a284bc --- /dev/null +++ b/docs/useEnsuredForwardedRef.md @@ -0,0 +1,63 @@ +# `useEnsuredForwardedRef` + +React hook to use a ForwardedRef safely. + +In some scenarios, you may need to use a _ref_ from inside and outside a component. If that's the case, you should use `React.forwardRef` to pass it through the child component. This is useful when you only want to forward that _ref_ and expose an internal `HTMLelement` to a parent component, for example. However, if you need to manipulate that reference inside a child's lifecycle hook... things get complicated, since you can't always ensure that the _ref_ is being sent by the parent component and if it is not, you will get `undefined` instead of a valid _ref_. + +This hook is useful in this specific case, it will __ensure__ that you get a valid reference on the other side. + +## Usage + +```jsx +import {ensuredForwardRef} from 'react-use'; + +const Demo = () => { + return ( + + ); +}; + +const Child = ensuredForwardRef((props, ref) => { + useEffect(() => { + console.log(ref.current.getBoundingClientRect()) + }, []) + + return ( +
+ ); +}); +``` + +## Alternative usage + +```jsx +import {useEnsuredForwardedRef} from 'react-use'; + +const Demo = () => { + return ( + + ); +}; + +const Child = React.forwardRef((props, ref) => { + // Here `ref` is undefined + const ensuredForwardRef = useEnsuredForwardedRef(ref); + // ensuredForwardRef will always be a valid reference. + + useEffect(() => { + console.log(ensuredForwardRef.current.getBoundingClientRect()) + }, []) + + return ( +
+ ); +}); +``` + +## Reference + +```ts +ensuredForwardRef(Component: RefForwardingComponent): ForwardRefExoticComponent & RefAttributes>; + +useEnsuredForwardedRef(ref: React.MutableRefObject): React.MutableRefObject; +``` diff --git a/src/__stories__/useEnsuredForwardedRef.story.tsx b/src/__stories__/useEnsuredForwardedRef.story.tsx new file mode 100644 index 0000000000..b2623f2fc4 --- /dev/null +++ b/src/__stories__/useEnsuredForwardedRef.story.tsx @@ -0,0 +1,79 @@ +import { storiesOf } from '@storybook/react'; +import React, { forwardRef, useRef, useState, useEffect, MutableRefObject } from 'react'; +import { useEnsuredForwardedRef } from '..'; +import ShowDocs from './util/ShowDocs'; + +import { boolean, withKnobs } from '@storybook/addon-knobs'; + +const INITIAL_SIZE = { + width: null, + height: null, +}; + +const Demo = ({ activeForwardRef }) => { + const ref = useRef(null); + + const [size, setSize] = useState(INITIAL_SIZE); + + useEffect(() => { + handleClick(); + }, [activeForwardRef]); + + const handleClick = () => { + if (activeForwardRef) { + const { width, height } = ref.current.getBoundingClientRect(); + setSize({ + width, + height, + }); + } else { + setSize(INITIAL_SIZE); + } + }; + + return ( + <> + +
Parent component using external ref: (textarea size)
+
{JSON.stringify(size, null, 2)}
+ + + ); +}; + +const Child = forwardRef(({}, ref: MutableRefObject) => { + const ensuredForwardRef = useEnsuredForwardedRef(ref); + + const [size, setSize] = useState(INITIAL_SIZE); + + useEffect(() => { + handleMouseUp(); + }, []); + + const handleMouseUp = () => { + const { width, height } = ensuredForwardRef.current.getBoundingClientRect(); + setSize({ + width, + height, + }); + }; + + return ( + <> +
Child forwardRef component using forwardRef: (textarea size)
+
{JSON.stringify(size, null, 2)}
+
You can resize this textarea:
+