-
-
Save btoo/65e7d4303f49299c785d38f8758525e6 to your computer and use it in GitHub Desktop.
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; | |
} |
@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';
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
@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.
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.
delete
type
keyword