跳到主要内容

react-hooks 深度指引(typescript 版)

· 阅读需 17 分钟

介绍

React 在 16.8 版本以上可以使用,hooks 优点在于能够更好的复用性,也解决无状态组件的生命周期以及状态管理的问题,可以通过自定义 hook 的形式将组件分割的更细粒度,方便拓展和维护。

不管是 decorator(装饰器) 语法提案的不稳定,还是 class 类的 ts 支持性不如函数,不得不承认如今 React 和 React 生态已经全面拥抱函数了。

函数式组件

定义

函数式组件可以用普通函数和箭头函数定义

普通函数

function People() {
return <div>Hello World!</div>;
}

箭头函数

const People = () => {
return <div>Hello World!</div>;
};

调用

组件调用以 jsx 标签方式

<App />

传参

除了字符串以外的任意类型的参数,都必须写在大括号内,否则都将视为字符串

// 字符串
<People name="张三" />
// 数字
<People age={24} />
// 布尔
<People isAdult={true} isMan />
// 空 或者不传
<People sex={undefined} />
// 对象
<People detail={{ height: 180, weight: 150 }} />

在 tsx 中,任意没有在组件内部定义的参数,都无法传递

接收参数

import React from 'react';

// 定义参数及类型,用于参数传递和获取的提示及校验
interface PeopleProps {
name: string;
age: number;
isAdult: boolean;
sex?: 'man' | 'woman'; // 加了 "?" 表示非必填参数,反之则必填
detail: { height: number; weight: number };
}

const Country: React.FC<PeopleProps> = props => {
const { name, age, isAdult, data, detail } = props; // 参数解构

return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
<li>是否成年:{isAdult ? '是' : '否'}</li>
<li>年龄:{age}</li>
</ul>
);
};

export default Country;

<Country name="张三" age="24" isAdult detail={{ height: 180, weight: 150 }} />;
FC

FC 是一个泛型, 是 FunctionComponent 的缩写,react 函数组件类型声明,接收了传入的参数类型,并扩展了 children 参数,同时添加了组件的 propTypes, contextTypes, defaultProps, displayName 等属性

当函数没有外部参数时可以不传递泛型 FC 的参数,或者不声明 FC(不推荐)

基础 hooks api

useState

useState 方法用于定义组件内部变量,参数为默认值(可以不传),并且接收一个泛型参数作为这个值的类型,返回一个数组

  • 数组的第一个元素是变量的值,这个值不可直接修改
  • 第二个元素是一个方法,用于修改这个变量
const App: FC = () => {
const [count, setCount] = useState<number>(0);

const onClick = () => {
setCount(count + 1);
};

return <button onClick={onClick}>点击我 {count}</button>;
};
关于默认值

强烈建议给默认值,否则接下来任意的获取或操作这个变量都是非常危险的,并且总是要判断非空

const App: FC = () => {
const [count, setCount] = useState<number>(0);

const onClick = () => {
// Object is possibly 'null'
count && setCount(count + 1);
};

return <button onClick={onClick}>点击我 {count}</button>;
};

有时候就是没法设置默认值但你又十分的确认不会为空时,这里可以使用 typescript 的非空断言符 "!"

const App: FC = () => {
const [count, setCount] = useState<number>();

const onClick = () => {
setCount(count! + 1);
};

return <button onClick={onClick}>点击我 {count}</button>;
};

函数作为 useState 参数

如果 state 默认值需要复杂的一系列计算和判断,并依赖于其他变量或参数,并且,可以使用 函数作为 useState 的参数,并将函数的返回值作为 useState 的默认值

const [count, setCount] = useState(() => {
const initialCount = someExpensiveComputation(props);
return initialCount;
});

useEffect

useEffect(fn, [deps]);

useEffect 的第一个函数参数用于执行一些副作用代码,可以把它当做 mounted 生命周期,也可以是 watch 监听函数(每次 state 状态改变的时候会执行),这取决于你提供给它的第二个参数

effect 会在每轮组件渲染完成后执行

  • useEffect(fn, []) 第二个参数为空数组:第一个函数只会在组件初始化时执行一次。那他就是 mounted 生命周期

  • useEffect(fn) 不提供第二个参数:任意的渲染阶段都会执行第一个函数(props 或者 state 发生改变)

  • useEffect(fn, [deps]) 提供第二个参数为内部 state 或者 外部 props:组件初始化时会执行一次;每次当你指定的状态发生改变后才会执行;

组件初始化时使用 [] 空数组作为第二个参数

interface Book {
name: string;
author: string;
}

const bookList: Book[] = [
{ name: '钢铁是如何炼成的', author: '张三' },
{ name: '取经之路', author: '李四' }
];

// 模拟请求 api
const getBookList = () => {
return new Promise<Book[]>(resolve => setTimeout(resolve, 300 bookList));
};

const App: FC = () => {
const [dataList, setDataList] = useState<Book[]>([]);

useEffect(() => {
// 这里面只会在初始化时执行一次,可以看做 mounted 生命周期
getBookList().then(data => {
setDataList(data);
});
}, []);
};

假设又有一个需求是用户输入时对输入的书名进行模糊搜索并返回给用户,那我们就可以使用 watch 模式

// 每次 bookName 发生改变时,都会调用这个方法
useEffect(() => {
// 这里面每次拿到的 bookName 都是最新的
getBookListByName(bookName).then(data => {
setDataList(data);
});
}, [bookName]); // 这个 effect 依赖于 bookName

清除 effect

组件卸载时需要清除 effect 创建的订阅或计时器 ID 等数据。具体做法是 useEffect 函数内部需要返回一个清除函数,用于处理上述操作

useEffect(() => {
const timer = setInterval(() => {
//
}, 1000);
return () => {
// 清除定时器
clearInterval(timer);
};
});

useRef

useRef 一般用于 DOM 节点或组件实例的获取 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数

const App: FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputRef.current!.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={onButtonClick}>获得焦点</button>
</>
);
};

useRef 同样可以作为一般变量使用,current 属性可直接命令式的获取和修改。但是, ref.current 的修改不会触发组件的重新渲染

所以,如果你的变量绑定到了 ui 层,那么请使用 useState

useImperativeHandle

useImperativeHandle 可以让你的自定义组件像通过 ref 使用 DOM 元素一样,命令式的访问其暴露出的内部属性,它接受三个参数

  1. 第一个参数:传入 对象,来自于父组件定义的 ref

  2. 第二个参数:传入一个函数,该函数返回一个对象,该对象包含我们要暴露给父组件的各种属性和方法

  3. 第三个参数:传入一个依赖项数组,useImperativeHandle 会重新计算当这些依赖项中的任何一项发生更改时将返回的内容,大部分情况都不需要传递这个参数

比如在父组件中想要直接调用子组件的方法,使用 useImperativeHandle 的组件必须要通过 forwardRef 来转发 ref

interface TableProps {}

interface TableRef {
load: () => void;
}

const Table = forwardRef<TableRef, TableProps>((props, ref) => {
const [dataList, setDataList] = useState<any[]>([]);
const getData = () => {
//
};
useImperativeHandle(ref, () => ({
data: dataList, // 暴露子组件属性
load: getData // 暴露子组件方法
}));

return <table></table>;
});

const Page = () => {
const tableRef = useRef<TableRef>(null); // 定义子组件的 ref

useEffect(() => {
// 在任意地方都可以通过 ref.current 获取子组件的属性和方法了
tableRef.current?.load();
}, []);
return (
<div>
<Table ref={tableRef} />
</div>
);
};

进阶 hooks api

useContext

useContext 是全局状态管理的方案之一,用于多层级组件或跨兄弟组件传参的问题。由于 React 组件的特性,所以一般这个状态也会定义在树的根部

App.tsx
import React, { useContext, useState, useEffect } from 'react';
import Button from './Button';

type Theme = 'dark' | 'light';

interface ThemeStore {
theme: Theme;
setTheme: (theme: Theme) => void;
}

export const ThemeContext = React.createContext({} as ThemeStore);

const App = () => {
const [theme, setTheme] = useState<Theme>('light');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<h3
style={{
background: theme === 'dark' ? '#000' : '#fff',
color: theme === 'dark' ? '#fff' : '#000'
}}
>
全局主题: <span>{theme === 'dark' ? '深色' : '浅色'}</span>
</h3>
<Button />
</ThemeContext.Provider>
);
};

export default App;

这里把根组件 App 中定义的 theme 和 setTheme 处理成了全局状态

  • createContext 用于创建一个 Context 对象
  • 这个 Context 对象上会包含一个 Provider 组件,主要用于监听状态的改变并通知子组件重新渲染,value 属性是状态数据

子组件

Button.tsx
import { FC, useContext } from 'react';
import { ThemeContext } from './App';

const Button: FC = () => {
const { theme, setTheme } = useContext(ThemeContext);

const onToggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};

return <button onClick={onToggleTheme}>切换主题</button>;
};

export default Button;
  • useContext 接收一个 Context Context 的状态,可以在被 这个 Context.Provider 包裹的任意子组件内中使用

useReducer

useReduceruseState 的替代方案,如过 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等,useReducer 会比 useState 更适用

import { FC, useReducer } from 'react';

interface State {
count: number;
}

const initialState: State = { count: 0 };

interface IncrementAction {
type: 'increment';
}

interface DecrementAction {
type: 'decrement';
}

interface WithArgumentsAction {
type: 'setCount';
payload: number;
}

type Actions = IncrementAction | DecrementAction | WithArgumentsAction;

function reducer(state: State, action: Actions) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'setCount':
return { count: action.payload };
default:
throw new Error();
}
}

const Counter: FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<input onBlur={e => dispatch({ type: 'setCount', payload: Number(e.target.value) })} />
</>
);
};

export default Counter;

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 用于缓存值,它仅会在某个依赖项改变时才重新计算这个值,这种优化有助于避免在每次渲染时都进行高开销的计算

  • 第一个参数(回调函数)的返回值,会作为 useMemo 的返回值
  • 第二个参数(依赖数组)决定要以哪些状态去更新,如果不传,则只会在第一次渲染时更新

演示两个案例

复杂计算的应用

import React, { useState, useMemo } from 'react';

function allTotal(count: number) {
console.log('重新计算');
let total = 0;
for (let i = 0; i <= count; i++) {
total += i;
}
return total;
}

export default function MemoDemo() {
const [count, setCount] = useState(10);
const [show, setShow] = useState(true);

const total = allTotal(count);

return (
<div>
<h2>计算数字的和:{total}</h2>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setShow(!show)}>切换</button>
</div>
);
}

当改变 show 的值的时候,for 循环跟它本身没有任何关系,但是因为整个组件的刷新,所以 for 也会执行一次,那么怎么不让它循环呢?

const total = allTotal(count) 改成

const total = useMemo(() => {
return allTotal(count);
}, [count]);

只有当 count 改变的时候,它才会重新执行,你可以把它当做 Vue.js 中的 computed 计算属性

传入子组件引用类型

import React, { useState, memo } from 'react';

interface Data {
name: string;
}

interface ChildProps {
data: Data;
}

const Child = memo<ChildProps>(props => {
console.log('子组件渲染了');
return <div>{props.data} </div>;
});

export default function MemoDemoTwo() {
const data: Data = { name: 'Jack' };

const [show, setShow] = useState(true);

return (
<div>
<Child data={data}></Child>

<button onClick={e => setShow(!show)}>切换</button>
</div>
);
}

上述 demo 中,点击切换按钮会一直触发子组件的打印,但是子组件根本没有用到 show 属性。熟悉 React 的朋友们可能知道:父组件通过子组件的 props 传递内部状态到子组件,每次状态发生发生改变,子组件就会重新执行渲染。(无论子组件有没有真正用到这个状态)

React.memo

React.memo 同样是为了避免子组件重复的优化 API,它会将组件现在的 stateprops 和其下一个 stateprops 进行浅比较,如果它们的值没有变化,就不会进行更新

问题来了:那为什么子组件还是重新渲染了呢?这就涉及到这部分的标题: 传入子组件引用类型

data 是一个局部变量,父组件每次重新渲染的时候,data 都会定义一个新的 data,引用的地址也自然不同,然而 React.memo 在进行浅比较的时候,发现地址不同,自然重新渲染了

useMemo 的功能,有点类似,也是用来控制渲染,不过粒度更细了,我们可以把 datauseMemo 包裹起来

const data = useMemo<Data>(() => {
return { name: 'Jack' };
}, []);

因为是引用类型,所以同样的,如果子组件传入的是事件属性,同样会触发重新渲染

import React, { useState, memo, useMemo } from 'react';

interface ChildProps {
onClick: () => void;
}

const Child = memo<ChildProps>(props => {
console.log('子组件渲染了');
return <div onClick={props.onClick}>子组件</div>;
});

export default function MemoDemoTwo() {
const [show, setShow] = useState(true);

const onClick = () => {
//
};

return (
<div>
<Child onClick={onClick}></Child>

<button onClick={e => setShow(!show)}>切换</button>
</div>
);
}

同样把 onClickuseMemo 包裹起来,就可以阻止重新渲染了

const onClick = useMemo(() => {
return () => {
//
};
}, []);

上述语法是在 useMemo 的 callback 中返回了 onClick 方法

等价于

const onClick = () => {
//
};

const memorizedOnClick = useMemo(() => onClick, []);
&nbsp;

需要什么,就要在 useMemo 的 callback 中返回什么

useCallback

useCallback 可以理解为 useMemo 的语法糖,不同地方是

  • useMemo 返回回调函数中 return 的值
  • useCallback 直接返回整个回调函数 所以上一个案例可以优化成
const onClick = () => {};

const memorizedOnClick = useCallback(onClick);

或者直接写成

const onClick = useCallback(() => {
//
});