-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add ensuredForwardRef and useEnsuredForwardedRef
- Loading branch information
Oriol Colomer Aragonés
committed
Oct 25, 2019
1 parent
a114474
commit 1bfe063
Showing
6 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |