Mutable and immutable useRef semantics with React & TypeScript (2024)

Wojciech Matuszewski

Posted on

Mutable and immutable useRef semantics with React & TypeScript (3) Mutable and immutable useRef semantics with React & TypeScript (4) Mutable and immutable useRef semantics with React & TypeScript (5) Mutable and immutable useRef semantics with React & TypeScript (6) Mutable and immutable useRef semantics with React & TypeScript (7)

#typescript #react #webdev #hooks

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 looking at how to make the current property immutable, mutable, and know without much effort if the ref is one or the other.

All the behavior I'm going to talk about is only relevant in the context of TypeScript. The mutability / immutability is enforced at type level, not runtime level.

Immutable current property

The immutable semantics of the useRef hooks are usually used with DOM elements. A common use-case might be to get the ref of an element and focus that element whenever a button is clicked.

Here is how I would write that.

import * as React from "react";const Component = () => { const inputRef = React.useRef<HTMLInputElement>(null); return ( <div> <input type="text" name="name" ref={inputRef} /> <button type="button" onClick={() => inputRef.current?.focus()}> Click to focus the input </button> </div> );};

Notice the type and the value I’ve initialized the useRef with. The semantics I’ve used signal that I’m relying on React to manage the ref for me. In our case, this means that I cannot mutate the inputRef.current. If I ever tried to do that, TypeScript would complain.

import * as React from "react";const Component = () => { const inputRef = React.useRef<HTMLInputElement>(null); return ( <div> {/* Cannot assign to 'current' because it is a read-only property */} <input type = "text" ref = {callbackRefValue => inputRef.current = callbackRefValue}> <button type="button" onClick={() => inputRef.current?.focus()}> Click to focus the input </button> </div> );};

After writing similar code for a while, I’ve created a rule of thumb I follow to understand if the ref that I’m looking is immutable.

If the useRef is initialized with null and the initial value does not belong to the provided type, the current property is immutable.

In our case, the null initial value does not belong to the type HTMLInputElement so the current property cannot be mutated.

Mutable current property

To have the current property of the ref be mutable, we need to change how we are declaring ref itself.

Suppose we are writing a component that deals with timers. The useRef hook is an ideal candidate to hold a reference to a timer. With the timer reference at hand, we can make sure that we clear the timer when the component unmounts.

Here is an, albeit a bit contrived, example.

import * as React from "react";const Component = () => { const timerRef = React.useRef<number | null>(null); // This is also a valid declaration // const timerRef = React.useRef<number>() React.useEffect(() => { // Mutation of the `current` property timerRef.current = setTimeout(/* ... */) return clearInterval(timerRef.current) }, []) return ( // ... );};

Since in the beginning, I have no way to know what the reference to the later declared setTimeout might be, I've initialized the useRef with null. Apart from the types, the declaration of the ref might seem eerily similar to the one in the Immutable current property section.
However, since the initially provided value (in our case null) wholly belongs to the type I've declared the useRef with (number | null), the current property is allowed to be mutable.

Similarly to the immutable current property case, here is my rule of thumb.

If the useRef is initialized with a value that belongs to the provided type, the current property of the ref is mutable.

In our case, the null initial value belongs to the type number | null so the current property can be mutated.
As an alternative, I could have declared the timerRef variable the following way

const timerRef = React.useRef<number>(); // the `timerRef.current` is also mutable

Why is the current allowed to be mutated in this case? Because the timerRef is implicitly initialized with the undefined value. The undefined value belongs to the type I've declared the timerRef - the React.useRef typings are overloaded depending on the type of the initial value.

const timerRef = React.useRef<number>();// Really isconst timerRef = React.useRef<number>(undefined);// The `React.useRef` type definitions specify an overload whenever the type of the initial value is `undefined`function useRef<T = undefined>(): MutableRefObject<T | undefined>; // Notice the `MutableRefObject`.

Summary

When I started working with React & TypeScript, I found the difference between mutable and immutable refs quite confusing. I hope that this article was helpful and cleared some of the questions you might have had on the subject matter.

You can find me on twitter - @wm_matuszewski.

Thank you for your time.

Top comments (11)

Subscribe

Maciek Grzybek

Maciek Grzybek

Senior software developer with ❤️ for JS, TS and React. Check out my blog www.maciekgrzybek.dev

  • Location

    Białystok

  • Work

    Senior Frontend Developer @ Ramp.Network

  • Joined

Jun 12 '21

  • Copy link

Nice 😊II like reading about these kinds of nuances 👌

Frederic CHAPLIN

Frederic CHAPLIN

  • Location

    Montpellier, France

  • Work

    Senior Fullstack Developer

  • Joined

Jun 13 '21

  • Copy link

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

Wojciech Matuszewski

Wojciech Matuszewski

Doing stuff with JavaScript / Golang.Probably coding / working or at the gym.

  • Work

    Serverless Engineer at StediInc

  • Joined

Jun 13 '21

  • Copy link

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...

Frederic CHAPLIN

Frederic CHAPLIN

  • Location

    Montpellier, France

  • Work

    Senior Fullstack Developer

  • Joined

Jun 13 '21 • Edited on Jun 13 • Edited

  • Copy link

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 🙂.

Nicholas Boll

Nicholas Boll

  • Joined

Jul 1 '21 • Edited on Jul 1 • Edited

  • Copy link

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

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

Frederic CHAPLIN

Frederic CHAPLIN

  • Location

    Montpellier, France

  • Work

    Senior Fullstack Developer

  • Joined

Jul 2 '21 • Edited on Jul 2 • Edited

  • Copy link

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

or

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

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>'.

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.

Nicholas Boll

Nicholas Boll

  • Joined

Jul 7 '21 • Edited on Jul 7 • Edited

  • Copy link

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 assignedconst 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, [])}

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

// util filefunction useElementRef<E extends keyof ElementTagNameMap>(element: E) { return React.useRef<ElementTagNameMap[E]>(null)}// usageconst ref = useElementRef('div') // React.RefObject<HTMLDivElement>

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?

Frederic CHAPLIN

Frederic CHAPLIN

  • Location

    Montpellier, France

  • Work

    Senior Fullstack Developer

  • Joined

Aug 14 '21

  • Copy link

Because in this example case Typescript may :

  • throw exceptions at compile time
  • and give intent to the reader
  • without adding more code.

Dr. Derek Austin 🥳

Dr. Derek Austin 🥳

  • Joined

Sep 20 '22

  • Copy link

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.

kazamov

kazamov

  • Joined

Jun 13 '21

  • Copy link

Thanks for the explanation!

Essential Randomness

Essential Randomness

Senior Software Engineer. Manic Entrepreneur Edgy Girl.

  • Work

    Full Stack Developer

  • Joined

Dec 8 '21

  • Copy link

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

For further actions, you may consider blocking this person and/or reporting abuse

Mutable and immutable useRef semantics with React & TypeScript (2024)
Top Articles
Top 10 Best Trait Mods for Stellaris (All Free) – FandomSpot
The 10 Best Planet Mods for Stellaris – FandomSpot
Nerdwallet Chase
Cecil Burton Funeral Home | Shelby, North Carolina
Syracuse Pets Craigslist
Gasbuddy Joliet
Dippin Base Stat Total
Gasbuddy Costco Hawthorne
Uta Kinesiology Advising
Csl Plasma Birthday Bonus
799: The Lives of Others - This American Life
Sdn Wright State 2023
Savage X Fenty Wiki
Deshaun Watson Timeline: What Has Occurred Since First Lawsuit Filed
Everything You Might Want to Know About Tantric Massage - We've Asked a Pro
A Comprehensive Guide to Redgif Downloader
Nsu Kpcom Student Handbook
Ups Cc Center
Farmers And Merchants Bank Broadway Va
Po Box 6726 Portland Or 97228
What's the Difference Between Halal and Haram Meat & Food?
Shooters Lube Discount Code
AT&T Mission | Cell Phones, Wireless Plans & Accessories | 2409 E Interstate Highway 2, Mission, TX | AT&T Store
Nccer Log In
Ice Quartz Osrs
Kentuky Fried Chicken Near Me
David Goggins Is A Fraud
Greatpeople.me Login Schedule
The Autopsy of Jane Doe - Kritik | Film 2016 | Moviebreak.de
Jockey Standings Saratoga 2023
Tqha Yearling Sale 2023 Results
Used Drift Boats For Sale Craigslist
Banette Gen 3 Learnset
Closest Postal Service To My Location
Wells Fargo Hiring Hundreds to Develop New Tech Hub in the Columbus Region
Joe Bartlett Wor Salary
Porter House Ink Photos
Lildeadjanet
African American Thursday Blessings Gif
Where Is Item Number On Stanley Cup
236 As A Fraction
Alles, was ihr über Saison 03 von Call of Duty: Warzone 2.0 und Call of Duty: Modern Warfare II wissen müsst
Rydell on LinkedIn: STARTING TODAY you no longer have to wait in a long line to get your oil…
Uw Oshkosh Wrestling
Ticketmaster La Dodgers
Research Tome Neltharus
Blow Dry Bar Boynton Beach
Slushy Leaks
Yoshidakins
Swim University Chemical Calculator
Vegansoda Mfc
Six Broadway Wiki
Latest Posts
Article information

Author: Dong Thiel

Last Updated:

Views: 5622

Rating: 4.9 / 5 (79 voted)

Reviews: 94% of readers found this page helpful

Author information

Name: Dong Thiel

Birthday: 2001-07-14

Address: 2865 Kasha Unions, West Corrinne, AK 05708-1071

Phone: +3512198379449

Job: Design Planner

Hobby: Graffiti, Foreign language learning, Gambling, Metalworking, Rowing, Sculling, Sewing

Introduction: My name is Dong Thiel, I am a brainy, happy, tasty, lively, splendid, talented, cooperative person who loves writing and wants to share my knowledge and understanding with you.