We've grouped the three variables we track during a gesture into a single object ref. I think it's more convenient, and communicates the intent better than just having some separate refs floating around your code with no clear relationship.
So, if your ref contents is a box itself, you don't need one more box to carry the first one around. Also, if you have several related refs anyways, why not put them into one box?
That's it for the stuff I use frequently. There are two more cases that work the same with or without a useRef
, but they're very fragile and I wouldn't rely on these. Still, they'd be interesting to cover.
OK, let
variable resets on re-render. Then, if our component never re-renders, maybe we're safe skip the useRef
and just use a let
:
const Icon = memo(() => {
let clicks = 0;
const onClick = () => {
clicks++;
console.log(clicks);
};
return <SomeStaticSVG onClick={onClick} />;
}, () => true);
Not using any props in a component and slapping a memo
on it is not enough — we could pass a useless prop and change it, like <Icon gotcha={Math.random()} />
— React doesn't know if we care for gotcha
. An extra hint in our memo comparator does the job. Hooks that can re-render our component are a no-go, too — useState
, useReducer
, useContext
, or any custom hooks based on these.
Components like this one are not as useless as you might think — I've actually made an optimized icon pack with a similar pattern. Still, the lack of props is very limiting. But the major problem with this code is that React doesn't give any guarantees about memo
— at some point it might start discarding old values to free memory, resetting your precious clicks. Dangerous!
A slightly more practical (yet still sloppy) scenario is using a ref only inside callbacks that are created in the first render and cached forever. Yes, we reset the value on every render, but who cares if all the function that use it are stuck in the scope of the first render:
function Swiper(p) {
let clicks = 0;
const onClick = useRef(() => {
clicks++;
console.log(clicks);
}).current;
return <div onClick={onClick}>click me</div>
}
useCallback(..., [])
won't cut it, since, again, react does not actually guarantee it will cache forever. With an explicit constant useRef
we're safe, but the whole thing explodes if you ever need to capture a state/props in a callback, and rewrite it to useCallback
or remove caching altogether. Not recommended.
For the sake of an argument, let's assume I find .current
absolutely inacceptable for religious reasons. What could I do to never type it again? There's a whole bunch of solutions if I'm really determined.
A least adventurous option is a custom hook that's just like a default ref, but replaces current
with a different name. v
is fine — it's short, it stands for Value, and it's a fine-looking letter. Here we go:
// inner object is the ref-box now
const useV = (init) => useRef({ v: init }).current;
// use as follows
const startX = useV(0);
return <div
onTouchStart={(e) => startX.v = e.clientX}
onTouchMove={(e) => setOffset(e.clientX - startX.v)}
style={{ transform: `translateX(${offset}px)` }}
>{children}</div>
But that's boring. What if we always put all the refs in a component into a large object? Anything we can do with multiple refs is doable with a single one. Looks like something a person who hates hooks but is forced to use them could do:
// hope you're old enough to get this hommage
const that = useRef({
startX: 0,
// WOW we can even have CLASS METHODS back!
onTouchStart(e) {
this.startX = e.clientX;
},
onTouchMove(e) {
// And call state update handles since they're stable
setOffset(e.clientX - this.startX);
},
}).current;
return <div
onTouchStart={that.onTouchStart}
onTouchMove={that.onTouchMove}
style={{ transform: `translateX(${offset}px)` }}
>{children}</div>
The fact that we can have methods on that large stateful object is very exciting. On a sadder note, we can't read current props or state, because they don't have a stable reference. We could start copying props into that
, but the very idea of "current props" gets fuzzy once you enter concurrent mode, and I'm not going to die on this (ha, this
) hill, or at least not today.
In an unexpected twist, we could even move ref management into a HOC. Remember createReactClass? Well, it's back:
const makeComponent = descriptor => props => {
const scope = useRef(descriptor).current;
return scope.render(props);
};
const Swiper = makeComponent({
// you can't use arrows because you need "this"
render(props) {
// any hooks in render() are OK:
const [value, setValue] = useState(0);
return <div onClick={this.onClick} {...props} />;
},
clicks: 0,
onClick() {
console.log(this.clicks++);
},
});
Apart from the missing props / state access, these solutions have other downsides:
useRef
can work around that, though.Anyways, { current }
is not the only object shape that could work as a ref. What else can we do?
Objects are not the only JS thing that can be a stable container for a changing value. Let's try a function instead! (Don't get me started with (() => {}) instanceof Object
, functions are clearly not objects). First, let's try a polymorphic handle that can both get and set the value:
function useFunRef(init) {
const ref = useRef(init);
const handle = useRef((...args) => {
// if we pass an argument, update the value
if (args.length) {
ref.current = args[0];
}
return ref.current;
}).current;
return handle;
}
Using it is simple: you either call the handle with no arguments to get the current value, or with a new value to update:
const [offset, setOffset] = useState(0);
const nodeRef = useFunRef();
const startX = useFunRef(0);
return <div
onTouchStart={(e) => startX(e.touches[0].clientX)}
onTouchMove={(e) => setOffset(e.touches[0].clientX - startX())}
ref={nodeRef}
style={{ transform: `translateX(${offset}px)` }}
>{children}</div>
I like how this one integrates with DOM refs thanks to the callback-ref syntax. As an added advantage, functions should be faster to create (then throw away) than objects. And, since you're using more functions, your programming clearly becomes more functional.
If you don't like functions that do different things depending on the number of arguments, we can separate the getter and the setter, similarly to what useState
does:
function useStateRef(init) {
const ref = useRef(init);
const setter = useRef((v) => ref.current = v).current;
const getter = useRef(() => ref.current).current;
return [getter, setter];
}
// usage example
const [startX, setStartX] = useStateRef(0);
return <div
onTouchStart={(e) => setStartX(e.clientX)}
onTouchMove={(e) => setOffset(e.clientX - startX())}
>{children}</div>
So yes, a function can be a ref-box, too. That's good to know. Is there anything else?
Until now, we've been playing with the box shape without straying too far from the overall concept. But maybe that's what we call "a poultice for a dead man" in Russia? (English tip: a poultice is a warm bag of herbs used in traditional medicine. It surely won't help if you're dead. I learnt this word just to write this post.) What if we don't need a box?
Component scope resets on every render. Fine, we need another scope to store our value. Module scope is too drastic — can we just get one that persists between re-renders, but is unique to every component? I'm the master of my scopes, so why not:
function makeClicker() {
// this is the outer / instance scope
let clicks = 0;
// we can declare callbacks here
const onClick = () => console.log(clicks++);
return (props) => {
// this is the inner / render scope
return <div onClick={onClick} {...props} />;
}
}
function Clicker(props) {
// Now we need to manage the instance scope
const render = useRef(makeClicker()).current;
// and turn it into a regular component
return render(props);
};
While we're at it, more of the same can be done with a generator — sure, we can only return
once, but why not yield
our JSX on every render instead?
function* genClicker(props) {
let clicks = 0;
const onClick = () => console.log(clicks++);
while (true) {
props = yield (<div
onClick={onClick}
{...props}
/>);
}
}
function Clicker(props) {
const render = useRef(genClicker(props)).current;
return render.next(props).value;
}
In both cases, we can't use hooks in the outer scope. If we were to turn clicks
into state, we couldn't do it like this:
const makeClicker = () => {
const [clicks, setClicks] = useState(0);
const onClick = () => setClicks(c => c + 1);
return (props) => {
return <div onClick={onClick}>{clicks}</div>;
}
};
It doesn't explode, since we happen to call useState
on every render (because we call makeClicker
on every render and throw it away), but clicks
will be stuck at 0 — it's a const
from the first render. We're free to use hooks both in our inner scope and the Swiper
wrapper, though. This also means that we can't use our outer refs to cache state update / dispatch handles, which I liked very much.
These concepts are very interesting, because they're in line with the hooks mindset: minimal object use (good for memory & minification) and creative handling of JS scopes. At the same time, we don't need an object box to host our ref! Also, if we manage to build a lazy ref for out instance scope, we skip recreating useless variables and callbacks on every render, which is pleasant. The syntax and the limitations on hooks in the outer scope are sad, but I feel like they can be worked around (maybe something like clicks = yield useGenState(0)
). Promising.
In this article, we've seen why useRef
has that weird .current
property, and learnt some tricks to write .current
less:
const onClear = useRef(() => setValue('')).current;
refs
into a mutable ref-object, and mutate that instead of current
: pos = useRef({ x: 0, y: 0 }).current
, read with pos.x
, write with pos.x = e.clientX()
In some cases, you could drop the useRef
and use a simple let
variable instead, but I don't recommend it.
To stimulate our imagination, we've also implemented seven alternate APIs on top of the default useRef
that don't use .current
:
useV(0).v
makeComponent
factory that lets you put the render function, along with some properties and methods, into an object, yet still allows for hooks.useRefs
: a useState
-like one that has separate get and set handles: const [getX, setX] = useStateRef(0)
, and one with a single handle.Maybe this wasn't very useful (I'm not eager to rewrite all my code using these patterns), but I hope it was great fun (it sure was for me). React is amazingly flexible, which is why I love it. Hope this mental exercise got you excited. See you later!