0%

TypeScript 结合 React.useState 正确方式

useDarkMode hook :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type DarkModeState = "dark" | "light";
type SetDarkModeState = React.Dispatch<React.SetStateAction<DarkModeState>>;
function useDarkMode() {
const preferDarkQuery = "(prefers-color-scheme: dark)";
const [mode, setMode] = React.useState<DarkModeState>(() => {
const lsVal = window.localStorage.getItem("colorMode");

if (lsVal) {
return lsVal === "dark" ? "dark" : "light";
} else {
return window.matchMedia(preferDarkQuery).matches ? "dark" : "light";
}
});
React.useEffect(() => {
const mediaQuery = window.matchMedia(preferDarkQuery);
const handleChange = () => {
setMode(mediaQuery.matches ? "dark" : "light");
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);
React.useEffect(() => {
window.localStorage.setItem("colorMode", mode);
}, [mode]);
// we're doing it this way instead of as an effect so we only
// set the localStorage value if they explicitly change the default
return [mode, setMode] as const;
}

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:

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

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

This expression is not callable.
Not all constituents of type ‘string | React.Dispatch<SetStateAction>’ are callable.
Type ‘string’ has no call signatures.(2349)
You can read each addition of indentation as “because”, so let’s read that again:

这个表达式不是可被调用的. Because not all constituents of type ‘string | React.Dispatch<SetStateAction>’ are callable. 因为推断出有俩值,其中是 string 不可调用(2349)

我们肉眼可见 setMode 是个函数,为啥 ts 推断它是两种类型?

我们换种写法:

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

在这种情形下 array 有以下推断:

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

TypeScript 自己推断不出来数组的先后顺序,它只知道数组返回了个 string 和 Dispatch 类型

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:

1
2
3
function useState<S>(
initialState: S | (() => S)
): [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.

我们需要告诉 TypeScript 我们确保数组不会改变.

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

Or we could make a specific type for a variable:

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

更好的方式 是 Typescript 内建了这个能力,因为 typescript 已经知道了 array 的类型.所以我们只需要告诉 ts “返回数据是不变的” 我们指定返回值为 const

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

这样我们就不需要 ts 自己判断返回类型

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:

1
2
3
4
5
6
7
8
function useDarkMode() {
const [mode, setMode] = React.useState<"dark" | "light">(() => {
// ...
return "light";
});
// ...
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.

参考

ref1