React components represent DOM tags that make up a particular part of a UI returned by the render
method. For cases where you want to interact with the DOM node outside the regular React lifecycle, we can use refs.
In this post, we will go over a short example where we need to dynamically reference the DOM node in functional child components by demonstrating how useRef
and createRef
differ.
Create the ref object using useRef
_useRef_
returns a mutable ref object whose_.current_
property is initialized to the passed argument (_initialValue_
). The returned object will persist for the full lifetime of the component. — React Docs
Let’s declare a parent-child relationship where a list component renders some undetermined number of rows:
// Parent: Feed
// Child: FancyRow
function Feed({ edges }: FeedProps): React.Element<'div'> {
const parentRef = useRef<HTMLDivElement>();
return (
<div className={styles[‘feed’]}>
<h4 className={styles[‘feed__item-title’]}>
List Container
</h4>
{edges.map((edge, i) => (
<FancyRow key={i} />
))}
</div>
);
}
React treats the ref
attribute similar to the key
prop when making lists. Declaring the ref
attribute on a function component will trigger an error in the console.
/* Error, ref will be undefined */
{edges.map((edge, i) => (
<FancyRow ref={parentRef} key={i} />
))}
To understand why this happens, recall React classes are objects that create instances to hold an object’s state.
Function components, on the other hand, do not have instances. When function components are re-rendered, the refObject
will lose its reference to the DOM and undefined.
We can reference our DOM from the child by declaring an alternative accessory from the parent like the following:
// Parent
{edges.map((edge, i) => (
<FeedRow parentRef={parentRef} key={i} />
))}
// Child
function FeedRow(props: RowProps): React.Element<’div’>{
return (
<div ref={props.parentRef} className={styles[‘feed__item’]} >
<div className={styles[‘feed__item-meta’]}>
<button>Container Button</button>
</div>
</div>
);
};
Alternatively, according to the docs, we can “forward” the ref directly to a child component using forwardRef
. We can use the regular ref
attribute and get the reference to one of the children from the component wrapped inforwardRef
as regular props.
// Parent
{edges.map((edge, i) => (
<FancyRow ref={parentRef} key={i} />
))}
const FancyRow = React.forwardRef((props, ref) =>
<FeedRow
key={props.edge}
parentRef={ref}
{…props}
/>
// Child
function FeedRow(props: RowProps): React.Element<’div’>{
return (
<div ref={props.parentRef} className={styles[‘feed__item’]} >
<div className={styles[‘feed__item-meta’]}>
<button>Container Button</button>
</div>
</div>
);
};
So far, we have a list of row containers.
Let’s look further into how the useRef
hook references the DOM rendered from a child. Here, we create a click handler from the parent and pass it to the child as regular props.
function Feed({ edges }: Props): ReactNode {
const parentRef = React.useRef();
const onClick = () => {
if (parentRef.current) {
parentRef.current.style.border = ‘5px dashed red’;
}
};
return (
<div className={styles[‘feed’]}>
<h4 className={styles[‘feed__item-title’]}>List Container</h4>
{edges.map((edge, i) => (
<FancyRow
ref={parentRef}
key={i}
parentOnClick={onClick}
edge={edge}/>))}
</div>
);
}
We will also want to update FeedRow
and attach our click handler from props as the event handler for the button.
const FeedRow = (props): React.Element<’div’> => {
return (
<div ref={props.parentRef} className={styles[‘feed__item’]} >
<div className={styles[‘feed__item-meta’]}>
<button onClick={() => props.parentOnClick()}>
Container Button
</button>
</div>
</div>
);
};
If we click any button from our list, only the final container will render a dashed, red border. As the docs state, the object returned by will persist for the full lifetime of the component. Our reference to the DOM node of FeedRow
is set once in the lifecycle of Feed as it renders its children elements.
Create the ref object using createRef
In contrast to useRef
, createRef
does not preserve the ref instance to the DOM in a child and will always create a new reference.
In applications where children are dynamically rendered based on data, we want to assign new ref objects for each child element on the initial render. We can store a list of refs in the local state of Feed
const [elements] = useState<Array<HTMLDivElement>>(
Array(edges.length)
.fill(0)
.map(() => React.createRefuseState<Array<?HTMLDivElement>>())
);
Revisiting our top-level parent component, we pass our reference array an index value as we map over our data.
// Parent
const onClick = (rowIndex: number): void => {
if (elements[rowIndex]) {
elements[rowIndex].current.style.border = '5px dashed red';
}
};
...
{edges.map((edge, i) => (
<FancyRow
rowIndex={i}
ref={elements[i]}
key={i}
parentOnClick={onClick}
edge={edge}>
</FancyRow>))}
And by threading the index prop from the child component as a parameter to the click handler function, we can get the correct DOM reference:
const FeedRow = (props): Element<’div’> => {
return (
<div ref={props.parentRef} className={styles[‘feed__item’]} >
<div className={styles[‘feed__item-meta’]}>
<button onClick={() =>
props.parentOnClick(props.rowIndex)}>
Container Button</button>
</div>
</div>);
};
And there you have it. Use this pattern to dynamically assign refs to children in order to reference their DOM nodes.
Thanks for reading!
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch