6

Wrapping React.useState with TypeScript

 3 years ago
source link: https://kentcdodds.com/blog/wrapping-react-use-state-with-type-script
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Wrapping React.useState with TypeScript

Software Engineer, React Training, Testing JavaScript Training

Photo by Daiga Ellaby

How to make a custom hook that wraps useState with TypeScript properly

I made a useDarkMode hook that looks like this:

1type DarkModeState = 'dark' | 'light'
2type SetDarkModeState = React.Dispatch<React.SetStateAction<DarkModeState>>
4function useDarkMode() {
5 const preferDarkQuery = '(prefers-color-scheme: dark)'
6 const [mode, setMode] = React.useState<DarkModeState>(() => {
7 const lsVal = window.localStorage.getItem('colorMode')
8 if (lsVal) {
9 return lsVal === 'dark' ? 'dark' : 'light'
10 } else {
11 return window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'
15 React.useEffect(() => {
16 const mediaQuery = window.matchMedia(preferDarkQuery)
17 const handleChange = () => {
18 setMode(mediaQuery.matches ? 'dark' : 'light')
20 mediaQuery.addEventListener('change', handleChange)
21 return () => mediaQuery.removeEventListener('change', handleChange)
22 }, [])
24 React.useEffect(() => {
25 window.localStorage.setItem('colorMode', mode)
26 }, [mode])
28 // we're doing it this way instead of as an effect so we only
29 // set the localStorage value if they explicitly change the default
30 return [mode, setMode] as const

Then it is used like this:

1function App() {
2 const [mode, setMode] = useDarkMode()
3 return (
4 <>
5 {/* ... */}
6 <Home mode={mode} setMode={setMode} />
7 {/* ... */}
8 <Page mode={mode} setMode={setMode} />
9 {/* ... */}
10 </>
14function Home({
15 mode,
16 setMode,
17}: {
18 mode: DarkModeState
19 setMode: SetDarkModeState
20}) {
21 return (
22 <>
23 {/* ... */}
24 <Navigation mode={mode} setMode={setMode} />
25 {/* ... */}
26 </>
30function Page({
31 mode,
32 setMode,
33}: {
34 mode: DarkModeState
35 setMode: SetDarkModeState
36}) {
37 return (
38 <>
39 {/* ... */}
40 <Navigation mode={mode} setMode={setMode} />
41 {/* ... */}
42 </>
46function Navigation({
47 mode,
48 setMode,
49}: {
50 mode: DarkModeState
51 setMode: SetDarkModeState
52}) {
53 return (
54 <>
55 {/* ... */}
56 <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
57 {mode === 'light' ? <RiMoonClearLine /> : <RiSunLine />}
58 </button>
59 {/* ... */}
60 </>

This works great, and powers the "dark mode" support for all the Epic React workshop apps (for example React Fundamentals).

A closer look

I want to call out a few things about the hook itself that made things work well from a TypeScript perspective. First, let's clear out all the extra stuff and just look at the important bits. We'll even clear out the TypeScript and add it iteratively:

1function useDarkMode() {
2 const [mode, setMode] = React.useState(() => {
3 // ...
4 return 'light'
7 // ...
9 return [mode, setMode]
12function App() {
13 const [mode, setMode] = useDarkMode()
14 return (
15 <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>
16 Toggle from {mode}
17 </button>

From the get-go, we've got an error when calling setMode:

1This expression is not callable.
2 Not all constituents of type 'string | React.Dispatch<SetStateAction<string>>' are callable.
3 Type 'string' has no call signatures.(2349)

You can read each addition of indentation as "because", so let's read that again:

This expression is not callable. Because not all constituents of type 'string | React.Dispatch<SetStateAction<string>>' are callable. Because type 'string' has no call signatures.(2349)

The "expression" it's referring to is the call to setMode, so it's saying that setMode isn't callable because it can be either React.Dispatch<SetStateAction<string>> (which is a callable function) or string (which is not callable).

For us reading the code we know that setMode is a callable function, so the question is: why is the setMode type both a function and a string?

Let me rewrite something and we'll see if the reason jumps out at you:

1const array = useDarkMode()
2const mode = array[0]
3const setMode = array[1]

The array in this case has the following type:

1Array<string | React.Dispatch<React.SetStateAction<string>>>

So the array that's being returned from useDarkMode is an Array with elements that are either a string or a React.Dispatch type. As far as TypeScript is concerned, it has no idea that the first element of the array is the string and the second element is the function. All it knows for sure is that the array has elements of those two types. So when we pull any values out of this array, those values have to be one of the two types.

But React's useState hook manages to ensure when we extract values out of it. Let's take a quick look at their type definition for useState:

1function useState<S>(
2 initialState: S | (() => S),
3): [S, Dispatch<SetStateAction<S>>]

Ah, so they have a return type that is an array with explicit types. So rather than an array of elements that can be one of two types, it's explicitly an array with two elements where the first is the type of state and the second is a Dispatch SetStateAction for that type of state.

So we need to tell TypeScript that we intend to ensure our array values don't ever change. There are a few ways to do this, we could set the return type for our function:

1function useDarkMode(): [string, React.Dispatch<React.SetStateAction<string>>] {
2 // ...
3 return [mode, setMode]

Or we could make a specific type for a variable:

1function useDarkMode() {
2 // ...
3 const returnValue: [string, React.Dispatch<React.SetStateAction<string>>] = [
4 mode,
5 setMode,
7 return returnValue

Or, even better, TypeScript has this capability built-in. Because TypeScript already knows the types in our array, so we can just tell TypeScript: "the type for this value is constant" so we can cast our value as a const:

1function useDarkMode() {
2 // ...
3 return [mode, setMode] as const

And that makes everything happy without having to spend a ton of time typing out our types 😉

And we can take it a step further because with our Dark Mode functionality, the string can be either dark or light so we can do better than TypeScript's inference and pass the possible values explicitly:

1function useDarkMode() {
2 const [mode, setMode] = React.useState<'dark' | 'light'>(() => {
3 // ...
4 return 'light'
7 // ...
9 return [mode, setMode] as const

This will help us when we call setMode to ensure we not only call it with a string, but the right type of string. I also created type aliases for this and the dispatch function to make the prop types easier as I pass these values around my app.

Hope that was interesting and helpful to you! Enjoy 🎉


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK