Skip to content

Commit

Permalink
feat: add ensuredForwardRef and useEnsuredForwardedRef
Browse files Browse the repository at this point in the history
  • Loading branch information
Oriol Colomer Aragonés committed Oct 25, 2019
1 parent a114474 commit 1bfe063
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<br/>
<br/>
- [**Miscellaneous**]()
- [`useEnsuredForwardedRef`](./docs/useEnsuredForwardedRef.md) and [`ensuredForwardRef`](./docs/useEnsuredForwardedRef.md) &mdash; use a React.forwardedRef safely. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-useensuredforwardedref--demo)


<br />
<br />
Expand Down
63 changes: 63 additions & 0 deletions docs/useEnsuredForwardedRef.md
Original file line number Diff line number Diff line change
@@ -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 (
<Child />
);
};

const Child = ensuredForwardRef((props, ref) => {
useEffect(() => {
console.log(ref.current.getBoundingClientRect())
}, [])

return (
<div ref={ref} />
);
});
```

## Alternative usage

```jsx
import {useEnsuredForwardedRef} from 'react-use';

const Demo = () => {
return (
<Child />
);
};

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 (
<div ref={ensuredForwardRef} />
);
});
```

## Reference

```ts
ensuredForwardRef<T, P = {}>(Component: RefForwardingComponent<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

useEnsuredForwardedRef<T>(ref: React.MutableRefObject<T>): React.MutableRefObject<T>;
```
79 changes: 79 additions & 0 deletions src/__stories__/useEnsuredForwardedRef.story.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button onClick={handleClick} disabled={!activeForwardRef}>
{activeForwardRef ? 'Update parent component' : 'forwardRef value is undefined'}
</button>
<div>Parent component using external ref: (textarea size)</div>
<pre>{JSON.stringify(size, null, 2)}</pre>
<Child ref={activeForwardRef ? ref : undefined} />
</>
);
};

const Child = forwardRef(({}, ref: MutableRefObject<HTMLTextAreaElement>) => {
const ensuredForwardRef = useEnsuredForwardedRef(ref);

const [size, setSize] = useState(INITIAL_SIZE);

useEffect(() => {
handleMouseUp();
}, []);

const handleMouseUp = () => {
const { width, height } = ensuredForwardRef.current.getBoundingClientRect();
setSize({
width,
height,
});
};

return (
<>
<div>Child forwardRef component using forwardRef: (textarea size)</div>
<pre>{JSON.stringify(size, null, 2)}</pre>
<div>You can resize this textarea:</div>
<textarea ref={ensuredForwardRef} onMouseUp={handleMouseUp} />
</>
);
});

storiesOf('Miscellaneous|useEnsuredForwardedRef', module)
.addDecorator(withKnobs)
.add('Docs', () => <ShowDocs md={require('../../docs/useEnsuredForwardedRef.md')} />)
.add('Demo', () => {
const activeForwardRef = boolean('activeForwardRef', true);
return <Demo activeForwardRef={activeForwardRef} />;
});
53 changes: 53 additions & 0 deletions src/__tests__/useEnsuredForwardedRef.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import { renderHook } from '@testing-library/react-hooks';
import TestUtils from 'react-dom/test-utils';
import { useEnsuredForwardedRef } from '..';

let container: HTMLDivElement;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
container = null;
});

test('should return a valid ref with existing forwardedRef', () => {
const { result } = renderHook(() => {
const ref = useRef(null);
const ensuredRef = useEnsuredForwardedRef(ref);

TestUtils.act(() => {
ReactDOM.render(<div ref={ensuredRef} />, container);
});

return {
initialRef: ref,
ensuredForwardedRef: ensuredRef,
};
});

const { initialRef, ensuredForwardedRef } = result.current;

expect(ensuredForwardedRef).toStrictEqual(initialRef);
});

test('should return a valid ref when the forwarded ref is undefined', () => {
const { result } = renderHook(() => {
const ref = useEnsuredForwardedRef<HTMLDivElement>(undefined);

TestUtils.act(() => {
ReactDOM.render(<div id="test_id" ref={ref} />, container);
});

return { ensuredRef: ref };
});

const { ensuredRef } = result.current;

expect(ensuredRef.current.id).toBe('test_id');
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { default as useDefault } from './useDefault';
export { default as useDrop } from './useDrop';
export { default as useDropArea } from './useDropArea';
export { default as useEffectOnce } from './useEffectOnce';
export { default as useEnsuredForwardedRef, ensuredForwardRef } from './useEnsuredForwardedRef';
export { default as useEvent } from './useEvent';
export { default as useFavicon } from './useFavicon';
export { default as useFullscreen } from './useFullscreen';
Expand Down
33 changes: 33 additions & 0 deletions src/useEnsuredForwardedRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
forwardRef,
useRef,
useEffect,
MutableRefObject,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
RefForwardingComponent,
PropsWithChildren,
} from 'react';

export default function useEnsuredForwardedRef<T>(forwardedRef: MutableRefObject<T>): MutableRefObject<T> {
const ensuredRef = useRef(forwardedRef && forwardedRef.current);

useEffect(() => {
if (!forwardedRef) {
return;
}
forwardedRef.current = ensuredRef.current;
}, [forwardedRef]);

return ensuredRef;
}

export function ensuredForwardRef<T, P = {}>(
Component: RefForwardingComponent<T, P>
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
return forwardRef((props: PropsWithChildren<P>, ref) => {
const ensuredRef = useEnsuredForwardedRef(ref as MutableRefObject<T>);
return Component(props, ensuredRef);
});
}

0 comments on commit 1bfe063

Please sign in to comment.