DEV Community

Cover image for Mutable and immutable useRef semantics with React & TypeScript

Mutable and immutable useRef semantics with React & TypeScript

Wojciech Matuszewski on June 12, 2021

In this post, you will learn how different ways declaring a ref with useRef hook influence the immutability of the current ref property. We will be...
Collapse
 
maciekgrzybek profile image
Maciek Grzybek

Nice 😊II like reading about these kinds of nuances 👌

Collapse
 
fchaplin profile image
Frederic CHAPLIN

Thank you ! A question : your constants are declared as any. Shouldn't we type every declaration in Typescript ?

Collapse
 
wojciechmatuszewski profile image
Wojciech Matuszewski

Hey, thank you for reaching out.

In TypeScript, you can leverage type inference. So, while I could explicitly annotate every variable with the corresponding type, I defer that work until necessary and rely on the notion of type inference.

You can read more about type inference here: typescriptlang.org/docs/handbook/t...

Collapse
 
fchaplin profile image
Frederic CHAPLIN • Edited

Yep, but it may be a better practice to explicitly type your constants while declaring them, especially when inferred type is not basic. You may gain time on weird issues later. And your code may be clearer when debugging 🙂.

Thread Thread
 
nicholasboll profile image
Nicholas Boll • Edited

I think explicitly typing everything makes it harder to read and now you have to understand the nuances of when you should or should not explicitly type.

const foo = 'foo' // 'foo'
const bar: string = 'bar' // string
Enter fullscreen mode Exit fullscreen mode

Not only is the second one harder to read and parse, it actually widened our type.

Thread Thread
 
fchaplin profile image
Frederic CHAPLIN • Edited

This is a really simple assignation example and I agree with you on this (except for a little typo) . But for function returns, and libs specific types, making a rule of typing explicitly everything WILL help.

Example:

const timerRef : React.MutableRefObject<number | null> = React.useRef<number | undefined>();
//mutable
Enter fullscreen mode Exit fullscreen mode

or

const inputRef: React.RefObject<HTMLInputElement> = React.useRef<HTMLInputElement>(null);
//not mutable
Enter fullscreen mode Exit fullscreen mode

By explicitly typing, you give explicitly the mutability information to other developpers (or to you in a month or two). So you improve readability.

And if you try

const inputRef: React.MutableRefObject<HTMLInputElement> = React.useRef<HTMLInputElement>(null);
//TS2322: Type 'RefObject<HTMLInputElement>' is not assignable to type MutableRefObject<HTMLInputElement>'.
Enter fullscreen mode Exit fullscreen mode

Here, typescript tell you instantly you're making a mistake: "No, it's not mutable!".

I know there are many sources that says you can use implicit types, but if you use them too much, you may lose some typescript gifts.

Thread Thread
 
nicholasboll profile image
Nicholas Boll • Edited

I'd probably argue there should be a useMutableRef and useRef rather than complicated types to communicate intent. I often have these small functions that map to normal functions to more clearly communicate intent:

const mutableRef = useMutableRef(false) // mutable, default assigned
const immutableRef = useRef<HTMLInputElement>(null) // React handles this, no default assigned

/**
 * Alias to `useEffect` that intentionally is only run on mount/unmount
 */
const useMount = (callback?: () => void) => {
  React.useEffect(callback, [])
}
Enter fullscreen mode Exit fullscreen mode

It is even possible to create nice utility functions that make element refs easier to work with:

// util file
function useElementRef<E extends keyof ElementTagNameMap>(element: E) {
  return React.useRef<ElementTagNameMap[E]>(null)
}

// usage
const ref = useElementRef('div') // React.RefObject<HTMLDivElement>
Enter fullscreen mode Exit fullscreen mode

Notice the theme where the Typescript types start to disappear for normal usage? This means you can still get the benefits of Typescript without explicitly using Typescript. Even JavaScript users of your code can benefit. This technique works better for libraries, especially if you have JavaScript users of your library. You can use JSDoc to explicitly type JS code, but that is a pain for non-primitive types.

I say there doesn't need to be a tradeoff between Typescript gifts and expressing intent. If your team only uses Typescript and understands all the types in use, maybe you don't need to spend any extra time communicating intent through functions. But it is very useful for JavaScript users in addition to Typescript users who don't spend time finding out all the nuances of Typescript type differences like useRef. You have to learn something extra either way (type differences or which function to use), but why not communicate intent explicitly through names vs types?

Thread Thread
 
fchaplin profile image
Frederic CHAPLIN

Because in this example case Typescript may :

  • throw exceptions at compile time
  • and give intent to the reader
  • without adding more code.
Thread Thread
 
doctorderek profile image
Dr. Derek Austin 🥳

I actually never type anything in TypeScript unless I have to, and I consider explicit types to be an antipattern.

In my opinion, it's easy to check VSCode's Intellisense to make sure that the right type was inferred.

In React, for example, I've never had to actually use the FC type or explicitly return JSX.Element; if I write a function component, then TypeScript catches it 100% of the time.

There are definitely certain cases where I type function returns, such as if I'm using a "pseudo enum" (union type of strings) and want to coerce the function return down from string to either "thingOne" | "thingTwo" -- so I do see your point.

Overall, I don't think it's useful for productivity or type safety to explicitly type things when the implicit type was correct, so I try to avoid it.

Collapse
 
arian94 profile image
Arian94

I did mutate an object that was created using useRef and initialized it with null like this:

useRef<Record<string, number>>(null);
Enter fullscreen mode Exit fullscreen mode

Then, mutate it like below:

if (count.current === null) {
    count.current = { foo: 0 };
} else {
    count.current.foo = count.current.foo + 1;
}
console.log(count.current); // will output {foo: 0} ... {foo: 1} ... {foo: 2} on each execution.
Enter fullscreen mode Exit fullscreen mode

The only thing happening here is that TypeScript will throw error since we are mutating a 'read-only' property but if you ignore this error, it does mutate it anyway.

Collapse
 
kazamov profile image
kazamov

Thanks for the explanation!

Collapse
 
essentialrandom profile image
Essential Randomness

Thank you!! This kept tripping me up every time.