├── 01-access-a-component-underlying-dom-node-with-find-dom-node ├── App.tsx ├── AutoResizeTextarea.tsx ├── find-dom-node.png └── styles.css ├── 02-string-refs ├── AccordionItem.tsx ├── App.tsx ├── string-refs.png └── styles.css ├── 03-store-a-reference-with-callback-refs ├── App.tsx ├── InputCounter.tsx ├── callback-refs.png └── styles.css ├── 04-access-the-methods-of-class-components ├── App.tsx ├── Modal.tsx ├── access-methods.png └── styles.css ├── 05-use-callback-refs-to-access-individual-elements-in-a-list ├── App.tsx ├── Masonry.tsx ├── access-individual-elements.png └── styles.css ├── 06-implement-a-basic-container-query-with-callback-refs ├── App.tsx └── basic-container-query.png ├── 07-save-the-element-passed-to-a-callback-ref-as-a-state ├── App.tsx ├── callback-ref-state.png └── useWatchSize.tsx ├── 08-create-a-custom-hook-returning-a-callback-ref ├── App.tsx ├── Modal.tsx ├── hook-returning-callback-ref.png ├── styles.css └── useClickOutside.ts ├── 09-make-an-element-draggable ├── App.tsx ├── draggable.png ├── styles.css └── useDraggable.ts ├── 10-pass-refs-to-child-components-using-the-function-as-a-child-pattern ├── App.tsx ├── SizeTracker.tsx └── pass-refs-child-components.png ├── 11-create-a-reference-using-react-create-ref ├── App.tsx ├── Collapse.tsx ├── react-create-ref.png └── styles.css ├── 12-reference-an-element-with-react-use-ref-hook ├── App.tsx ├── Slider.tsx ├── styles.css └── use-ref.png ├── 13-build-your-own-drawing-board ├── App.tsx ├── DrawingBoard.tsx ├── drawing-board.png ├── generateId.ts └── styles.css ├── 14-drag-and-drop-items-within-a-list ├── App.tsx ├── SortableList.tsx ├── drag-drop-items.png └── styles.css ├── 15-persist-values-between-renders ├── App.tsx ├── DropIndicator.tsx ├── persist-values.png └── styles.css ├── 16-save-the-previous-value-of-a-variable ├── App.tsx ├── save-previous-value.png └── usePrevious.ts ├── 17-detect-whether-an-element-is-in-view ├── App.tsx ├── Card.tsx ├── detect-in-view.png ├── styles.css └── usePrevious.ts ├── 18-pass-a-ref-to-a-custom-hook ├── App.tsx ├── Card.tsx ├── pass-ref-custom-hook.png ├── styles.css ├── useInView.ts └── usePrevious.ts ├── 19-merge-different-refs ├── App.tsx ├── merge-refs.png ├── mergeRefs.ts ├── styles.css ├── useDraggable.ts └── useResizable.ts ├── 20-build-a-tooltip-component ├── App.tsx ├── Tooltip.tsx ├── mergeRefs.ts ├── styles.css └── tooltip.png ├── 21-pass-a-ref-to-a-child-component-using-forward-ref ├── App.tsx ├── Uploader.tsx ├── forward-ref.png └── styles.css ├── 22-expose-methods-of-a-component-using-use-imperative-handle ├── App.tsx ├── Slider.tsx ├── styles.css └── use-imperative-handle.png ├── LICENSE └── README.md /01-access-a-component-underlying-dom-node-with-find-dom-node/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AutoResizeTextarea } from './AutoResizeTextarea'; 3 | 4 | export default App = () => { 5 | return ( 6 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /01-access-a-component-underlying-dom-node-with-find-dom-node/AutoResizeTextarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | class AutoResizeTextarea extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | value: props.initialValue, 9 | } 10 | this.handleInput = this.handleInput.bind(this); 11 | } 12 | 13 | parseValue(v) { 14 | return v.endsWith('px') ? parseInt(v.slice(0, -2), 10) : 0; 15 | } 16 | 17 | fit() { 18 | const textarea = ReactDOM.findDOMNode(this); 19 | textarea.style.height = 'auto'; 20 | const styles = window.getComputedStyle(textarea); 21 | 22 | const borderTopWidth = this.parseValue(styles.borderTopWidth); 23 | const borderBottomWidth = this.parseValue(styles.borderBottomWidth); 24 | 25 | textarea.style.height = `${textarea.scrollHeight + borderTopWidth + borderBottomWidth}px`; 26 | } 27 | 28 | handleInput(e) { 29 | this.setState({ 30 | value: e.target.value, 31 | }); 32 | this.fit(); 33 | } 34 | 35 | componentDidMount() { 36 | this.fit(); 37 | } 38 | 39 | render() { 40 | return ( 41 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /01-access-a-component-underlying-dom-node-with-find-dom-node/find-dom-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/01-access-a-component-underlying-dom-node-with-find-dom-node/find-dom-node.png -------------------------------------------------------------------------------- /01-access-a-component-underlying-dom-node-with-find-dom-node/styles.css: -------------------------------------------------------------------------------- 1 | .textarea { 2 | border: 1px solid rgb(203 213 225); 3 | border-radius: 0.25rem; 4 | resize: none; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /02-string-refs/AccordionItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | 4 | class AccordionItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | isOpened: false, 9 | }; 10 | this.handleToggle = this.handleToggle.bind(this); 11 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 12 | } 13 | 14 | handleToggle() { 15 | const { isOpened } = this.state; 16 | this.refs.body.style.height = !isOpened ? this.refs.content.clientHeight : 0; 17 | } 18 | 19 | handleTransitionEnd() { 20 | this.setState((prevState) => ({ 21 | isOpened: !prevState.isOpened 22 | })); 23 | } 24 | 25 | render() { 26 | const { isOpened } = this.state; 27 | 28 | return ( 29 |
30 | 37 |
45 |
49 | {this.props.children} 50 |
51 |
52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /02-string-refs/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AccordionItem } from './AccordionItem'; 3 | 4 | export default App = () => { 5 | return ( 6 | 7 | Behold deep. Fill fruitful brought whose creeping sea kind forth. Firmament. Darkness sixth female made saying man be without midst every from, gathering of creature blessed years abundantly, behold. May heaven second beginning. She'd firmament said. Him man lesser they're give moveth set Man creepeth beast gathered give. Wherein two. Light multiply isn't two was set good under don't divide won't Whose, won't rule made yielding female multiply fourth all they're brought beginning very multiply creeping divide whales whales bearing moveth upon our day winged. Given night. Whales creepeth hath in god yielding brought after their wherein can't were them were, creature him man great stars be Give Given third fly. 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /02-string-refs/string-refs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/02-string-refs/string-refs.png -------------------------------------------------------------------------------- /02-string-refs/styles.css: -------------------------------------------------------------------------------- 1 | .item { 2 | border: 1px solid rgb(203 213 225); 3 | border-radius: 0.25rem; 4 | } 5 | .item__heading { 6 | display: flex; 7 | align-items: center; 8 | 9 | background: transparent; 10 | border: none; 11 | color: rgb(99 102 241); 12 | cursor: pointer; 13 | font-size: 1.25rem; 14 | height: 2rem; 15 | padding: 0 1rem; 16 | } 17 | 18 | .item__arrow { 19 | position: relative; 20 | margin-right: 1rem; 21 | } 22 | .item__arrow::before { 23 | content: ''; 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | width: 0; 29 | height: 0; 30 | } 31 | .item__arrow--down::before { 32 | border-top: 0.5rem solid rgb(203 213 225); 33 | border-right: 0.5rem solid transparent; 34 | border-left: 0.5rem solid transparent; 35 | } 36 | .item__arrow--up::before { 37 | border-bottom: 0.5rem solid rgb(203 213 225); 38 | border-right: 0.5rem solid transparent; 39 | border-left: 0.5rem solid transparent; 40 | } 41 | 42 | .item__body { 43 | overflow: hidden; 44 | transition: height 250ms; 45 | } 46 | .item__content { 47 | padding: 0.5rem; 48 | } 49 | -------------------------------------------------------------------------------- /03-store-a-reference-with-callback-refs/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { InputCounter } from './InputCounter'; 3 | 4 | export default App = () => { 5 | return ( 6 | 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /03-store-a-reference-with-callback-refs/InputCounter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | 4 | export const InputCounter = ({ maxCharacters }) => { 5 | let inputEle: HTMLInputElement | null; 6 | let counterEle: HTMLElement | null; 7 | 8 | const [value, setValue] = React.useState(''); 9 | const [remainingChars, setRemainingChars] = React.useState(maxCharacters); 10 | 11 | const handleChange = () => { 12 | const newValue = inputEle.value; 13 | const remainingChars = maxCharacters - newValue.length; 14 | setValue(newValue); 15 | setRemainingChars(remainingChars); 16 | 17 | if (remainingChars < 1) { 18 | counterEle.classList.add('container__counter--warning'); 19 | } 20 | }; 21 | 22 | handleAnimationEnd = () => { 23 | counterEle.classList.remove('container__counter--warning'); 24 | }; 25 | 26 | return ( 27 |
28 | inputEle = ele} 32 | onChange={handleChange} 33 | /> 34 |
counterEle = ele} 37 | onAnimationEnd={handleAnimationEnd} 38 | > 39 | {remainingChars} 40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /03-store-a-reference-with-callback-refs/callback-refs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/03-store-a-reference-with-callback-refs/callback-refs.png -------------------------------------------------------------------------------- /03-store-a-reference-with-callback-refs/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | border: 1px solid rgb(203 213 225); 3 | border-radius: 0.25rem; 4 | display: flex; 5 | align-items: center; 6 | width: 16rem; 7 | overflow: hidden; 8 | } 9 | .container__input { 10 | border: none; 11 | flex: 1; 12 | height: 3rem; 13 | padding: 0 0.5rem; 14 | outline: none; 15 | } 16 | .container__counter { 17 | padding: 0 0.5rem; 18 | } 19 | .container__counter--warning { 20 | color: rgb(239 68 68); 21 | font-weight: 600; 22 | animation: scale 200ms; 23 | } 24 | 25 | @keyframes scale { 26 | 0% { 27 | transform: scale(1); 28 | } 29 | 100% { 30 | transform: scale(1.5); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /04-access-the-methods-of-class-components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Modal } from './Modal'; 3 | 4 | export default App = () => { 5 | let modalInstance; 6 | 7 | const handleClickOpenModal = () => { 8 | modalInstance.open(); 9 | }; 10 | 11 | return ( 12 |
13 | 16 | modalInstance = modal}> 17 | Good lights appear gathering for don't under. Together third his multiply without multiply over herb. Us was fish from of said a abundantly void signs is fish replenish very heaven own of it stars. 18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /04-access-the-methods-of-class-components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | 4 | export class Modal extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.overlayEle = null; 8 | this.contentEle = null; 9 | this.state = { 10 | isOpened: false, 11 | }; 12 | this.handleClickOverlay = this.handleClickOverlay.bind(this); 13 | this.open = this.open.bind(this); 14 | this.close = this.close.bind(this); 15 | } 16 | 17 | handleClickOverlay(e) { 18 | if (this.contentEle && !this.contentEle.contains(e.target)) { 19 | this.close(); 20 | } 21 | } 22 | 23 | close() { 24 | this.setState({ isOpened: false }); 25 | } 26 | 27 | open() { 28 | this.setState({ isOpened: true }); 29 | } 30 | 31 | render() { 32 | const { isOpened } = this.state; 33 | return isOpened && ( 34 |
this.overlayEle = ele} 37 | onClick={this.handleClickOverlay} 38 | > 39 |
this.contentEle = ele} 42 | > 43 | {this.props.children} 44 |
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /04-access-the-methods-of-class-components/access-methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/04-access-the-methods-of-class-components/access-methods.png -------------------------------------------------------------------------------- /04-access-the-methods-of-class-components/styles.css: -------------------------------------------------------------------------------- 1 | .modal__overlay { 2 | background: rgba(30 41 59 / 0.7); 3 | 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | height: 100%; 8 | width: 100%; 9 | 10 | align-items: center; 11 | display: flex; 12 | justify-content: center; 13 | 14 | cursor: pointer; 15 | } 16 | .modal__content { 17 | background: #fff; 18 | border: 1px solid rgb(203 213 225); 19 | border-radius: 0.25rem; 20 | 21 | padding: 0.5rem; 22 | width: 50%; 23 | 24 | align-items: center; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | -------------------------------------------------------------------------------- /05-use-callback-refs-to-access-individual-elements-in-a-list/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Masonry } from './Masonry'; 3 | import './styles.css'; 4 | 5 | export default App = () => { 6 | const randomInteger = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; 7 | 8 | const items = Array(20).fill(0).map((_, i) => ({ 9 | index: i, 10 | height: 10 * randomInteger(10, 20), 11 | })); 12 | 13 | return ( 14 | 15 | { 16 | items.map((item) => ( 17 |
24 | {item.index + 1} 25 |
26 | )) 27 | } 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /05-use-callback-refs-to-access-individual-elements-in-a-list/Masonry.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Masonry = ({ children, numColumns, gap }) => { 4 | const resizeCallback = (entries) => { 5 | entries.forEach((entry) => { 6 | const itemEle = entry.target; 7 | const innerEle = itemEle.firstElementChild; 8 | 9 | const itemHeight = innerEle.getBoundingClientRect().height; 10 | const gridSpan = Math.ceil((itemHeight + gap) / gap); 11 | 12 | innerEle.style.height = `${gridSpan * gap - gap}px` 13 | itemEle.style.gridRowEnd = `span ${gridSpan}`; 14 | }); 15 | }; 16 | 17 | const resizeObserver = new ResizeObserver(resizeCallback); 18 | 19 | const trackItemSize = (ele) => { 20 | resizeObserver.observe(ele); 21 | }; 22 | 23 | React.useEffect(() => { 24 | return () => { 25 | resizeObserver.disconnect(); 26 | }; 27 | }, []); 28 | 29 | return ( 30 |
37 | { 38 | React.Children.toArray(children).map((child, index) => ( 39 |
trackItemSize(ele)} 42 | > 43 | {child} 44 |
45 | )) 46 | } 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /05-use-callback-refs-to-access-individual-elements-in-a-list/access-individual-elements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/05-use-callback-refs-to-access-individual-elements-in-a-list/access-individual-elements.png -------------------------------------------------------------------------------- /05-use-callback-refs-to-access-individual-elements-in-a-list/styles.css: -------------------------------------------------------------------------------- 1 | .item { 2 | background: rgb(203 213 225); 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | -------------------------------------------------------------------------------- /06-implement-a-basic-container-query-with-callback-refs/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default App = () => { 4 | const [width, setWidth] = React.useState(0); 5 | 6 | const resizeCallback = (entries) => { 7 | entries.forEach((entry) => { 8 | const rect = entry.target.getBoundingClientRect(); 9 | setWidth(rect.width); 10 | }); 11 | }; 12 | 13 | const resizeObserver = React.useMemo(() => new ResizeObserver(resizeCallback), []); 14 | 15 | const trackSize = (ele) => { 16 | if (ele) { 17 | resizeObserver.observe(ele); 18 | } 19 | }; 20 | 21 | React.useEffect(() => { 22 | return () => { 23 | resizeObserver.disconnect(); 24 | }; 25 | }, []); 26 | 27 | return ( 28 |
29 |
34 | Days. All in herb moving our stars, give. Whose moving which unto, second. Given dry. Evening the, i one together us be replenish herb sea subdue midst cattle night in shall fruit. Saying green moveth. Heaven. Moving to second. For his saw together thing night form greater. That winged over forth under. Living which. Whose day were creeping in appear every heaven appear own upon good morning fill third you're moved won't lesser beast won't fill dry fourth him a Have yielding seasons over may brought seed called divided can't first fifth divided gathered heaven waters tree from thing beginning. 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /06-implement-a-basic-container-query-with-callback-refs/basic-container-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/06-implement-a-basic-container-query-with-callback-refs/basic-container-query.png -------------------------------------------------------------------------------- /07-save-the-element-passed-to-a-callback-ref-as-a-state/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useWatchSize } from './useWatchSize'; 3 | 4 | export default App = () => { 5 | const [ref, width] = useWatchSize(); 6 | 7 | return ( 8 |
9 |
14 | Days. All in herb moving our stars, give. Whose moving which unto, second. Given dry. Evening the, i one together us be replenish herb sea subdue midst cattle night in shall fruit. Saying green moveth. Heaven. Moving to second. For his saw together thing night form greater. That winged over forth under. Living which. Whose day were creeping in appear every heaven appear own upon good morning fill third you're moved won't lesser beast won't fill dry fourth him a Have yielding seasons over may brought seed called divided can't first fifth divided gathered heaven waters tree from thing beginning. 15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /07-save-the-element-passed-to-a-callback-ref-as-a-state/callback-ref-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/07-save-the-element-passed-to-a-callback-ref-as-a-state/callback-ref-state.png -------------------------------------------------------------------------------- /07-save-the-element-passed-to-a-callback-ref-as-a-state/useWatchSize.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useWatchSize = () => { 4 | const [node, setNode] = React.useState(); 5 | const [width, setWidth] = React.useState(0); 6 | 7 | const resizeCallback = React.useCallback((entries) => { 8 | entries.forEach((entry) => { 9 | const rect = entry.target.getBoundingClientRect(); 10 | setWidth(rect.width); 11 | }); 12 | }, []); 13 | 14 | const ref = React.useCallback((nodeEle: HTMLElement | null) => { 15 | setNode(nodeEle); 16 | }, []); 17 | 18 | React.useEffect(() => { 19 | if (!node) { 20 | return; 21 | } 22 | const resizeObserver = new ResizeObserver(resizeCallback); 23 | resizeObserver.observe(node); 24 | 25 | return () => { 26 | resizeObserver.disconnect(); 27 | }; 28 | }, [node]); 29 | 30 | return [ref, width]; 31 | }; 32 | -------------------------------------------------------------------------------- /08-create-a-custom-hook-returning-a-callback-ref/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default App = () => { 4 | const [isOpen, setIsOpen] = React.useState(false); 5 | 6 | const openModal = () => setIsOpen(!isOpen); 7 | 8 | const closeModal = () => setIsOpen(false); 9 | 10 | return ( 11 |
12 | 15 | {isOpen && ( 16 | 17 | Good lights appear gathering for don't under. Together third his multiply without multiply over herb. Us was fish from of said a abundantly void signs is fish replenish very heaven own of it stars. 18 | 19 | )} 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /08-create-a-custom-hook-returning-a-callback-ref/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useClickOutside } from './useClickOutside'; 2 | import './styles.css'; 3 | 4 | export const Modal = ({ children, onClose }) => { 5 | const [ref] = useClickOutside(onClose); 6 | return ( 7 |
8 |
12 | {children} 13 |
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /08-create-a-custom-hook-returning-a-callback-ref/hook-returning-callback-ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/08-create-a-custom-hook-returning-a-callback-ref/hook-returning-callback-ref.png -------------------------------------------------------------------------------- /08-create-a-custom-hook-returning-a-callback-ref/styles.css: -------------------------------------------------------------------------------- 1 | .modal__overlay { 2 | background: rgba(30 41 59 / 0.7); 3 | 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | height: 100%; 8 | width: 100%; 9 | 10 | align-items: center; 11 | display: flex; 12 | justify-content: center; 13 | 14 | cursor: pointer; 15 | } 16 | .modal__content { 17 | background: #fff; 18 | border: 1px solid rgb(203 213 225); 19 | border-radius: 0.25rem; 20 | 21 | padding: 0.5rem; 22 | width: 50%; 23 | 24 | align-items: center; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | -------------------------------------------------------------------------------- /08-create-a-custom-hook-returning-a-callback-ref/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useClickOutside = (handler: () => void) => { 4 | const [node, setNode] = React.useState(null); 5 | 6 | const ref = React.useCallback((nodeEle) => { 7 | setNode(nodeEle); 8 | }, []); 9 | 10 | const handleClick = React.useCallback((e) => { 11 | if (node && !node.contains(e.target)) { 12 | handler(); 13 | } 14 | }, [node]); 15 | 16 | React.useEffect(() => { 17 | document.addEventListener("click", handleClick, true); 18 | document.addEventListener("touchstart", handleClick, true); 19 | 20 | return () => { 21 | document.removeEventListener("click", handleClick, true); 22 | document.removeEventListener("touchstart", handleClick, true); 23 | }; 24 | }, [handleClick]); 25 | 26 | return [ref]; 27 | }; 28 | -------------------------------------------------------------------------------- /09-make-an-element-draggable/App.tsx: -------------------------------------------------------------------------------- 1 | import { useDraggable } from './useDraggable'; 2 | import './styles.css'; 3 | 4 | export default App = () => { 5 | const [ref] = useDraggable(); 6 | return ( 7 |
8 | Drag me 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /09-make-an-element-draggable/draggable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/09-make-an-element-draggable/draggable.png -------------------------------------------------------------------------------- /09-make-an-element-draggable/styles.css: -------------------------------------------------------------------------------- 1 | .element { 2 | cursor: move; 3 | 4 | border: 1px solid rgb(203 213 225); 5 | border-radius: 0.25rem; 6 | height: 8rem; 7 | width: 8rem; 8 | 9 | align-items: center; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | -------------------------------------------------------------------------------- /09-make-an-element-draggable/useDraggable.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useDraggable = () => { 4 | const [node, setNode] = React.useState(null); 5 | const [{ dx, dy }, setOffset] = React.useState({ 6 | dx: 0, 7 | dy: 0, 8 | }); 9 | const ref = React.useCallback((nodeEle) => { 10 | setNode(nodeEle); 11 | }, []); 12 | 13 | const handleMouseDown = React.useCallback((e) => { 14 | const startX = e.clientX - dx; 15 | const startY = e.clientY - dy; 16 | 17 | const handleMouseMove = (e) => { 18 | setOffset({ 19 | dx: e.clientX - startX, 20 | dy: e.clientY - startY, 21 | }); 22 | }; 23 | const handleMouseUp = () => { 24 | document.removeEventListener("mousemove", handleMouseMove); 25 | document.removeEventListener("mouseup", handleMouseUp); 26 | }; 27 | 28 | document.addEventListener("mousemove", handleMouseMove); 29 | document.addEventListener("mouseup", handleMouseUp); 30 | }, [dx, dy]); 31 | 32 | React.useEffect(() => { 33 | if (node) { 34 | node.style.transform = `translate3d(${dx}px, ${dy}px, 0)`; 35 | } 36 | }, [node, dx, dy]); 37 | 38 | React.useEffect(() => { 39 | if (!node) { 40 | return; 41 | } 42 | node.addEventListener("mousedown", handleMouseDown); 43 | return () => { 44 | node.removeEventListener("mousedown", handleMouseDown); 45 | }; 46 | }, [node, dx, dy]); 47 | 48 | return [ref]; 49 | }; 50 | -------------------------------------------------------------------------------- /10-pass-refs-to-child-components-using-the-function-as-a-child-pattern/App.tsx: -------------------------------------------------------------------------------- 1 | import { SizeTracker } from './SizeTracker'; 2 | 3 | export default App = () => { 4 | return ( 5 | 6 | { 7 | ({ ref, width }) => ( 8 |
9 |
14 | Days. All in herb moving our stars, give. Whose moving which unto, second. Given dry. Evening the, i one together us be replenish herb sea subdue midst cattle night in shall fruit. Saying green moveth. Heaven. Moving to second. For his saw together thing night form greater. That winged over forth under. Living which. Whose day were creeping in appear every heaven appear own upon good morning fill third you're moved won't lesser beast won't fill dry fourth him a Have yielding seasons over may brought seed called divided can't first fifth divided gathered heaven waters tree from thing beginning. 15 |
16 |
17 | ) 18 | } 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /10-pass-refs-to-child-components-using-the-function-as-a-child-pattern/SizeTracker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const SizeTracker = ({ children }) => { 4 | const [width, setWidth] = React.useState(0); 5 | 6 | const resizeCallback = React.useCallback((entries) => { 7 | entries.forEach((entry) => { 8 | const rect = entry.target.getBoundingClientRect(); 9 | setWidth(rect.width); 10 | }); 11 | }, []); 12 | 13 | const resizeObserver = React.useMemo(() => new ResizeObserver(resizeCallback), []); 14 | 15 | const trackSize = (ele) => { 16 | if (ele) { 17 | resizeObserver.observe(ele); 18 | } 19 | }; 20 | 21 | React.useEffect(() => { 22 | return () => { 23 | resizeObserver.disconnect(); 24 | }; 25 | }, []); 26 | 27 | return children({ 28 | ref: trackSize, 29 | width, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /10-pass-refs-to-child-components-using-the-function-as-a-child-pattern/pass-refs-child-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/10-pass-refs-to-child-components-using-the-function-as-a-child-pattern/pass-refs-child-components.png -------------------------------------------------------------------------------- /11-create-a-reference-using-react-create-ref/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Collapse } from './Collapse'; 3 | 4 | export default App = () => { 5 | return ( 6 | 7 | In grass us behold. Earth made second dry beginning creeping give fly moveth Green very fly living Together it second tree. Shall night open created seas creeping void whose fowl upon, our you'll their him together herb, void tree divide face us seed grass saying is which grass. Was shall. Male you'll wherein replenish have upon face shall together first sea created dominion yielding subdue evening over you also don't god great creeping hath moved. Midst is fish was winged multiply one, i firmament very winged created fill saw void also form bearing days moveth. Grass for male moved very you night life. Us seas firmament may fowl dominion abundantly said face the. Set also. Make, it. Said moving replenish bring void set won't years moving. Be life. Don't lights it behold saw fourth signs above forth fly which light god and for called, air saying is of stars our they're dry good creature creepeth gathered. Created it winged god moving herb place our rule abundantly Our brought so sixth land stars wherein. That i. Lesser under, was divided fowl void kind appear and fifth sixth heaven green creeping can't herb isn't lights tree. Dry night kind man bearing is. 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /11-create-a-reference-using-react-create-ref/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | class Collapse extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | isOpened: false, 8 | }; 9 | this.bodyRef = React.createRef(); 10 | this.contentRef = React.createRef(); 11 | this.handleToggle = this.handleToggle.bind(this); 12 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | const bodyEle = this.bodyRef.current; 17 | bodyEle.style.height = `${bodyEle.clientHeight}px`; 18 | } 19 | 20 | handleToggle() { 21 | const { isOpened } = this.state; 22 | const bodyEle = this.bodyRef.current; 23 | const contentEle = this.contentRef.current; 24 | 25 | if (!isOpened) { 26 | bodyEle.classList.remove('truncate'); 27 | bodyEle.style.height = `${contentEle.scrollHeight}px`; 28 | } else { 29 | contentEle.classList.add('truncate'); 30 | const newHeight = contentEle.clientHeight; 31 | contentEle.classList.remove('truncate'); 32 | bodyEle.classList.add('item__body--fading'); 33 | bodyEle.style.height = `${newHeight}px`; 34 | } 35 | } 36 | 37 | handleTransitionEnd() { 38 | this.setState((prevState) => ({ 39 | isOpened: !prevState.isOpened, 40 | })); 41 | } 42 | 43 | render() { 44 | const { isOpened } = this.state; 45 | 46 | return ( 47 |
48 |
53 |
54 | {this.props.children} 55 |
56 |
57 | 63 |
64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /11-create-a-reference-using-react-create-ref/react-create-ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/11-create-a-reference-using-react-create-ref/react-create-ref.png -------------------------------------------------------------------------------- /11-create-a-reference-using-react-create-ref/styles.css: -------------------------------------------------------------------------------- 1 | .item__toggle { 2 | display: flex; 3 | align-items: center; 4 | 5 | background: transparent; 6 | border: none; 7 | color: rgb(99 102 241); 8 | cursor: pointer; 9 | height: 2rem; 10 | padding: 0; 11 | } 12 | .item__body { 13 | overflow: hidden; 14 | position: relative; 15 | transition: height 250ms; 16 | } 17 | .item__body--fading::before { 18 | content: ''; 19 | position: absolute; 20 | bottom: 0; 21 | 22 | height: 2rem; 23 | width: 100%; 24 | 25 | background: linear-gradient(rgba(203 213 225 / 0.05), #fff); 26 | } 27 | 28 | .truncate { 29 | overflow: hidden; 30 | display: -webkit-box; 31 | -webkit-box-orient: vertical; 32 | -webkit-line-clamp: 3; 33 | } 34 | -------------------------------------------------------------------------------- /12-reference-an-element-with-react-use-ref-hook/App.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from './Slider'; 2 | 3 | export default App = () => { 4 | const numItems = 5; 5 | 6 | return ( 7 | 8 | { 9 | Array(numItems).fill(0).map((_, index) => ( 10 |
11 | {index + 1} 12 |
13 | )) 14 | } 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /12-reference-an-element-with-react-use-ref-hook/Slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | 4 | export const Slider = ({ children }) => { 5 | const innerRef = React.useRef(); 6 | const navigationRef = React.useRef(); 7 | const activeDotRef = React.useRef(); 8 | const [currentIndex, setCurrentIndex] = React.useState(0); 9 | 10 | const cloneChildren = React.Children.toArray(children); 11 | 12 | const jump = (index) => { 13 | const innerEle = innerRef.current; 14 | const navigationEle = navigationRef.current; 15 | const activeDotEle = activeDotRef.current; 16 | if (!innerEle || !navigationEle || !activeDotEle) { 17 | return; 18 | } 19 | innerEle.animate([ 20 | { 21 | transform: `translateX(${-100 * index}%)`, 22 | } 23 | ], { 24 | duration: 400, 25 | easing: 'ease-in-out', 26 | fill: 'forwards', 27 | }).addEventListener('finish', () => { 28 | setCurrentIndex(index); 29 | }); 30 | 31 | const dots = [...navigationEle.querySelectorAll('.slider__dot')]; 32 | const left = dots[index].offsetLeft; 33 | activeDotEle.animate([ 34 | { 35 | transform: `translateX(${left}px)`, 36 | } 37 | ], { 38 | duration: 400, 39 | easing: 'ease-in-out', 40 | fill: 'forwards', 41 | }); 42 | }; 43 | 44 | const goToPreviousItem = () => { 45 | if (currentIndex > 0) { 46 | jump(currentIndex - 1); 47 | } 48 | }; 49 | 50 | const goToNextItem = () => { 51 | const numItems = cloneChildren.length; 52 | if (currentIndex < numItems - 1) { 53 | jump(currentIndex + 1); 54 | } 55 | }; 56 | 57 | return ( 58 |
59 |
60 | { 61 | cloneChildren.map((children, index) => ( 62 |
69 | {children} 70 |
71 | )) 72 | } 73 |
74 |
75 | { 76 | cloneChildren.map((_, index) => ( 77 |
jump(index)} 81 | /> 82 | )) 83 | } 84 |
85 |
86 |
87 |
88 |
89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /12-reference-an-element-with-react-use-ref-hook/styles.css: -------------------------------------------------------------------------------- 1 | .slider { 2 | overflow: hidden; 3 | position: relative; 4 | width: 100%; 5 | height: 20rem; 6 | } 7 | .slider__inner { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | .slider__item { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | 21 | /* Demo purpose */ 22 | background: rgb(241 245 249); 23 | align-items: center; 24 | display: flex; 25 | justify-content: center; 26 | font-size: 2.5rem; 27 | font-weight: 500; 28 | } 29 | .slider__navigation { 30 | position: absolute; 31 | bottom: 1rem; 32 | left: 50%; 33 | transform: translateX(-50%); 34 | 35 | align-items: center; 36 | display: flex; 37 | justify-content: center; 38 | gap: 0.5rem; 39 | } 40 | .slider__dot, 41 | .slider__dot--active { 42 | background: rgb(203 213 225); 43 | border-radius: 50%; 44 | cursor: pointer; 45 | 46 | height: 0.5rem; 47 | width: 0.5rem; 48 | } 49 | .slider__dot--active { 50 | background: rgb(100 116 139); 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | } 55 | 56 | .slider__prev, 57 | .slider__next { 58 | position: absolute; 59 | top: 50%; 60 | transform: translateY(-50%); 61 | height: 1rem; 62 | width: 0.5rem; 63 | } 64 | .slider__prev::before, 65 | .slider__next::before { 66 | cursor: pointer; 67 | content: ''; 68 | position: absolute; 69 | border-style: solid; 70 | height: 0; 71 | width: 0; 72 | } 73 | .slider__prev::before { 74 | border-color: transparent rgb(148 163 184) transparent transparent; 75 | border-width: 0.5rem 0.5rem 0.5rem 0; 76 | } 77 | .slider__next::before { 78 | border-color: transparent transparent transparent rgb(148 163 184); 79 | border-width: 0.5rem 0 0.5rem 0.5rem; 80 | } 81 | .slider__prev { 82 | left: 0.5rem; 83 | } 84 | .slider__next { 85 | right: 0.5rem; 86 | } 87 | -------------------------------------------------------------------------------- /12-reference-an-element-with-react-use-ref-hook/use-ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/12-reference-an-element-with-react-use-ref-hook/use-ref.png -------------------------------------------------------------------------------- /13-build-your-own-drawing-board/App.tsx: -------------------------------------------------------------------------------- 1 | import { DrawingBoard } from './DrawingBoard'; 2 | 3 | export default App = () => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /13-build-your-own-drawing-board/DrawingBoard.tsx: -------------------------------------------------------------------------------- 1 | import { generateId } from './generateId'; 2 | import * as React from 'react'; 3 | import './styles.css'; 4 | 5 | type Point = { 6 | x: number; 7 | y: number; 8 | }; 9 | type Line = { 10 | id: string; 11 | points: Point[]; 12 | }; 13 | 14 | export const DrawingBoard = () => { 15 | const svgRef = React.useRef(); 16 | const [isDrawing, setIsDrawing] = React.useState(false); 17 | const [id, setId] = React.useState(''); 18 | const [lines, setLines] = React.useState([]); 19 | 20 | const handleMouseDown = (e) => { 21 | handleStartDrawing(e.clientX, e.clientY); 22 | }; 23 | 24 | const handleTouchStart = (e) => { 25 | handleStartDrawing(e.touches[0].clientX, e.touches[0].clientY); 26 | }; 27 | 28 | const handleStartDrawing = (x, y) => { 29 | const id = generateId(6); 30 | const svgRect = svgRef.current.getBoundingClientRect(); 31 | const startingPoint = { 32 | x: x - svgRect.x, 33 | y: y - svgRect.y, 34 | }; 35 | setIsDrawing(true); 36 | setId(id); 37 | setLines((lines) => ( 38 | lines.concat({ 39 | id, 40 | points: [startingPoint], 41 | }) 42 | )); 43 | }; 44 | 45 | const handleMouseMove = (e) => { 46 | handleMoving(e.clientX, e.clientY); 47 | }; 48 | 49 | const handleTouchMove = (e) => { 50 | handleMoving(e.touches[0].clientX, e.touches[0].clientY); 51 | }; 52 | 53 | const handleMoving = (x, y) => { 54 | if (!isDrawing) { 55 | return; 56 | } 57 | const svgRect = svgRef.current.getBoundingClientRect(); 58 | 59 | setLines((lines) => ( 60 | lines.map((line) => ( 61 | line.id === id 62 | ? { 63 | ...line, 64 | points: line.points.concat({ 65 | x: x - svgRect.x, 66 | y: y - svgRect.y, 67 | }), 68 | } 69 | : line 70 | )) 71 | )); 72 | }; 73 | 74 | const handleStopDrawing = () => { 75 | setIsDrawing(false); 76 | }; 77 | 78 | // For demo purspose 79 | React.useEffect(() => { 80 | const svgEle = svgRef.current; 81 | if (!svgEle) { 82 | return; 83 | } 84 | const { height, width } = svgEle.getBoundingClientRect(); 85 | svgEle.setAttribute('width', width); 86 | svgEle.setAttribute('viewBox', `0 0 ${width} ${height}`); 87 | }); 88 | 89 | return ( 90 | 105 | { 106 | lines.map(({ id, points }) => ( 107 | `${point.x},${point.y}`).join(" ")} 113 | /> 114 | )) 115 | } 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /13-build-your-own-drawing-board/drawing-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/13-build-your-own-drawing-board/drawing-board.png -------------------------------------------------------------------------------- /13-build-your-own-drawing-board/generateId.ts: -------------------------------------------------------------------------------- 1 | export const generateId = (length: number): string => 2 | Array(length) 3 | .fill('') 4 | .map((v) => Math.random().toString(36).charAt(2)) 5 | .join(''); 6 | -------------------------------------------------------------------------------- /13-build-your-own-drawing-board/styles.css: -------------------------------------------------------------------------------- 1 | .board { 2 | cursor: crosshair; 3 | } 4 | -------------------------------------------------------------------------------- /14-drag-and-drop-items-within-a-list/App.tsx: -------------------------------------------------------------------------------- 1 | import { SortableList } from './SortableList'; 2 | import './styles.css'; 3 | 4 | export default App = () => { 5 | return ( 6 | 7 |
A
8 |
B
9 |
C
10 |
D
11 |
E
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /14-drag-and-drop-items-within-a-list/SortableList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const SortableList = ({ children }) => { 4 | const clonedItems = React.useMemo(() => { 5 | return React.Children.map(children, (child, index) => ({ 6 | id: index, 7 | content: child, 8 | })); 9 | }, [children]); 10 | 11 | const [items, setItems] = React.useState(clonedItems); 12 | 13 | const [draggingIndex, setDraggingIndex] = React.useState(-1); 14 | const dragNode = React.useRef(); 15 | 16 | const handleDragStart = (e, index) => { 17 | const { target } = e; 18 | setDraggingIndex(index); 19 | 20 | dragNode.current = target; 21 | e.dataTransfer.effectAllowed = 'move'; 22 | e.dataTransfer.setData('text/html', target); 23 | }; 24 | 25 | const handleDragOver = (e, index) => { 26 | e.preventDefault(); 27 | if (dragNode.current !== e.target) { 28 | let newItems = [...items]; 29 | newItems.splice(index, 0, newItems.splice(draggingIndex, 1)[0]); 30 | setDraggingIndex(index); 31 | setItems(newItems); 32 | } 33 | }; 34 | 35 | return ( 36 |
37 | { 38 | items.map((item, index) => ( 39 |
handleDragStart(e, index)} 43 | onDragOver={(e) => handleDragOver(e, index)} 44 | > 45 | {item.content} 46 | 47 | )) 48 | } 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /14-drag-and-drop-items-within-a-list/drag-drop-items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/14-drag-and-drop-items-within-a-list/drag-drop-items.png -------------------------------------------------------------------------------- /14-drag-and-drop-items-within-a-list/styles.css: -------------------------------------------------------------------------------- 1 | .item { 2 | border: 1px solid rgb(203 213 225); 3 | border-radius: 0.25rem; 4 | cursor: pointer; 5 | 6 | align-items: center; 7 | display: flex; 8 | justify-content: center; 9 | 10 | font-size: 1.5rem; 11 | font-weight: 500; 12 | margin-bottom: 1rem; 13 | padding: 1rem 0; 14 | 15 | width: 16rem; 16 | } 17 | -------------------------------------------------------------------------------- /15-persist-values-between-renders/App.tsx: -------------------------------------------------------------------------------- 1 | import { DropIndicator } from './DropIndicator'; 2 | 3 | export default App = () => { 4 | return ( 5 | 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /15-persist-values-between-renders/DropIndicator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | 4 | export const DropIndicator = () => { 5 | const containerRef = React.useRef(); 6 | const dragCount = React.useRef(0); 7 | const [isDragging, setDragging] = React.useState(false); 8 | 9 | const handleDrop = (e: DragEvent): void => { 10 | e.preventDefault(); 11 | setDragging(false); 12 | dragCount.current = 0; 13 | // Do something with `e.dataTransfer.files` ... 14 | }; 15 | 16 | const handleDragOver = (e: DragEvent): void => { 17 | e.preventDefault(); 18 | }; 19 | 20 | const handleDragEnter = (e: DragEvent): void => { 21 | e.preventDefault(); 22 | dragCount.current += 1; 23 | if (dragCount.current <= 1) { 24 | setDragging(true); 25 | } 26 | }; 27 | 28 | const handleDragLeave = (e: DragEvent): void => { 29 | e.preventDefault(); 30 | dragCount.current -= 1; 31 | if (dragCount.current <= 0) { 32 | setDragging(false); 33 | } 34 | }; 35 | 36 | return ( 37 |
45 | {isDragging ? 'Drag and drop a file here' : ( 46 | 47 | )} 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /15-persist-values-between-renders/persist-values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/15-persist-values-between-renders/persist-values.png -------------------------------------------------------------------------------- /15-persist-values-between-renders/styles.css: -------------------------------------------------------------------------------- 1 | .indicator { 2 | align-items: center; 3 | display: flex; 4 | justify-content: center; 5 | 6 | width: 100%; 7 | height: 24rem; 8 | } 9 | 10 | .indicator__dragging { 11 | border: 2px dashed rgb(203 213 225); 12 | } 13 | -------------------------------------------------------------------------------- /16-save-the-previous-value-of-a-variable/App.tsx: -------------------------------------------------------------------------------- 1 | import { usePrevious } from './usePrevious'; 2 | import * as React from 'react'; 3 | 4 | export default App = () => { 5 | const [scrollPosition, setScrollPosition] = React.useState(0); 6 | const previousScrollPosition = usePrevious(scrollPosition); 7 | 8 | const handleScroll = () => setScrollPosition(window.scrollY); 9 | 10 | React.useEffect(() => { 11 | document.addEventListener("scroll", handleScroll); 12 | return () => { 13 | document.removeEventListener("scroll", handleScroll); 14 | }; 15 | }, []); 16 | 17 | React.useEffect(() => { 18 | if (previousScrollPosition < scrollPosition) { 19 | console.log("Scroll down"); 20 | } else if (previousScrollPosition > scrollPosition) { 21 | console.log("Scroll up"); 22 | } 23 | }, [scrollPosition, previousScrollPosition]); 24 | 25 | 26 | return ( 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /16-save-the-previous-value-of-a-variable/save-previous-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/16-save-the-previous-value-of-a-variable/save-previous-value.png -------------------------------------------------------------------------------- /16-save-the-previous-value-of-a-variable/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const usePrevious = (value) => { 4 | const ref = React.useRef(); 5 | React.useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | }; 10 | -------------------------------------------------------------------------------- /17-detect-whether-an-element-is-in-view/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Card } from './Card'; 3 | 4 | export default App = () => { 5 | return ( 6 |
7 | { 8 | Array(40).fill(0).map((_, index) => ( 9 | {index + 1} 10 | )) 11 | } 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /17-detect-whether-an-element-is-in-view/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | import { usePrevious } from './usePrevious'; 4 | 5 | export const Card = ({ children }) => { 6 | const eleRef = React.useRef(); 7 | const [isInView, setIsInView] = React.useState(false); 8 | const wasInView = usePrevious(isInView); 9 | 10 | const checkInView = () => { 11 | const ele = eleRef.current; 12 | if (!ele) { 13 | return; 14 | } 15 | const rect = ele.getBoundingClientRect(); 16 | setIsInView(rect.top < window.innerHeight && rect.bottom >= 0); 17 | }; 18 | 19 | React.useEffect(() => { 20 | checkInView(); 21 | }, []); 22 | 23 | React.useEffect(() => { 24 | document.addEventListener("scroll", checkInView); 25 | window.addEventListener("resize", checkInView); 26 | return () => { 27 | document.removeEventListener("scroll", checkInView); 28 | window.removeEventListener("resize", checkInView); 29 | }; 30 | }, []); 31 | 32 | React.useEffect(() => { 33 | const ele = eleRef.current; 34 | if (!ele) { 35 | return; 36 | } 37 | if (!wasInView && isInView) { 38 | // Element has come into view 39 | ele.classList.add('card__animated'); 40 | } 41 | }, [isInView]); 42 | 43 | return ( 44 |
45 | {children} 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /17-detect-whether-an-element-is-in-view/detect-in-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/17-detect-whether-an-element-is-in-view/detect-in-view.png -------------------------------------------------------------------------------- /17-detect-whether-an-element-is-in-view/styles.css: -------------------------------------------------------------------------------- 1 | .card { 2 | opacity: 0; 3 | will-change: transform, opacity; 4 | transform: translateY(4rem) scale(0.8); 5 | transition: all 400ms; 6 | 7 | /* Demo purpose */ 8 | align-items: center; 9 | display: flex; 10 | justify-content: center; 11 | 12 | border: 1px solid rgb(203 213 225); 13 | border-radius: 0.25rem; 14 | font-size: 2rem; 15 | font-weight: 500; 16 | padding: 1rem; 17 | 18 | height: 8rem; 19 | width: 8rem; 20 | } 21 | .card__animated { 22 | opacity: 1; 23 | transform: translateY(0) scale(1); 24 | } 25 | 26 | .grid { 27 | display: flex; 28 | flex-wrap: wrap; 29 | gap: 1rem; 30 | } 31 | -------------------------------------------------------------------------------- /17-detect-whether-an-element-is-in-view/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const usePrevious = (value) => { 4 | const ref = React.useRef(); 5 | React.useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | }; 10 | -------------------------------------------------------------------------------- /18-pass-a-ref-to-a-custom-hook/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useInView } from './useInView'; 3 | 4 | export default App = () => { 5 | const initialProducts = Array(20).fill(0).map((_, index) => ({ 6 | name: `${index + 1}`, 7 | })); 8 | const [isFetching, setIsFetching] = React.useState(false); 9 | const [products, setProducts] = React.useState(initialProducts); 10 | 11 | const bottomRef = React.useRef(); 12 | 13 | const fetchMoreData = () => { 14 | const fetched = Array(20).fill(0).map((_, index) => ({ 15 | name: `${products.length + index + 1}`, 16 | })); 17 | setIsFetching(false); 18 | setProducts(products.concat(fetched)); 19 | }; 20 | 21 | const handleReachBottom = () => { 22 | setIsFetching(true); 23 | setTimeout(fetchMoreData, 2000); 24 | }; 25 | 26 | useInView(bottomRef, handleReachBottom); 27 | 28 | return ( 29 | <> 30 |
31 | { 32 | products.map((product, index) => ( 33 | 34 | {product.name} 35 | 36 | )) 37 | } 38 |
39 |
40 | {isFetching && ( 41 |
42 |
43 | Loading more data ... 44 |
45 |
46 | )} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /18-pass-a-ref-to-a-custom-hook/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './styles.css'; 3 | import { useInView } from './useInView'; 4 | 5 | export const Card = ({ children }) => { 6 | const ref = React.useRef(); 7 | const handleInView = (ele) => { 8 | ele.classList.add("card__animated"); 9 | }; 10 | 11 | useInView(ref, handleInView); 12 | 13 | return ( 14 |
{children}
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /18-pass-a-ref-to-a-custom-hook/pass-ref-custom-hook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/18-pass-a-ref-to-a-custom-hook/pass-ref-custom-hook.png -------------------------------------------------------------------------------- /18-pass-a-ref-to-a-custom-hook/styles.css: -------------------------------------------------------------------------------- 1 | .card { 2 | opacity: 0; 3 | will-change: transform, opacity; 4 | transform: translateY(2rem) scale(0.8); 5 | transition: all 400ms; 6 | 7 | /* Demo purpose */ 8 | align-items: center; 9 | display: flex; 10 | justify-content: center; 11 | 12 | border: 1px solid rgb(203 213 225); 13 | border-radius: 0.25rem; 14 | font-size: 2rem; 15 | font-weight: 500; 16 | padding: 1rem; 17 | 18 | height: 8rem; 19 | width: 8rem; 20 | } 21 | .card__animated { 22 | opacity: 1; 23 | transform: translateY(0) scale(1); 24 | } 25 | .loading { 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | margin: 1rem 0; 30 | } 31 | .loading__inner { 32 | background-color: rgb(99 102 241); 33 | border-radius: 9999px; 34 | color: #fff; 35 | padding: 0.25rem 1rem; 36 | } 37 | 38 | .grid { 39 | display: flex; 40 | flex-wrap: wrap; 41 | gap: 1rem; 42 | } 43 | -------------------------------------------------------------------------------- /18-pass-a-ref-to-a-custom-hook/useInView.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { usePrevious } from './usePrevious'; 3 | 4 | export const useInView = (ref, onInView) => { 5 | const [isInView, setIsInView] = React.useState(false); 6 | const wasInView = usePrevious(isInView); 7 | 8 | const checkInView = () => { 9 | const ele = ref.current; 10 | if (!ele) { 11 | return; 12 | } 13 | const rect = ele.getBoundingClientRect(); 14 | setIsInView( 15 | rect.top < window.innerHeight && rect.bottom >= 0 16 | ); 17 | }; 18 | 19 | React.useEffect(() => { 20 | checkInView(); 21 | }, []); 22 | 23 | React.useEffect(() => { 24 | document.addEventListener("scroll", checkInView); 25 | window.addEventListener("resize", checkInView); 26 | return () => { 27 | document.removeEventListener("scroll", checkInView); 28 | window.removeEventListener("resize", checkInView); 29 | }; 30 | }, []); 31 | 32 | React.useEffect(() => { 33 | const ele = ref.current; 34 | if (ele && !wasInView && isInView) { 35 | onInView(ele); 36 | } 37 | }, [isInView, ref]); 38 | }; 39 | -------------------------------------------------------------------------------- /18-pass-a-ref-to-a-custom-hook/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const usePrevious = (value) => { 4 | const ref = React.useRef(); 5 | React.useEffect(() => { 6 | ref.current = value; 7 | }); 8 | return ref.current; 9 | }; 10 | -------------------------------------------------------------------------------- /19-merge-different-refs/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mergeRefs } from './mergeRefs'; 3 | import './styles.css'; 4 | import { useDraggable } from './useDraggable'; 5 | import { useResizable } from './useResizable'; 6 | 7 | export default App = () => { 8 | const [draggableRef] = useDraggable(); 9 | const [resizableRef] = useResizable(); 10 | const ref = mergeRefs([draggableRef, resizableRef]); 11 | 12 | return ( 13 |
14 |
15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /19-merge-different-refs/merge-refs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/19-merge-different-refs/merge-refs.png -------------------------------------------------------------------------------- /19-merge-different-refs/mergeRefs.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const mergeRefs = (refs: Array | React.LegacyRef | null>): React.RefCallback => { 4 | return (value) => { 5 | refs.forEach((ref) => { 6 | if (typeof ref === "function") { 7 | ref(value); 8 | } else if (ref != null) { 9 | (ref as React.MutableRefObject).current = value; 10 | } 11 | }); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /19-merge-different-refs/styles.css: -------------------------------------------------------------------------------- 1 | .element { 2 | cursor: move; 3 | 4 | border: 1px solid rgb(203 213 225); 5 | border-radius: 0.25rem; 6 | height: 8rem; 7 | width: 8rem; 8 | } 9 | 10 | .resizable { 11 | position: relative; 12 | 13 | border: 1px solid rgb(203 213 225); 14 | border-radius: 0.25rem; 15 | height: 8rem; 16 | width: 8rem; 17 | } 18 | .resizer { 19 | background: transparent; 20 | position: absolute; 21 | } 22 | .resizable:hover .resizer { 23 | background: rgb(99 102 241); 24 | } 25 | 26 | .resizer--r { 27 | cursor: col-resize; 28 | 29 | right: 0; 30 | top: 50%; 31 | transform: translate(50%, -50%); 32 | 33 | height: 2rem; 34 | width: 0.25rem; 35 | } 36 | 37 | .resizer--b { 38 | cursor: row-resize; 39 | 40 | bottom: 0; 41 | left: 50%; 42 | transform: translate(-50%, 50%); 43 | 44 | height: 0.25rem; 45 | width: 2rem; 46 | } 47 | -------------------------------------------------------------------------------- /19-merge-different-refs/useDraggable.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useDraggable = () => { 4 | const [node, setNode] = React.useState(null); 5 | const [{ dx, dy }, setOffset] = React.useState({ 6 | dx: 0, 7 | dy: 0, 8 | }); 9 | const ref = React.useCallback((nodeEle) => { 10 | setNode(nodeEle); 11 | }, []); 12 | 13 | const handleMouseDown = React.useCallback((e) => { 14 | const startX = e.clientX - dx; 15 | const startY = e.clientY - dy; 16 | 17 | const handleMouseMove = (e) => { 18 | setOffset({ 19 | dx: e.clientX - startX, 20 | dy: e.clientY - startY, 21 | }); 22 | }; 23 | const handleMouseUp = () => { 24 | document.removeEventListener("mousemove", handleMouseMove); 25 | document.removeEventListener("mouseup", handleMouseUp); 26 | }; 27 | 28 | document.addEventListener("mousemove", handleMouseMove); 29 | document.addEventListener("mouseup", handleMouseUp); 30 | }, [dx, dy]); 31 | 32 | React.useEffect(() => { 33 | if (node) { 34 | node.style.transform = `translate3d(${dx}px, ${dy}px, 0)`; 35 | } 36 | }, [node, dx, dy]); 37 | 38 | React.useEffect(() => { 39 | if (!node) { 40 | return; 41 | } 42 | node.addEventListener("mousedown", handleMouseDown); 43 | return () => { 44 | node.removeEventListener("mousedown", handleMouseDown); 45 | }; 46 | }, [node, dx, dy]); 47 | 48 | return [ref]; 49 | }; 50 | -------------------------------------------------------------------------------- /19-merge-different-refs/useResizable.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useResizable = () => { 4 | const [node, setNode] = React.useState(null); 5 | const [{ w, h }, setSize] = React.useState({ 6 | w: 0, 7 | h: 0, 8 | }); 9 | const ref = React.useCallback((nodeEle) => { 10 | setNode(nodeEle); 11 | }, []); 12 | 13 | const handleMouseDown = React.useCallback((e) => { 14 | e.stopPropagation(); 15 | 16 | const startX = e.clientX; 17 | const startY = e.clientY; 18 | 19 | const styles = window.getComputedStyle(node); 20 | const w = parseInt(styles.width, 10); 21 | const h = parseInt(styles.height, 10); 22 | 23 | const handleMouseMove = (e) => { 24 | const newDx = e.clientX - startX; 25 | const newDy = e.clientY - startY; 26 | setSize({ 27 | w: w + newDx, 28 | h: h + newDy, 29 | }); 30 | }; 31 | const handleMouseUp = () => { 32 | document.removeEventListener("mousemove", handleMouseMove); 33 | document.removeEventListener("mouseup", handleMouseUp); 34 | }; 35 | 36 | document.addEventListener("mousemove", handleMouseMove); 37 | document.addEventListener("mouseup", handleMouseUp); 38 | }, [node]); 39 | 40 | React.useEffect(() => { 41 | if (node && w > 0 && h > 0) { 42 | node.style.width = `${w}px`; 43 | node.style.height = `${h}px`; 44 | } 45 | }, [node, w, h]); 46 | 47 | React.useEffect(() => { 48 | if (!node) { 49 | return; 50 | } 51 | const resizerElements = [...node.querySelectorAll('.resizer')]; 52 | resizerElements.forEach((resizerEle) => { 53 | resizerEle.addEventListener("mousedown", handleMouseDown); 54 | }); 55 | 56 | return () => { 57 | resizerElements.forEach((resizerEle) => { 58 | resizerEle.removeEventListener("mousedown", handleMouseDown); 59 | }); 60 | }; 61 | }, [node]); 62 | 63 | return [ref]; 64 | }; 65 | -------------------------------------------------------------------------------- /20-build-a-tooltip-component/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Tooltip } from './Tooltip'; 3 | 4 | export default App = () => { 5 | return ( 6 | 7 |
Hover me
8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /20-build-a-tooltip-component/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { mergeRefs } from './mergeRefs'; 4 | import './styles.css'; 5 | 6 | export const Tooltip = ({ children, tip }) => { 7 | const triggerRef = React.useRef(); 8 | const tipRef = React.useRef(); 9 | 10 | const [isOpen, setIsOpen] = React.useState(false); 11 | 12 | const handleMouseEnter = () => setIsOpen(true); 13 | const handleMouseLeave = () => setIsOpen(false); 14 | 15 | React.useEffect(() => { 16 | if (!isOpen || !triggerRef.current || !tipRef.current) { 17 | return; 18 | } 19 | const triggerRect = triggerRef.current.getBoundingClientRect(); 20 | const tipRect = tipRef.current.getBoundingClientRect(); 21 | const top = (triggerRect.y + window.pageYOffset) + triggerRect.height + 8; 22 | const left = (triggerRect.x + window.pageXOffset) + ((triggerRect.width - tipRect.width) / 2); 23 | tipRef.current.style.transform = `translate(${left}px, ${top}px)`; 24 | }, [isOpen, triggerRef, tipRef]); 25 | 26 | const child = typeof children === 'string' ? {children} : React.Children.only(children); 27 | const clonedEle = React.cloneElement(child, { 28 | ...child.props, 29 | ref: mergeRefs([child.ref, triggerRef]), 30 | onMouseEnter: handleMouseEnter, 31 | onMouseLeave: handleMouseLeave, 32 | }); 33 | 34 | return ( 35 | <> 36 | {clonedEle} 37 | {isOpen && ReactDOM.createPortal( 38 |
{tip}
, 39 | document.body 40 | )} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /20-build-a-tooltip-component/mergeRefs.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const mergeRefs = (refs: Array | React.LegacyRef | null>): React.RefCallback => { 4 | return (value) => { 5 | refs.forEach((ref) => { 6 | if (typeof ref === "function") { 7 | ref(value); 8 | } else if (ref != null) { 9 | (ref as React.MutableRefObject).current = value; 10 | } 11 | }); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /20-build-a-tooltip-component/styles.css: -------------------------------------------------------------------------------- 1 | .tip__content { 2 | background-color: rgb(15 23 42); 3 | border-radius: 0.25rem; 4 | color: #fff; 5 | padding: 0.25rem 0.5rem; 6 | 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | } 11 | .tip__content::after { 12 | background-color: rgb(15 23 42); 13 | content: ''; 14 | 15 | position: absolute; 16 | top: -0.25rem; 17 | left: 50%; 18 | transform: translateX(-50%) rotate(45deg); 19 | 20 | width: 0.5rem; 21 | height: 0.5rem; 22 | } 23 | -------------------------------------------------------------------------------- /20-build-a-tooltip-component/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/20-build-a-tooltip-component/tooltip.png -------------------------------------------------------------------------------- /21-pass-a-ref-to-a-child-component-using-forward-ref/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Uploader } from './Uploader'; 3 | import './styles.css'; 4 | 5 | export default App = () => { 6 | const uploaderRef = React.useRef(); 7 | 8 | const handleClickContainer = () => { 9 | const uploadBtn = uploaderRef.current; 10 | if (uploadBtn) { 11 | uploadBtn.click(); 12 | } 13 | }; 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /21-pass-a-ref-to-a-child-component-using-forward-ref/Uploader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Uploader = React.forwardRef((props, ref) => { 4 | const inputRef = React.useRef(); 5 | 6 | const handleClick = () => { 7 | const inputEle = inputRef.current; 8 | if (inputEle) { 9 | inputEle.click(); 10 | } 11 | }; 12 | 13 | const handleFileChange = (e) => { 14 | // Handle the selected file `e.target.files` ... 15 | }; 16 | 17 | return ( 18 |
19 | 26 | 32 |
33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /21-pass-a-ref-to-a-child-component-using-forward-ref/forward-ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/21-pass-a-ref-to-a-child-component-using-forward-ref/forward-ref.png -------------------------------------------------------------------------------- /21-pass-a-ref-to-a-child-component-using-forward-ref/styles.css: -------------------------------------------------------------------------------- 1 | .uploader__button { 2 | background: transparent; 3 | border: 1px solid rgb(203 213 225); 4 | border-radius: 0.25rem; 5 | cursor: pointer; 6 | height: 2rem; 7 | padding: 0 0.5rem; 8 | } 9 | .uploader__input { 10 | display: none; 11 | } 12 | 13 | .container { 14 | border: 2px dashed rgb(203 213 225); 15 | border-radius: 0.25rem; 16 | cursor: pointer; 17 | height: 8rem; 18 | width: 8rem; 19 | 20 | align-items: center; 21 | display: flex; 22 | justify-content: center; 23 | } 24 | .container__icon path { 25 | fill: none; 26 | stroke: #000; 27 | stroke-linecap: round; 28 | stroke-linejoin: round; 29 | stroke-width: 1; 30 | } 31 | .container__uploader { 32 | display: none; 33 | } 34 | -------------------------------------------------------------------------------- /22-expose-methods-of-a-component-using-use-imperative-handle/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slider } from './Slider'; 3 | import './styles.css'; 4 | 5 | export default App = () => { 6 | const numItems = 5; 7 | 8 | const sliderRef = React.useRef(); 9 | const [currentIndex, setCurrentIndex] = React.useState(0); 10 | 11 | const handleClickPreviousButton = () => { 12 | const slider = sliderRef.current; 13 | if (slider) { 14 | slider.goToPreviousItem(); 15 | } 16 | }; 17 | 18 | const handleClickNextButton = () => { 19 | const slider = sliderRef.current; 20 | if (slider) { 21 | slider.goToNextItem(); 22 | } 23 | }; 24 | 25 | const handleClickDot = (index) => { 26 | const slider = sliderRef.current; 27 | if (slider) { 28 | slider.jump(index); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 | 35 | { 36 | Array(numItems).fill(0).map((_, index) => ( 37 |
38 | {index + 1} 39 |
40 | )) 41 | } 42 |
43 |
44 | { 45 | Array(numItems).fill(0).map((_, index) => ( 46 |
handleClickDot(index)} 50 | /> 51 | )) 52 | } 53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /22-expose-methods-of-a-component-using-use-imperative-handle/Slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Slider = React.forwardRef(({ children, onActivate }, ref) => { 4 | const innerRef = React.useRef(); 5 | const [currentIndex, setCurrentIndex] = React.useState(0); 6 | 7 | const cloneChildren = React.Children.toArray(children); 8 | 9 | React.useImperativeHandle(ref, () => ({ 10 | goToPreviousItem, 11 | goToNextItem, 12 | jump, 13 | })); 14 | 15 | const jump = (index) => { 16 | const innerEle = innerRef.current; 17 | if (!innerEle) { 18 | return; 19 | } 20 | innerEle.animate([ 21 | { 22 | transform: `translateX(${-100 * index}%)`, 23 | } 24 | ], { 25 | duration: 400, 26 | easing: 'ease-in-out', 27 | fill: 'forwards', 28 | }).addEventListener('finish', () => { 29 | setCurrentIndex(index); 30 | onActivate(index); 31 | }); 32 | }; 33 | 34 | const goToPreviousItem = () => { 35 | if (currentIndex > 0) { 36 | jump(currentIndex - 1); 37 | } 38 | }; 39 | 40 | const goToNextItem = () => { 41 | const numItems = cloneChildren.length; 42 | if (currentIndex < numItems - 1) { 43 | jump(currentIndex + 1); 44 | } 45 | }; 46 | 47 | return ( 48 |
49 |
50 | { 51 | cloneChildren.map((children, index) => ( 52 |
59 | {children} 60 |
61 | )) 62 | } 63 |
64 |
65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /22-expose-methods-of-a-component-using-use-imperative-handle/styles.css: -------------------------------------------------------------------------------- 1 | .slider { 2 | overflow: hidden; 3 | position: relative; 4 | width: 100%; 5 | height: 20rem; 6 | } 7 | .slider__inner { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | .slider__item { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | 21 | /* Demo purpose */ 22 | background: rgb(241 245 249); 23 | align-items: center; 24 | display: flex; 25 | justify-content: center; 26 | font-size: 2.5rem; 27 | font-weight: 500; 28 | } 29 | 30 | .container { 31 | position: relative; 32 | } 33 | 34 | .container__navigation { 35 | position: absolute; 36 | bottom: 1rem; 37 | left: 50%; 38 | transform: translateX(-50%); 39 | 40 | align-items: center; 41 | display: flex; 42 | justify-content: center; 43 | gap: 0.5rem; 44 | } 45 | .container__dot { 46 | background: rgb(203 213 225); 47 | border-radius: 50%; 48 | cursor: pointer; 49 | 50 | height: 0.5rem; 51 | width: 0.5rem; 52 | } 53 | .container__dot--active { 54 | background: rgb(100 116 139); 55 | } 56 | 57 | .container__prev, 58 | .container__next { 59 | position: absolute; 60 | top: 50%; 61 | transform: translateY(-50%); 62 | height: 1rem; 63 | width: 0.5rem; 64 | } 65 | .container__prev::before, 66 | .container__next::before { 67 | cursor: pointer; 68 | content: ''; 69 | position: absolute; 70 | border-style: solid; 71 | height: 0; 72 | width: 0; 73 | } 74 | .container__prev::before { 75 | border-color: transparent rgb(148 163 184) transparent transparent; 76 | border-width: 0.5rem 0.5rem 0.5rem 0; 77 | } 78 | .container__next::before { 79 | border-color: transparent transparent transparent rgb(148 163 184); 80 | border-width: 0.5rem 0 0.5rem 0.5rem; 81 | } 82 | .container__prev { 83 | left: 0.5rem; 84 | } 85 | .container__next { 86 | right: 0.5rem; 87 | } 88 | -------------------------------------------------------------------------------- /22-expose-methods-of-a-component-using-use-imperative-handle/use-imperative-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phuocng/master-of-react-ref/082b7c02321fff9c6843a5a1b63edd1f5ec68149/22-expose-methods-of-a-component-using-use-imperative-handle/use-imperative-handle.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 phuocng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Master of React ref 2 | 3 | React Ref is a fantastic feature in React that allows you to directly access and manipulate DOM (Document Object Model) elements. 4 | In simpler terms, it's a way to reference a specific element in your React app, giving you the power to modify and interact with it outside of the normal React data flow. 5 | 6 | This series is your ultimate guide to mastering the use of ref in React. 7 | We'll start with the basics, such as string and callback ref, and then dive into more advanced topics like the `useRef()` hook and `forwardRef()` API. 8 | By the end, you'll have a deep understanding of how to use ref like a pro in your React applications. 9 | Get ready to take your React skills to the next level! 10 | 11 | - [01: Access a component's underlying DOM node with findDOMNode()](https://phuoc.ng/collection/react-ref/access-a-component-underlying-dom-node-with-find-dom-node/) 12 | 13 | ![Access a component's underlying DOM node with findDOMNode()](/01-access-a-component-underlying-dom-node-with-find-dom-node/find-dom-node.png) 14 | 15 | - [02: String refs](https://phuoc.ng/collection/react-ref/string-refs/) 16 | 17 | ![String refs](/02-string-refs/string-refs.png) 18 | 19 | - [03: Store a reference with callback refs](https://phuoc.ng/collection/react-ref/store-a-reference-with-callback-refs/) 20 | 21 | ![Store a reference with callback refs](/03-store-a-reference-with-callback-refs/callback-refs.png) 22 | 23 | - [04: Access the methods of class components](https://phuoc.ng/collection/react-ref/access-the-methods-of-class-components/) 24 | 25 | ![Access the methods of class components](/04-access-the-methods-of-class-components/access-methods.png) 26 | 27 | - [05: Use callback refs to access individual elements in a list](https://phuoc.ng/collection/react-ref/use-callback-refs-to-access-individual-elements-in-a-list/) 28 | 29 | ![Use callback refs to access individual elements in a list](/05-use-callback-refs-to-access-individual-elements-in-a-list/access-individual-elements.png) 30 | 31 | - [06: Implement a basic container query with callback refs](https://phuoc.ng/collection/react-ref/implement-a-basic-container-query-with-callback-refs/) 32 | 33 | ![Implement a basic container query with callback refs](/06-implement-a-basic-container-query-with-callback-refs/basic-container-query.png) 34 | 35 | - [07: Save the element passed to a callback ref as a state](https://phuoc.ng/collection/react-ref/save-the-element-passed-to-a-callback-ref-as-a-state/) 36 | 37 | ![Save the element passed to a callback ref as a state](/07-save-the-element-passed-to-a-callback-ref-as-a-state/callback-ref-state.png) 38 | 39 | - [08: Create a custom hook returning a callback ref](https://phuoc.ng/collection/react-ref/create-a-custom-hook-returning-a-callback-ref/) 40 | 41 | ![Create a custom hook returning a callback ref](/08-create-a-custom-hook-returning-a-callback-ref/hook-returning-callback-ref.png) 42 | 43 | - [09: Make an element draggable](https://phuoc.ng/collection/react-ref/make-an-element-draggable/) 44 | 45 | ![Make an element draggable](/09-make-an-element-draggable/draggable.png) 46 | 47 | - [10: Pass refs to child components using the function as a child pattern](https://phuoc.ng/collection/react-ref/pass-refs-to-child-components-using-the-function-as-a-child-pattern/) 48 | 49 | ![Pass refs to child components using the function as a child pattern](/10-pass-refs-to-child-components-using-the-function-as-a-child-pattern/pass-refs-child-components.png) 50 | 51 | - [11: Create a reference using React.createRef()](https://phuoc.ng/collection/react-ref/create-a-reference-using-react-create-ref/) 52 | 53 | ![Create a reference using React.createRef()](/11-create-a-reference-using-react-create-ref/react-create-ref.png) 54 | 55 | - [12: Reference an element with React's useRef() hook](https://phuoc.ng/collection/react-ref/reference-an-element-with-react-use-ref-hook/) 56 | 57 | ![Reference an element with React's useRef() hook](/12-reference-an-element-with-react-use-ref-hook/use-ref.png) 58 | 59 | - [13: Build your own drawing board](https://phuoc.ng/collection/react-ref/build-your-own-drawing-board/) 60 | 61 | ![Build your own drawing board](/13-build-your-own-drawing-board/drawing-board.png) 62 | 63 | - [14: Drag and drop items within a list](https://phuoc.ng/collection/react-ref/drag-and-drop-items-within-a-list/) 64 | 65 | ![Drag and drop items within a list](/14-drag-and-drop-items-within-a-list/drag-drop-items.png) 66 | 67 | - [15: Persist values between renders](https://phuoc.ng/collection/react-ref/persist-values-between-renders/) 68 | 69 | ![Persist values between renders](/15-persist-values-between-renders/persist-values.png) 70 | 71 | - [16: Save the previous value of a variable](https://phuoc.ng/collection/react-ref/save-the-previous-value-of-a-variable/) 72 | 73 | ![Save the previous value of a variable](/16-save-the-previous-value-of-a-variable/save-previous-value.png) 74 | 75 | - [17: Detect whether an element is in view](https://phuoc.ng/collection/react-ref/detect-whether-an-element-is-in-view/) 76 | 77 | ![Detect whether an element is in view](/17-detect-whether-an-element-is-in-view/detect-in-view.png) 78 | 79 | - [18: Pass a ref to a custom hook](https://phuoc.ng/collection/react-ref/pass-a-ref-to-a-custom-hook/) 80 | 81 | ![Pass a ref to a custom hook](/18-pass-a-ref-to-a-custom-hook/pass-ref-custom-hook.png) 82 | 83 | - [19: Merge different refs](https://phuoc.ng/collection/react-ref/merge-different-refs/) 84 | 85 | ![Merge different refs](/19-merge-different-refs/merge-refs.png) 86 | 87 | - [20: Build a tooltip component](https://phuoc.ng/collection/react-ref/build-a-tooltip-component/) 88 | 89 | ![Build a tooltip component](/20-build-a-tooltip-component/tooltip.png) 90 | 91 | - [21: Pass a ref to a child component using forwardRef()](https://phuoc.ng/collection/react-ref/pass-a-ref-to-a-child-component-using-forward-ref/) 92 | 93 | ![Pass a ref to a child component using forwardRef()](/21-pass-a-ref-to-a-child-component-using-forward-ref/forward-ref.png) 94 | 95 | - [22: Expose methods of a component using useImperativeHandle()](https://phuoc.ng/collection/react-ref/expose-methods-of-a-component-using-use-imperative-handle/) 96 | 97 | ![Expose methods of a component using useImperativeHandle()](/22-expose-methods-of-a-component-using-use-imperative-handle/use-imperative-handle.png) 98 | --------------------------------------------------------------------------------