Day 7: Refs and Portals
What You'll Learn Today
- useRef hook basics
- Direct DOM element access
- Difference between Ref and State
- Forwarding Refs with forwardRef
- Using Portals
What Are Refs?
Refs (References) are a way to hold values that don't affect rendering. They're primarily used for direct DOM access.
flowchart TB
subgraph RefUseCases["Main Ref Use Cases"]
A["DOM element access"]
B["Holding timer IDs"]
C["Storing previous values"]
D["Integrating with external libraries"]
end
style A fill:#3b82f6,color:#fff
style B fill:#8b5cf6,color:#fff
style C fill:#22c55e,color:#fff
style D fill:#f59e0b,color:#fff
Difference from State
| Feature | State | Ref |
|---|---|---|
| Re-render on update | Yes | No |
| Value persistence | Across renders | Across renders |
| Main use | UI display data | Non-rendering data |
| Access method | Direct | .current property |
useRef Basics
useRef is a hook that creates a Ref object.
Basic Syntax
import { useRef } from 'react';
function Component() {
const ref = useRef(initialValue);
// Access via ref.current
}
Simple Example
function Counter() {
const countRef = useRef(0);
function handleClick() {
countRef.current += 1;
console.log(`Click count: ${countRef.current}`);
// Note: UI won't update!
}
return <button onClick={handleClick}>Click</button>;
}
TypeScript version
function Counter(): React.JSX.Element {
const countRef = useRef<number>(0);
function handleClick(): void {
countRef.current += 1;
console.log(`Click count: ${countRef.current}`);
// Note: UI won't update!
}
return <button onClick={handleClick}>Click</button>;
}
Accessing DOM Elements
The most common use of Refs is direct DOM access.
Focus Control
function TextInput() {
const inputRef = useRef(null);
function handleFocus() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>Focus</button>
</div>
);
}
TypeScript version
function TextInput(): React.JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
function handleFocus(): void {
inputRef.current?.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>Focus</button>
</div>
);
}
Scroll Control
function ScrollableList() {
const listRef = useRef(null);
function scrollToTop() {
listRef.current.scrollTo({
top: 0,
behavior: 'smooth'
});
}
function scrollToBottom() {
listRef.current.scrollTo({
top: listRef.current.scrollHeight,
behavior: 'smooth'
});
}
return (
<div>
<button onClick={scrollToTop}>Top</button>
<button onClick={scrollToBottom}>Bottom</button>
<ul ref={listRef} style={{ height: '200px', overflow: 'auto' }}>
{Array.from({ length: 50 }, (_, i) => (
<li key={i}>Item {i + 1}</li>
))}
</ul>
</div>
);
}
TypeScript version
function ScrollableList(): React.JSX.Element {
const listRef = useRef<HTMLUListElement>(null);
function scrollToTop(): void {
listRef.current?.scrollTo({
top: 0,
behavior: 'smooth'
});
}
function scrollToBottom(): void {
listRef.current?.scrollTo({
top: listRef.current.scrollHeight,
behavior: 'smooth'
});
}
return (
<div>
<button onClick={scrollToTop}>Top</button>
<button onClick={scrollToBottom}>Bottom</button>
<ul ref={listRef} style={{ height: '200px', overflow: 'auto' }}>
{Array.from({ length: 50 }, (_, i) => (
<li key={i}>Item {i + 1}</li>
))}
</ul>
</div>
);
}
Getting Element Dimensions
function MeasureBox() {
const boxRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (boxRef.current) {
const { width, height } = boxRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={boxRef} style={{ width: '200px', height: '100px', background: 'lightblue' }}>
Box
</div>
<p>Size: {dimensions.width} x {dimensions.height}</p>
</div>
);
}
TypeScript version
interface Dimensions {
width: number;
height: number;
}
function MeasureBox(): React.JSX.Element {
const boxRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
useEffect(() => {
if (boxRef.current) {
const { width, height } = boxRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={boxRef} style={{ width: '200px', height: '100px', background: 'lightblue' }}>
Box
</div>
<p>Size: {dimensions.width} x {dimensions.height}</p>
</div>
);
}
Storing Previous Values
Refs can store values from previous renders.
function PreviousValue() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}, Previous: {prevCount ?? 'none'}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
TypeScript version
function PreviousValue(): React.JSX.Element {
const [count, setCount] = useState<number>(0);
const prevCountRef = useRef<number | undefined>();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}, Previous: {prevCount ?? 'none'}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Extracting to Custom Hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount ?? 'none'}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
TypeScript version
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter(): React.JSX.Element {
const [count, setCount] = useState<number>(0);
const prevCount = usePrevious<number>(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount ?? 'none'}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Managing Timers
Storing timer IDs in Refs ensures reliable cleanup.
function Stopwatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
function start() {
if (!isRunning) {
setIsRunning(true);
intervalRef.current = setInterval(() => {
setTime(prev => prev + 10);
}, 10);
}
}
function stop() {
if (isRunning) {
clearInterval(intervalRef.current);
setIsRunning(false);
}
}
function reset() {
clearInterval(intervalRef.current);
setIsRunning(false);
setTime(0);
}
// Cleanup
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
const formatTime = (ms) => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const centiseconds = Math.floor((ms % 1000) / 10);
return `${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
};
return (
<div>
<p style={{ fontSize: '2rem' }}>{formatTime(time)}</p>
<button onClick={start} disabled={isRunning}>Start</button>
<button onClick={stop} disabled={!isRunning}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
TypeScript version
function Stopwatch(): React.JSX.Element {
const [time, setTime] = useState<number>(0);
const [isRunning, setIsRunning] = useState<boolean>(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
function start(): void {
if (!isRunning) {
setIsRunning(true);
intervalRef.current = setInterval(() => {
setTime(prev => prev + 10);
}, 10);
}
}
function stop(): void {
if (isRunning) {
clearInterval(intervalRef.current!);
setIsRunning(false);
}
}
function reset(): void {
clearInterval(intervalRef.current!);
setIsRunning(false);
setTime(0);
}
// Cleanup
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
const formatTime = (ms: number): string => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const centiseconds = Math.floor((ms % 1000) / 10);
return `${minutes}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
};
return (
<div>
<p style={{ fontSize: '2rem' }}>{formatTime(time)}</p>
<button onClick={start} disabled={isRunning}>Start</button>
<button onClick={stop} disabled={!isRunning}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
forwardRef
Use forwardRef to access a child component's DOM element from the parent.
flowchart LR
subgraph ForwardRef["forwardRef Mechanism"]
Parent["Parent Component<br/>ref={inputRef}"]
Child["Child Component<br/>forwardRef"]
DOM["DOM Element<br/>input"]
end
Parent -->|"forward ref"| Child -->|"apply ref"| DOM
style Parent fill:#3b82f6,color:#fff
style Child fill:#8b5cf6,color:#fff
style DOM fill:#22c55e,color:#fff
Basic Usage
import { forwardRef, useRef } from 'react';
// Child component: wrapped with forwardRef
const TextInput = forwardRef(function TextInput(props, ref) {
return (
<input
ref={ref}
type="text"
className="custom-input"
{...props}
/>
);
});
// Parent component
function Form() {
const inputRef = useRef(null);
function handleFocus() {
inputRef.current.focus();
}
return (
<div>
<TextInput ref={inputRef} placeholder="Enter text" />
<button onClick={handleFocus}>Focus</button>
</div>
);
}
TypeScript version
import { forwardRef, useRef, ComponentPropsWithoutRef } from 'react';
// Child component: wrapped with forwardRef
const TextInput = forwardRef<HTMLInputElement, ComponentPropsWithoutRef<'input'>>(
function TextInput(props, ref) {
return (
<input
ref={ref}
type="text"
className="custom-input"
{...props}
/>
);
}
);
// Parent component
function Form(): React.JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
function handleFocus(): void {
inputRef.current?.focus();
}
return (
<div>
<TextInput ref={inputRef} placeholder="Enter text" />
<button onClick={handleFocus}>Focus</button>
</div>
);
}
useImperativeHandle
Limit the methods exposed by child components.
import { forwardRef, useImperativeHandle, useRef } from 'react';
const CustomInput = forwardRef(function CustomInput(props, ref) {
const inputRef = useRef(null);
// Define exposed methods
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = '';
},
getValue() {
return inputRef.current.value;
}
}));
return <input ref={inputRef} {...props} />;
});
// Usage
function Form() {
const inputRef = useRef(null);
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
<button onClick={() => inputRef.current.clear()}>Clear</button>
<button onClick={() => alert(inputRef.current.getValue())}>Get Value</button>
</div>
);
}
TypeScript version
import { forwardRef, useImperativeHandle, useRef, ComponentPropsWithoutRef } from 'react';
interface CustomInputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
}
const CustomInput = forwardRef<CustomInputHandle, ComponentPropsWithoutRef<'input'>>(
function CustomInput(props, ref) {
const inputRef = useRef<HTMLInputElement>(null);
// Define exposed methods
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
clear() {
if (inputRef.current) inputRef.current.value = '';
},
getValue() {
return inputRef.current?.value ?? '';
}
}));
return <input ref={inputRef} {...props} />;
}
);
// Usage
function Form(): React.JSX.Element {
const inputRef = useRef<CustomInputHandle>(null);
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => inputRef.current?.clear()}>Clear</button>
<button onClick={() => alert(inputRef.current?.getValue())}>Get Value</button>
</div>
);
}
Portals
Portals let you render outside your component's DOM tree hierarchy.
flowchart TB
subgraph ReactTree["React Component Tree"]
App["App"]
Content["Content"]
Modal["Modal (Portal)"]
end
subgraph DOMTree["DOM Structure"]
Root["#root"]
ContentDOM["Content DOM"]
ModalRoot["#modal-root"]
ModalDOM["Modal DOM"]
end
App --> Content
Content --> Modal
Root --> ContentDOM
ModalRoot --> ModalDOM
Modal -.->|"Portal"| ModalDOM
style Modal fill:#f59e0b,color:#fff
style ModalDOM fill:#f59e0b,color:#fff
Basic Usage
import { createPortal } from 'react-dom';
function Modal({ children, onClose }) {
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
// Add <div id="modal-root"></div> to index.html
TypeScript version
import { createPortal } from 'react-dom';
import { ReactNode } from 'react';
interface ModalProps {
children: ReactNode;
onClose: () => void;
}
function Modal({ children, onClose }: ModalProps): React.ReactPortal {
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById('modal-root')!
);
}
// Add <div id="modal-root"></div> to index.html
Complete Modal Implementation
import { useState } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, title, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose} className="close-button">Γ</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
document.getElementById('modal-root')
);
}
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<h1>Main Content</h1>
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="Confirm"
>
<p>Are you sure you want to proceed?</p>
<button onClick={() => setIsModalOpen(false)}>Cancel</button>
<button onClick={() => {
console.log('Confirmed');
setIsModalOpen(false);
}}>OK</button>
</Modal>
</div>
);
}
TypeScript version
import { useState, ReactNode } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps): React.ReactPortal | null {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose} className="close-button">Γ</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
document.getElementById('modal-root')!
);
}
function App(): React.JSX.Element {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
return (
<div>
<h1>Main Content</h1>
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="Confirm"
>
<p>Are you sure you want to proceed?</p>
<button onClick={() => setIsModalOpen(false)}>Cancel</button>
<button onClick={() => {
console.log('Confirmed');
setIsModalOpen(false);
}}>OK</button>
</Modal>
</div>
);
}
Portal Use Cases
| Use Case | Description |
|---|---|
| Modals | Full-screen overlay dialogs |
| Tooltips | Element-attached popups |
| Dropdowns | Escape parent overflow settings |
| Notifications | Fixed position at screen edge |
Best Practices
When to Use Refs
// β
Good use of Refs
// - DOM manipulation (focus, scroll)
// - Storing timer IDs
// - Storing previous values
// - Integrating with external libraries
// β Don't use Refs for
// - Data to display in UI β Use State
// - Derived calculations β Use regular variables
When to Use Portals
// β
Good use of Portals
// - Modals, dialogs
// - Tooltips, popovers
// - Escaping parent CSS constraints
// β Don't use Portals for
// - Cases solvable with normal component composition
Summary
| Concept | Description |
|---|---|
| useRef | Hook to hold values without re-rendering |
| DOM Ref | Direct access to DOM elements |
| forwardRef | Forward Refs to child components |
| useImperativeHandle | Customize exposed Ref API |
| Portal | Render outside DOM tree |
Key Takeaways
- Ref changes don't cause re-renders
- DOM Refs are available after rendering
- Use forwardRef to access child component's DOM
- Use useImperativeHandle to limit exposed API
- Portals solve z-index and overflow issues
Exercises
Exercise 1: Basics
Create a component with a text input and button. Clicking the button should focus the input and select its contents.
Exercise 2: Application
Create an image gallery. Clicking a thumbnail opens a modal showing the full image (use Portal).
Challenge
Create a custom video player component. Use useImperativeHandle to expose play(), pause(), and seekTo(time) methods to parent components.
References
Coming Up Next: On Day 8, we'll learn about "Context API and State Management." Understand how to efficiently share data between components.