Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove forwardRef references from useRef and Manipulating the DOM with Refs pages #7364

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 24 additions & 68 deletions src/content/learn/manipulating-the-dom-with-refs.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,75 +343,39 @@ Read more about [how this helps find bugs](/reference/react/StrictMode#fixing-bu

## Accessing another component's DOM nodes {/*accessing-another-components-dom-nodes*/}

When you put a ref on a built-in component that outputs a browser element like `<input />`, React will set that ref's `current` property to the corresponding DOM node (such as the actual `<input />` in the browser).
<Pitfall>
Refs are an escape hatch that should be used sparingly. Manually manipulating _another_ component's DOM nodes makes your code even more fragile.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so i'm wondering if this is a bit too strong here... we're describing legit cases on this page (like focusing) but this makes it sound like even the stuff below is fragile

</Pitfall>

However, if you try to put a ref on **your own** component, like `<MyInput />`, by default you will get `null`. Here is an example demonstrating it. Notice how clicking the button **does not** focus the input:
You can pass refs from parent component to child components [just like any other prop](/learn/passing-props-to-a-component).

<Sandpack>

```js
```js {3-4,9}
import { useRef } from 'react';

function MyInput(props) {
return <input {...props} />;
function MyInput({ref}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same nit

return <input ref={ref} />;
}

export default function MyForm() {
function MyForm() {
const inputRef = useRef(null);

function handleClick() {
inputRef.current.focus();
}

return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
return <><MyInput ref={inputRef} /></>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this <> fragment isn't doing anything here

}
```

</Sandpack>

To help you notice the issue, React also prints an error to the console:

<ConsoleBlock level="error">

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

</ConsoleBlock>

This happens because by default React does not let a component access the DOM nodes of other components. Not even for its own children! This is intentional. Refs are an escape hatch that should be used sparingly. Manually manipulating _another_ component's DOM nodes makes your code even more fragile.

Instead, components that _want_ to expose their DOM nodes have to **opt in** to that behavior. A component can specify that it "forwards" its ref to one of its children. Here's how `MyInput` can use the `forwardRef` API:

```js
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
```

This is how it works:

1. `<MyInput ref={inputRef} />` tells React to put the corresponding DOM node into `inputRef.current`. However, it's up to the `MyInput` component to opt into that--by default, it doesn't.
2. The `MyInput` component is declared using `forwardRef`. **This opts it into receiving the `inputRef` from above as the second `ref` argument** which is declared after `props`.
3. `MyInput` itself passes the `ref` it received to the `<input>` inside of it.
In the above example, a ref is created in the parent component, `MyForm`, and is passed to the child component, `MyInput`. `MyInput` then passes the ref to `<input>`. Because `<input>` is a [built-in component](/reference/react-dom/components/common) React sets the `.current` property of the ref to the `<input>` DOM element.

Now clicking the button to focus the input works:
The `inputRef` created in `MyForm` now points to the `<input>` DOM element returned by `MyInput`. A click handler created in `MyForm` can access `inputRef` and call `focus()` to set the focus on `<input>`.

<Sandpack>

```js
import { forwardRef, useRef } from 'react';
import { useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
function MyInput({ref}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing

return <input ref={ref} />;
}

export default function Form() {
export default function MyForm() {
const inputRef = useRef(null);

function handleClick() {
Expand All @@ -431,33 +395,27 @@ export default function Form() {

</Sandpack>

In design systems, it is a common pattern for low-level components like buttons, inputs, and so on, to forward their refs to their DOM nodes. On the other hand, high-level components like forms, lists, or page sections usually won't expose their DOM nodes to avoid accidental dependencies on the DOM structure.

<DeepDive>

#### Exposing a subset of the API with an imperative handle {/*exposing-a-subset-of-the-api-with-an-imperative-handle*/}

In the above example, `MyInput` exposes the original DOM input element. This lets the parent component call `focus()` on it. However, this also lets the parent component do something else--for example, change its CSS styles. In uncommon cases, you may want to restrict the exposed functionality. You can do that with `useImperativeHandle`:
In the above example, the ref passed to `MyInput` is passed on to the original DOM input element. This lets the parent component call `focus()` on it. However, this also lets the parent component do something else--for example, change its CSS styles. In uncommon cases, you may want to restrict the exposed functionality. You can do that with [`useImperativeHandle`](/reference/react/useImperativeHandle):

<Sandpack>

```js
import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';
import { useRef, useImperativeHandle } from "react";

const MyInput = forwardRef((props, ref) => {
const MyInput = ({ ref }) => {
Copy link
Member

@gaearon gaearon Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should just be a function declaration now, no need for arrow

function MyInput({ ref }) {
  // ...
}

const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
return <input ref={realInputRef} />;
};

export default function Form() {
const inputRef = useRef(null);
Expand All @@ -469,17 +427,15 @@ export default function Form() {
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
<button onClick={handleClick}>Focus the input</button>
</>
);
}
```

</Sandpack>

Here, `realInputRef` inside `MyInput` holds the actual input DOM node. However, `useImperativeHandle` instructs React to provide your own special object as the value of a ref to the parent component. So `inputRef.current` inside the `Form` component will only have the `focus` method. In this case, the ref "handle" is not the DOM node, but the custom object you create inside `useImperativeHandle` call.
Here, `realInputRef` inside `MyInput` holds the actual input DOM node. However, [`useImperativeHandle`](/reference/react/useImperativeHandle) instructs React to provide your own special object as the value of a ref to the parent component. So `inputRef.current` inside the `Form` component will only have the `focus` method. In this case, the ref "handle" is not the DOM node, but the custom object you create inside [`useImperativeHandle`](/reference/react/useImperativeHandle) call.

</DeepDive>

Expand Down Expand Up @@ -591,7 +547,7 @@ export default function TodoList() {
const newTodo = { id: nextId++, text: text };
flushSync(() => {
setText('');
setTodos([ ...todos, newTodo]);
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
Expand Down
22 changes: 10 additions & 12 deletions src/content/reference/react/useRef.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,16 +448,16 @@ button { display: block; margin-bottom: 20px; }
#### Exposing a ref to your own component {/*exposing-a-ref-to-your-own-component*/}
Sometimes, you may want to let the parent component manipulate the DOM inside of your component. For example, maybe you're writing a `MyInput` component, but you want the parent to be able to focus the input (which the parent has no access to). You can use a combination of `useRef` to hold the input and [`forwardRef`](/reference/react/forwardRef) to expose it to the parent component. Read a [detailed walkthrough](/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes) here.
Sometimes, you may want to let the parent component manipulate the DOM inside of your component. For example, maybe you're writing a `MyInput` component, but you want the parent to be able to focus the input (which the parent has no access to). You can create a `ref` in the parent and pass the `ref` as prop to the child component. Read a [detailed walkthrough](/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes) here.
<Sandpack>
```js
import { forwardRef, useRef } from 'react';
import { useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
const MyInput = ({ref}) => {
Copy link
Member

@gaearon gaearon Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we generally put spaces around destructuring like ({ ref }).

the rest of the docs also uses a function declaration style, like function Input({ ref }), no need for an arrow here

return <input ref={ref} />;
};

export default function Form() {
const inputRef = useRef(null);
Expand Down Expand Up @@ -554,7 +554,7 @@ You might get an error in the console:
<ConsoleBlock level="error">
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
TypeError: Cannot read properties of null
</ConsoleBlock>
Expand All @@ -573,20 +573,18 @@ export default function MyInput({ value, onChange }) {
}
```
And then wrap it in [`forwardRef`](/reference/react/forwardRef) like this:
```js {3,8}
import { forwardRef } from 'react';
And then add `ref` to the list of props your component accepts and pass `ref` as a prop to the relevent child [built-in component](/reference/react-dom/components/common) like this:
const MyInput = forwardRef(({ value, onChange }, ref) => {
```js {1,6}
const MyInput = ({ value, onChange, ref}) => {
Copy link
Member

@gaearon gaearon Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should remain a function declaration, no need to change it to an arrow

export default function MyInput({ value, onChange, ref }) {
 // ...
}

also don't miss the space after ref, it should be ref }) rather than ref})

return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
);
});
};

export default MyInput;
```
Expand Down
Loading