介绍
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 是一个泛型, 是 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 元素一样,命令式的访问其暴露出的内部属性,它接受三个参数
第一个参数:传入 对象,来自于父组件定义的 ref
第二个参数:传入一个函数,该函数返回一个对象,该对象包含我们要暴露给父组件的各种属性和方法
第三个参数:传入一个依赖项数组,
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 组件树的特性,所以一般这个状态也会定义在树的根部
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 属性是状态数据
子组件
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
useReducer
是 useState
的替代方案,如过 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
同样是为了避免子组件重复的优化 API,它会将组件现在的 state
和 props
和其下一个 state
和 props
进行浅比较,如果它们的值没有变化,就不会进行更新
问题来了:那为什么子组件还是重新渲染了呢?这就涉及到这部分的标题: 传入子组件引用类型
data 是一个局部变量,父组件每次重新渲染的时候,data 都会定义一个新的 data,引用的地址也自然不同,然而 React.memo
在进行浅比较的时候,发现地址不同,自然重新渲染了
useMemo
的功能,有点类似,也是用来控制渲染,不过粒度更细了,我们可以把 data
用 useMemo
包裹起来
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>
);
}
同样把 onClick
用 useMemo
包裹起来,就可以阻止重新渲染了
const onClick = useMemo(() => {
return () => {
//
};
}, []);
上述语法是在 useMemo
的 callback 中返回了 onClick 方法
等价于
const onClick = () => {
//
};
const memorizedOnClick = useMemo(() => onClick, []);
需要什么,就要在 useMemo
的 callback 中返回什么
useCallback
useCallback
可以理解为 useMemo
的语法糖,不同地方是
useMemo
返回回调函数中return
的值useCallback
直接返回整个回调函数 所以上一个案例可以优化成
const onClick = () => {};
const memorizedOnClick = useCallback(onClick);
或者直接写成
const onClick = useCallback(() => {
//
});