Skip to content

Instantly share code, notes, and snippets.

@btoo
Last active September 11, 2022 15:30
Show Gist options
  • Save btoo/65e7d4303f49299c785d38f8758525e6 to your computer and use it in GitHub Desktop.
Save btoo/65e7d4303f49299c785d38f8758525e6 to your computer and use it in GitHub Desktop.
typescript type-safe version of usePrevious taken directly from the react docs https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
import { useRef, useEffect } from 'react';
/**
* a type-safe version of the `usePrevious` hook described here:
* @see {@link https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state}
*/
export function usePrevious<T>(
value: T,
): ReturnType<typeof useRef<T>>['current'] {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
@LordJohn42
Copy link

Thank you!

@jukbot
Copy link

jukbot commented Sep 8, 2021

thanks

@nemanjam
Copy link

nemanjam commented Feb 2, 2022

delete type keyword

Syntax error: Unexpected token, expected ","

> 1 | import { useRef, useEffect, type MutableRefObject } from 'react';

@btoo
Copy link
Author

btoo commented Feb 12, 2022

@nemanjam that syntax uses typescript's import type modifier, a feature that was introduced in version 4.5

if you can't support this feature yet, you can still import MutableRefObject in other ways, like

import type { MutableRefObject } from 'react';

or

import { MutableRefObject } from 'react';

@peckyboy
Copy link

Why return "MutableRefObject<T | undefined>['current']"?
Since its the value you're returning why not just
function usePrevious<T>(value:T):T|undefined{}
Except this was how it was done when this was posted then I think the above is appropriate.
It also reduces imports

@btoo
Copy link
Author

btoo commented Apr 22, 2022

@peckyboy nice question. The return type is MutableRefObject<T | undefined>['current'] in an effort to duplicate as little type-level information as possible. Using an explicit return type of T | undefined would be to assert that useRef<T>().current has the type T | undefined (at least based on the usePrevious function body), but usePrevious shouldn't have the right to make this assertion when it is expressly using (and more specifically, not declaring) useRef in its implementation details. Therefore, to be more correct would be to infer the useRef<T>().current type as directly as possible from the useRef<T>().current runtime code instead. Doing so ensures that, if useRef's implementation ever changes (thus, possibly changing the type of useRef<T>().current) from under our noses, we can maximize the chances that our typings are correct. In effect, using MutableRefObject<T | undefined>['current'] is more future-proof (and more importantly, more correct) than T | undefined.

Some might criticize the hardcoding of T | undefined in MutableRefObject<T | undefined>['current'] to itself be a duplication of type-level information, and their criticisms would be warranted. There's no gaurantee that the implementation of usePrevious returns MutableRefObject<T | undefined>['current']. That is, simply by looking at only the usePrevious function body and not the implementations of any of usePrevious's dependencies, like useRef, we can not deterministically claim the return type to be MutableRefObject<T | undefined>['current']. However, to these criticisms, I would say that MutableRefObject<T | undefined>['current'] duplicates less type information than T | undefined and is therefore still better.

Taking this a step further, I intend on updating this file to leverage an upcoming feature to be released in TypeScript 4.7: Instantiation Expressions. With Instantiation Expressions, we won't have to hope that useRef<T>()'s type is MutableRefObject<T | undefined>; we'll be able to infer that information directly from ReturnType<typeof useRef<T>>, allowing us to de-duplicate not only the T | undefined but also even the entire MutableRefObject<T | undefined>! You can check this out in action at this playground link. One added bonus of squeezing out that extra bit of type-inference is writing less code and removing a type import.
Screen Shot 2022-04-23 at 12 45 58 PM

Indeed, the most future-proof (i.e. the most correct) return type would be whatever TypeScript infers it as from ref.current (or, more specifically, the usePrevious function body implementation), but I know some linters require an explicit return type, so I thought i'd include it here.


WRT reducing imports, type imports are negligible because they're stripped away during compile time. From the type-only imports release notes:

import type only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there’s no remnant of it at runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment