Finalize migration from talk slides to article

This commit is contained in:
Corbin Crutchley
2020-10-15 20:43:55 -07:00
parent 73d7fa9553
commit e1699cc942

View File

@@ -10,23 +10,21 @@
}
---
# What is a React Ref?
Programming terminology can be rather confusing. The first time I'd heard about "React Refs", it was in the context of [getting a reference to a DOM node](#dom-ref). However, with the introduction of hooks, the `useRef` hook has expanded the definition of "refs".
- Two distinct things
- A "useRef" hook for mutable data properties
- Can be only used in functional components
- Does the same thing as "class instance variables" in class components
- A method of referencing DOM Elements
- Handled with "useRef" hook in functional components
- Use "React.createRef" for class components
Today, we'll be walking through two definitions of refs:
- A [mutable data property](#use-ref-mutate) to persist data across renders
- A [reference to DOM elements](#dom-ref)
# Mutable Data Storage
We'll also be exploring additional functionality to each of those two defintions, such as [component refs](#forward-ref), [adding more properties to a ref](#use-imperative-handle), and even exploring [common code gotchas associated with using `useRef`](#refs-in-use-effect).
- Exposed by "useRef" hook
- Utilizes "current" property to store data
- Utilizes "pass-by-reference"
> As most of this content relies on the `useRef` hook, we'll be using functional components for all of our examples. However, there are APIs such as [`React.createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) and [class instance variables](https://www.seanmcp.com/articles/storing-data-in-state-vs-class-variable/) that can be used to recreate `React.useRef` functionality with classes.
# Mutable Data Storage {#use-ref-mutate}
While `useState` is the most commonly known hook for data storage, it's not the only one on the block. React's `useRef` hook functions differently from `useState`, but they're both used for persisting data across renders.
```jsx
const ref = React.useRef();
@@ -34,6 +32,18 @@ const ref = React.useRef();
ref.current = "Hello!";
```
In this example, `ref.current` will contain `"Hello!"` after the intial render. The returned value from `useRef` is an object that contains a single key: `current`.
If you were to run the following code:
```jsx
const ref = React.useRef();
console.log(ref)
```
You'd find a `{current: undefined}` printed to the console. This is the shape of all React Refs. If you look at the TypeScript definition for the hooks, you'll see something like this:
```typescript
// React.d.ts
@@ -44,6 +54,10 @@ interface MutableRefObject {
function useRef(): MutableRefObject;
```
Why does `useRef` rely on storing data inside of a `current` property? It's so that you can utilize JavaScript's "pass-by-reference" functionality in order to avoid renders.
Now, you might think that the `useRef` hook is implemented something like the following:
```jsx
// This is NOT how it's implemented
function useRef(initial) {
@@ -66,15 +80,22 @@ function useRef(initial) {
}
```
- Don't cause re-renders on data change
However, that's not the case. [To quote Dan Apromov](https://github.com/facebook/react/issues/14387#issuecomment-493676850):
https://github.com/facebook/react/issues/14387#issuecomment-493676850 (screenshot this comment)
> ... `useRef` works more like this:
>
> ```jsx
> function useRef(initialValue) {
> const [ref, ignored] = useState({ current: initialValue })
> return ref
> }
> ```
- Useful for non-rendered data
- EG: Timers, animations
Because of this implementation, when you mutate the `current` value it will not cause a re-render.
Thanks to the lack of rendering on data storage, it's particularly useful for storing data that you need to keep a reference to, but don't need to render on-screen. One such example of this would be a timer:
```jsx
const dataRef = React.useRef();
@@ -92,14 +113,13 @@ https://github.com/facebook/react/issues/14387#issuecomment-493676850 (screensho
}, [dataRef]);
```
<iframe src="https://stackblitz.com/edit/react-use-ref-mutable-data?ctl=1&embed=1" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
https://stackblitz.com/edit/react-use-ref-mutable-data
# Visual Timer with Refs {#visual-timers}
# Visual Timer with Refs
While there are usages for timers without rendered values, what were to happen if we made the timer render a value in state?
- Use timer to set state
- Assign timer to ref
- Render the timer value
Let's take the example from before, but inside of the `setInterval`, we update a `useState` that contains a number to add one to it's state.
```jsx
const dataRef = React.useRef();
@@ -123,10 +143,13 @@ https://stackblitz.com/edit/react-use-ref-mutable-data
);
```
https://stackblitz.com/edit/react-use-ref-mutable-buggy-code
Now, we'd expect to see the timer update from `1` to `2` (and beyond) as the timer continues to render. However, if we look at the app while it runs, we'll see some behavior we might not expect:
- Closures can get stale
- Solved by "passing by reference"*
<iframe src="https://stackblitz.com/edit/react-use-ref-mutable-buggy-code?ctl=1&embed=1" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
This is because [the closure](https://whatthefuck.is/closure) that's passed to the `setInterval` has grown stale. This is a common problem when using React Hooks. While there's a simple solution hidden in `useState`'s API, let's solve this problem using mutations and `useRef`.
Because `useRef` relies on passing by reference and mutating that reference, if we simply introduce a second `useRef` and mutate it on every render to match the `useState` value, we can work around the limitations with the stale closure.
```jsx
const dataRef = React.useRef();
@@ -148,11 +171,30 @@ https://stackblitz.com/edit/react-use-ref-mutable-buggy-code
}, [dataRef]);
```
https://stackblitz.com/edit/react-use-ref-mutable-fixed-code
<iframe src="https://stackblitz.com/edit/react-use-ref-mutable-fixed-code?ctl=1&embed=1" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
> * I would not solve it this way in production. `useState` accepts a callback which...
> * I would not solve it this way in production. `useState` accepts a callback which you can use as an alternative (much more recommended) route:
>
> ```jsx
> const dataRef = React.useRef();
>
> const [timerVal, setTimerVal] = React.useState(0);
>
> const clearTimer = () => {
> clearInterval(dataRef.current);
> };
>
> React.useEffect(() => {
> dataRef.current = setInterval(() => {
> setTimerVal(tVal => tVal + 1);
> }, 500);
>
> return () => clearInterval(dataRef.current);
> }, [dataRef]);
> ```
> We're simply using a `useRef` to outline one of the important properties about refs: mutation.
# DOM Element References
# DOM Element References {#dom-ref}
- Stored using "ref" attribute
- HTMLDivElement stored in "current"
@@ -204,7 +246,7 @@ Ref attribute accepts functional API
https://stackblitz.com/edit/react-use-ref-effect-style-callback
# Component References
# Component References {#forward-ref}
- Can pass "ref" to components
- Property must not be called "ref"*
@@ -278,3 +320,265 @@ const App = () => {
```
https://stackblitz.com/edit/react-use-ref-effect-style-forward-ref
# Add Data to Ref {#use-imperative-handle}
```jsx
import React from "react";
import "./style.css";
const Container = React.forwardRef(({children}, ref) => {
return <div ref={ref} tabIndex="1">
{children}
</div>
})
export default function App() {
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.focus();
}, [elRef])
return (
<Container ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</Container>
);
}
```
https://stackblitz.com/edit/react-use-imperative-handle-demo-pre
- useImperativeHandle hook allows properties to be added to ref
- Can be used in combination with forwardRef
- Only properties returned in second param are set to ref
```jsx
import React from "react";
import "./style.css";
const Container = React.forwardRef(({children}, ref) => {
const divRef = React.useRef();
React.useImperativeHandle(ref, () => ({
focus: () => {
divRef.current.focus();
console.log("I have now focused");
}
}))
return <div ref={divRef} tabIndex="1">
{children}
</div>
})
export default function App() {
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.focus();
}, [elRef])
return (
<Container ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</Container>
);
}
```
https://stackblitz.com/edit/react-use-imperative-handle-demo-post
> Be cautious about using this in production. It breaks unidirectional data binding
```jsx
React.useEffect(() => {
elRef.current.konami();
}, [elRef])
```
https://stackblitz.com/edit/react-use-imperative-handle-demo-useful
# React Refs in `useEffect ` {#refs-in-use-effect}
- `useEffect` only does the array check on re-render
- Ref's current property set doesn't trigger a re-render
```jsx
export default function App() {
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.style.background = "lightblue";
}, [elRef]);
return (
<div ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
}
```
https://stackblitz.com/edit/react-use-ref-effect-style
However, what happens when you make the `div` render happen _after_ the initial render. What do you think will happen here?
```jsx
export default function App() {
const elRef = React.useRef();
const [shouldRender, setRender] = React.useState(false);
React.useEffect(() => {
if (!elRef.current) return;
elRef.current.style.background = 'lightblue';
}, [elRef.current])
React.useEffect(() => {
setTimeout(() => {
setRender(true);
}, 100);
}, []);
return !shouldRender ? null : (
<div ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
}
```
https://stackblitz.com/edit/react-use-ref-effect-bug-effect
```jsx
const [minus, setMinus] = React.useState(0);
const ref = React.useRef(0);
const addState = () => {
setMinus(minus + 1);
};
const addRef = () => {
ref.current = ref.current + 1;
};
React.useEffect(() => {
console.log(`ref.current:`, ref.current);
}, [ref.current]);
React.useEffect(() => {
console.log(`minus:`, minus);
}, [minus]);
```
https://stackblitz.com/edit/react-use-ref-not-updating
Here are some comments from Dan Apromov, of the React Core team:
https://github.com/facebook/react/issues/14387#issuecomment-503616820
https://twitter.com/dan_abramov/status/1093497348913803265
https://github.com/facebook/react/issues/14387#issuecomment-493677168
But what does Dan mean by "callback ref"?
# Callback Refs {#callback-refs}
- Remember, "ref" property accepts a function to access the node
- Because it's not using "useEffect", code can execute in proper timing
```jsx
const elRefCB = React.useCallback(node => {
if (node !== null) {
node.style.background = "lightblue";
}
}, []);
return !shouldRender ? null : (
<div ref={elRefCB}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
```
https://stackblitz.com/edit/react-use-ref-callback-styling
- Can still keep a traditional "useRef" reference
```jsx
const elRef = React.useRef();
console.log("I am rendering");
const elRefCB = React.useCallback(node => {
if (node !== null) {
node.style.background = "lightblue";
elRef.current = node;
}
}, []);
React.useEffect(() => {
console.log(elRef.current);
}, [elRef, shouldRender]);
```
https://stackblitz.com/edit/react-use-ref-callback-and-effect
# `useState` Refs {#usestate-refs}
- Combining useState and Callback Refs
- Will trigger a re-render
- Works in useEffect
```jsx
const [elRef, setElRef] = React.useState();
console.log('I am rendering');
const elRefCB = React.useCallback(node => {
if (node !== null) {
setElRef(node);
}
}, []);
React.useEffect(() => {
console.log(elRef);
}, [elRef])
```
https://stackblitz.com/edit/react-use-ref-callback-and-use-state
- Can be used to impact reference using useEffect instead of inside of callback
```jsx
const [elNode, setElNode] = React.useState();
const elRefCB = React.useCallback(node => {
if (node !== null) {
setElNode(node);
}
}, []);
React.useEffect(() => {
if (!elNode) return;
elNode.style.background = 'lightblue';
}, [elNode])
```
https://stackblitz.com/edit/react-use-ref-callback-and-state-effect
# Conclusion