Last active
April 27, 2025 07:13
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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 ofT | undefinedwould be to assert thatuseRef<T>().currenthas the typeT | undefined(at least based on theusePreviousfunction body), butusePreviousshouldn't have the right to make this assertion when it is expressly using (and more specifically, not declaring)useRefin its implementation details. Therefore, to be more correct would be to infer theuseRef<T>().currenttype as directly as possible from theuseRef<T>().currentruntime code instead. Doing so ensures that, ifuseRef's implementation ever changes (thus, possibly changing the type ofuseRef<T>().current) from under our noses, we can maximize the chances that our typings are correct. In effect, usingMutableRefObject<T | undefined>['current']is more future-proof (and more importantly, more correct) thanT | undefined.Some might criticize the hardcoding of
T | undefinedinMutableRefObject<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 ofusePreviousreturnsMutableRefObject<T | undefined>['current']. That is, simply by looking at only theusePreviousfunction body and not the implementations of any ofusePrevious's dependencies, likeuseRef, we can not deterministically claim the return type to beMutableRefObject<T | undefined>['current']. However, to these criticisms, I would say thatMutableRefObject<T | undefined>['current']duplicates less type information thanT | undefinedand 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 isMutableRefObject<T | undefined>; we'll be able to infer that information directly fromReturnType<typeof useRef<T>>, allowing us to de-duplicate not only theT | undefinedbut also even the entireMutableRefObject<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, theusePreviousfunction 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: