这里写一下一些 React Hook
的一些小技巧,看这个文章需要有一点的 React Hook
使用基础
React Hook
和 TypeScript
TypeScript
的好处就不说了,这里提一下怎么在 React Hook
中使用 TypeScript
,React Hook
的大部分 Hook
都使用了范型。
显式声明变量类型
// user 被识别成 any
const [user, setUser] = useState(null);
// idx 会被识别成 number,不过还是建议显式写成 number
const [idx, setIdx] = useState(0);
const [list, setList] = useState<PaperItem[]>(initList);
const [paperIdx, setPaperIdx] = useState<number>(0);
const [screenLoading, setScreenLoading] = useState<boolean>(false);
const [pageState, setPageState] = useState<PaperPageState>(
PaperPageState.preview
);
const listRef = useRef<PaperItem[]>(list);
const paperIdRef = useRef<string>('');
const entryRef = useRef<string>('default');
// 这里如果不写,程序还真不知道你这里想写的是样式。。。
const iconStyle = useMemo<React.CSSProperties>(() => {
if (!visible) {
return { display: 'none' };
}
}, [visible]);
复制代码
Hook
组件声明
类声明组件可以 React.Component<P={}, S={}, SS=any>
来实现声明组件的属性和状态
例如:
import React from 'react';
interface TextComponentProps {
initValue: string;
}
interface TextComponentState {
length: number;
value: string;
}
class TextComponent extends React.Component<
TextComponentProps,
TextComponentState
> {
constructor(props: TextComponentProps) {
const value = props.initValue;
const length = value.length;
this.state = { value, length };
}
render() {}
}
复制代码
React Hook
使用的是函数式组件,可以依赖 React.FunctionComponent<T>
或者采用别名 React.FC<T>
,T
为组件的属性范型
例如:
interface CarouselComponentProps {
onIdxChange: (idx: number) => void;
currPage: number;
listLength: number;
}
const CarouselComponent: React.FC<CarouselComponentProps> = ({
onIdxChange,
currPage,
listLength,
}) => {};
复制代码
如果需要 Ant Design
的那种子组件声明,可以改一下
interface CarouselComponentProps {
onIdxChange: (idx: number) => void;
currPage: number;
listLength: number;
}
export interface CarouselItemProps {
src: string;
}
interface Carousel extends React.FC<CarouselComponentProps> {
Item: React.FC<CarouselItemProps>;
}
const CarouselComponent: Carousel = ({
onIdxChange,
currPage,
listLength,
}) => {};
const CarouselItem: React.FC<CarouselItemProps> = ({ src }) => {};
CarouselComponent.Item = CarouselItem;
export default CarouselComponent;
复制代码
如果属性中需要获取 HTML
属性的话,可以继承 React.HTMLAttributes<T>
例如
// 这样组件就会获得 html div 元素的全部属性
interface CarouselComponentProps extends React.HTMLAttributes<HTMLDivElement> {
onIdxChange: (idx: number) => void;
currPage: number;
listLength: number;
}
复制代码
useState
使用技巧
useState
基础用法基本都知道,这个方法返回一个元组,元组的第一项是当前的 state
,第二项是返回修改 state
的方法,函数签名如下
useState<T>(initialState: T | (() => T)): [T, React.Dispatch<React.SetStateAction<T>>]
复制代码
基本使用
虽然这个 Hook
大部分时间都和类式的 this.setState
一致,但是因为函数组件闭包机制,实际上表现还是有那么的不一致
一个很简单的题目,下面两个组件,在快速点击三次后,3 秒后,控制台输出什么?
function Counter() {
const [count, setCount] = useState(0);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
class Counter extends Component {
state = { count: 0 };
log = () => {
this.setState({
count: this.state.count + 1,
});
setTimeout(() => {
console.log(this.state.count);
}, 3000);
};
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.log}>Click me</button>
</div>
);
}
}
复制代码
-
类组件输出的是
3 3 3
,没什么问题,因为点击 3 次之后,this.state.count
变成了3
,通过this
寻址到函数的state
为{count: 3}
,所以输出为3 3 3
-
函数式组件输出为
0 1 2
,函数式组件每次执行都有单独的闭包环境,保留这这次渲染的state
和props
,三次点击,渲染拆解如下:-
页面第一次渲染,页面看到 的
count = 0
-
第一次点击,事件处理器获取的
count = 0
,count
变成 1,第二次渲染,渲染后页面的看到count = 1
-
第二次点击,事件处理器获取的
count = 1
,count
变成 2, 第三次渲染,渲染后页面看到count = 2
-
第三次点击,事件处理器获取的
count = 2
,count
变成 3, 第四次渲染,渲染后页面看到count = 3
-
小技巧
如果函数执行需要最新的 state
, 但是函数又是在初始闭包中定义的时候,这个时候是获取不到最新的 state 的,例如
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
复制代码
这里获取到的 count
永远都是 0
,但是如果修改一下的话,就是正确的
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
}, []);
复制代码
利用 useState
返回的回调函数的回调函数肯定返回最新的状态,可以通过这个获取最新状态,并且如果 setState
一个同样的值,不会引起子组件重新渲染
// 假设 fn 在某个闭包中运行,可以通过这样拿到最新的 count,并且不会引起子组件渲染
const fn = () => {
let lastestCount = null;
setCount((c) => {
lastestCount = c;
return c;
});
};
复制代码
以上是关于在闭包中如何获取最新的 state
,但是这个不太推荐使用,两个问题
- 语义化太差了,读起来难受,调用
setState
不是为了改变state
- 虽然设置一个同样的
state
不会引起子组件重新渲染,但是组件本身还是会的
遇到这种情况,需要配合 useRef
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。`
useRef
这个说法,其实比较类似于类组件的 this
,通过 useRef
,可以缓存一些最新的 state
或者获取 DOM
引用
使用很简单,这样子
const Component: React.FC = () => {
const [count, setCount] = useState<number>(0);
const countRef = useRef<number>(count);
// 这样
countRef.current = count;
// 或者这样
useEffect(() => {
countRef.current = count;
}, [count]);
};
复制代码
有两种方式可以让 countRef
绑定最新的 state
,看个人习惯
useEffect
这个写了太多反而不想写了,这个是 React Hook
里面最强大的一个 Hook
,可以模拟 componentDidMount
,componentDidUpdate
,componentWillUnmount
三个生命周期,同样也有闭包的问题。
useCallback
& useMemo
& React.memo
日常中前面两个用的可能比较少,useCallback
返回一个记忆化的函数,useMemo
返回一个记忆化的值,如果 useMemo
返回的函数返回了一个函数,那和 useCallback
是一样的
useCallback(fn, []) === useMemo(() => fn, []);
复制代码
这两个可以配合 React.memo
做一些优化,比如传给子组件的回调函数
// 父组件
const Parent = () => {
const callback = () => {
/* 这里做一些修改父组件 state 的操作 */
};
return <Child calback={callback} />;
};
// 子组件
const Chlid = ({ callback }) => {
return <div onClick={callback}>test</div>;
};
复制代码
由 React
的重新渲染机制可以知道,父组件重新渲染,子组件会跟着重新渲染,所以可以给子组件加上一层 React.memo
,但是单单这样加还不够,React.momo
是浅比较,但是父组件这里明显重新渲染的时候,callback
也改变的,所以可以给 callback
包一层 useCallback
// 父组件
const Parent = () => {
const callback = useCallback(() => {
/* 这里做一些修改父组件 state 的操作 */
}, []);
return <Child calback={callback} />;
};
// 子组件
const Chlid = React.memo(({ callback }) => {
return <div onClick={callback}>test</div>;
});
复制代码
注意,useCallback
必须配合 React.memo
,这个例子只有其中一个都没用,不仅不能防止子组件重新渲染,还多加了一个浅比较
useContext
和 useReducer
这个可以参考我之前写的一个文章 useContext 和 useReducer 实现 Redux 和 React-Redux
useImperativeHandle
和 React.forwardRef
这两个可以实现类组件 ref
的效果,并且可以指定父组件可以引用的方法和属性,需要配合 useRef
// 父组件
const Parent = () => {
const listRef = useRef(null);
const fn = () => {
if (listRef.current) {
// 只能访问子组件实例的这四个方法
listRef.current.push(item);
listRef.current.pop();
listRef.current.shift();
listRef.current.unshift(item);
}
};
return <List ref={listRef} />;
};
// 子组件
const List = React.forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
push(item) {},
pop() {},
shift() {},
unshift(item) {},
}));
});
复制代码
总结
总结的一些 React Hook
的小技巧