本文采用的react相关技术为:
- [email protected]
- [email protected]
- 脚手架create-react-app
import react, { FC } from 'react'
type IProps = {
message: string
}
const Test: FC = (props) => {
return
{props.message}
}
export default Test
React.FC
(可以直接写为FC
)显式地定义了返回类型,其他方式是隐式推导的React.FC
对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全如果没有手动为FC
声明类型,则会报类型错误,此时我们需要手动进行类型的二次转换,比如:
// wrong
const Test: React.FC<{}> = () => 'hello'
// correct
onst Test: React.FC<{}> = () => ('hello' as unknown) as JSX.Element
useState()接受一个参数为默认值,该方法返回一个数组,第一个值为定义data的值,第二个为更新data的方法,他们总是成对出现的:
import react, { FC, useState } from 'react'
type IProps = {
message?: string
}
const Test: FC = (props) => {
// 大部分情况下,TS 会自动为你推导 state 的类型
// 这里会自动将 name 推导为 string 类型
let [name, setName] = useState("Ryuko_黑猫几绛")
return (
)
}
export default Test
修改state之后无法拿到最新的状态,要等到下一个事件循环周期执行时,状态才是最新的:
const Test: FC = props => {
const [people, setPeople] = useState<{ name: string, age: number }>({ name: "张三", age: 1 })
const handleUpdate = () => {
setPeople({ ...people, name: '王五' });
console.log(people.name); // 张三
}
return (
{
// dom 上面的数据会改变为 '王五'
}
{people.name}
)
}
export default Test;
但是在state不影响DOM的前提下,你是可以同步使用它:
const Test: FC = props => {
const [people, setPeople] = useState<{ name: string, age: number }>({ name: "张三", age: 1 })
const handleUpdate = () => {
setPeople({ ...people, name: '王五' });
people.name = "王五";
console.log(people.name); // 王五
}
return (
{
// dom 上面的数据会改变为 '王五'
}
{people.name}
)
}
export default Test;
useState要触发页面的更新,是比较新的值和旧的值是否一致(对于引用类型而言,比较是新旧对象的内存地址是否一致),如果不一致才会更新页面,所以若两次传入同一对象则不会触发组件更新。
const [temp,setTemp] = useState({a: 1, b: 2});
setTemp({a: 2}); // temp = {a: 2}
因此,如果是处理复杂的对象数据,我们可以这样做:
// 同样含义的变量可以合并成一个 state,代码可读性会提升很多
const [userInfo, setUserInfo] = useState({
firstName,
lastName,
school,
age,
address
})
setUserInfo(s=> ({
...s,
fristName,
}))
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内持续存在。
一个常见的用例便是命令式地访问子组件:
const TextInputWithFocusButton: FC = () => {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
注意:无法直接通过ref
来引用函数组件,因为函数组件没有对象
const ref = useRef(null)
// wrong!
// correct!
不过,我们可以通过父子组件通信的思想:父组件转发自己的ref给子组件,然后在父组件中通过这个ref(传递过去后会被子组件绑定)操作子组件的dom
通过查文档可知,如果想实现上述的引用思想,需要为子组件函数声明为:ForwardRef
类型:
function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
可以看到这个方法需要接收一个ForwardRefRenderFunction
的函数参数,这个函数的声明为:
interface ForwardRefRenderFunction<T, P = {}> {
(props: P, ref: ForwardedRef<T>): ReactElement | null;
displayName?: string | undefined;
// explicit rejected with `never` required due to
// https://github.com/microsoft/TypeScript/issues/36826
/**
* defaultProps are not supported on render functions
*/
defaultProps?: never | undefined;
/**
* propTypes are not supported on render functions
*/
propTypes?: never | undefined;
}
这是一个函数式接口,总的来看,我们需要为forwardRef
提供一个函数参数,这个参数需要:
因此我们可以这样声明子组件:
import react, { FC, forwardRef } from 'react'
/**
* @params props 父组件传递进来的数据
* @params ref 在父组件中定义的ref,交给子组件来绑定
*/
const Son = forwardRef((props,ref)=>{
return (
<>
>
)
})
export default Son
现在子组件目标元素绑定了父组件传递进来的ref
数据,我们可以回到父组件直接进行操作了:
import react, { FC, useRef } from 'react'
import Son from './Son'
const Father: FC = () => {
const inputRef = useRef(null)
return (
<>
{
// 给子组件绑定 ref
}
>
)
}
export default Father
从前面的例子来看,useRef似乎和ref相同,都是获取到实例dom节点
注意这一句话:ref 对象在组件的整个生命周期内持续存在并且保持不变,因此当更新 current 值时并不会 re-render ,这是与 useState 不同的地方。
根据useRef
的这个性质,我们可以用来模拟实现全局变量
下面看看这个例子,需求是点击按钮让点赞数 + 1,然后点击Alert弹框显示当前点赞数
import React, { useState } from "react";
const LikeButton: React.FC = () => {
const [like, setLike] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`)
}, 3000)
}
return (
<>
>
)
}
export default LikeButton
上面的代码看起来没有问题,但是由于在handleAlertClick中形成了闭包,所以弹出来的是第一次触发函数时的like值。也就是说,当我点赞数为5时,点击Alert,然后迅速点赞到10,最终弹出的值为5。
为什么不是界面上like的实时状态?
当我们更改状态的时候,React会重新渲染组件,每次的渲染都会拿到独立的like值,并重新定义个handleAlertClick函数,每个handleAlertClick函数体里的like值也是它自己的,所以当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。
总结:不同渲染之间无法共享state状态值
在组件前定义一个类似 global 的变量
import React from "react";
let like = 0;
const LikeButton: FC = () => {
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like}`);
}, 3000);
}
return (
<>
>
);
};
export default LikeButton;
总结:由于like变量是定义在组件外,所以不同渲染间是可以共用该变量,所以3秒后获取的like值就是最新的like值,该示例同时也说明:非state变量不会引起重新render
import React, { useRef } from "react";
const LikeButton: FC = () => {
// 定义一个实例变量
let like = useRef(0);
function handleAlertClick() {
setTimeout(() => {
alert(`you clicked on ${like.current}`);
}, 3000);
}
return (
<>
>
);
};
export default LikeButton;
总结:由于 useRef更改不会re-render,所以用useRef 作为组件实例的变量,保证多个不同渲染过程中,获取到的数据肯定是最新的。
useImperativeHandle
可以让你在使用ref
时给父组件暴露的指定的值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与forwardRef
一起使用
以这一章最开始的例子来说,还是先从子组件入手:
import react, { FC, forwardRef, useImperativeHandle } from 'react'
const Son = forwardRef((props,ref)=>{
useImperativeHandle(ref, ()=>({
talk: ()=>{
console.log("hi Ryuko!");
}
}))
return (
<>
>
)
})
export default Son
此时可以在父组件中直接调用子组件内部的方法:
import react, { FC, useRef } from 'react'
import Son from './Son'
export interface InputRefProps{
talk(): void
}
const Father: FC = () => {
const inputRef = useRef(null)
return (
<>
>
)
}
export default Father
首先看看这个hook的函数定义:
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
在useRef这一章的例子中我们知道了,funcion component
每次Render的内容都会形成一个快照并保存下来,因此当状态变更而re-render时,会形成N个Render状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。即:函数在每次渲染时是独立的。
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
useEffect
是react提供的一个专门来帮我们处理副作用的钩子
useEffect
在实际 DOM 渲染完毕后执行,它每次渲染时对应的数据也是独立的
虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect
的第二个参数告诉 React 用到了哪些外部变量:
useEffect(() => {
document.title = "Hello, " + name;
}, [name]); // Our deps
直到 name
改变时的 Rerender,useEffect
才会再次执行。
useEffect
第二个参数,回调函数会在每次第一次创建组件componentDidMount
和组件数据改变componentDidUpdate
时执行有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。
需求:监听url的变化来发送网络请求,保存返回结果
import React, { FC, useState, useEffect } from "react";
import ajax from '@utils/ajax'
type IProps = {
location:string
}
const Example: FC = (props) => {
const [data, setData] = useState({});
useEffect(() => {
getData();
}, [props.location]);
const getData = () => {
ajax.post().then(res => {
setData(res);
})
}
return {data}
}
export default Example;
当location发生变化时,useEffect中函数就会自动执行。
const timer = useRef()
//componentDidMount 和 componentDidUpdate
//可以在useEffect中来处理副作用
useEffect(() => {
console.log("useEffect执行了")
//副作用可以分为两类:
//1.无需清理的副作用: 发送网络请求,获取服务器相应数据,修改state
//2.需要清理的副作用: 订阅服务 定时器 ,只需要在useEffect中返回一个函数就可以在这个函数里面清除副作用
timer.current = setInterval(()=>{
console.log("定时器执行了")
},1000)
//componentWillUnmount
//这边返回的清除函数的执行时机:是在下一次函数组件re-render之后,useEffect之前执行
return () => {
console.log("清除函数执行了")
clearInterval(timer.current)
}
}
useEffect和useLayoutEffect的异同:
useLayoutEffect是在虚拟DOM构建完成后立即执行,useEffect是在真实DOM构建完成后立即执行
useLayoutEffect是同步执行,useEffect是异步执行
useInsertionEffect可以向页面中插入dom元素
useLayoutEffect可以在绘制屏幕前修改dom元素的样式
首先看看这个hook的函数定义:
function useMemo(factory: () => T, deps: DependencyList | undefined): T;
useMemo
的泛型显示的制定了参数的返回类型,例如:
// wrong!
// 不能将类型“number”分配给类型“string”。ts(2322)
const result = React.useMemo<string>(() => 1, [])
useMemo
类似于Vue的计算属性,如果有一些属性值是可以根据其他值推导出来的,我们就可以使用useMemo
它的参数有两个:
const length = useMemo(() => {
return list.length;
}, [list]);
const [count, setCount] = useState(0);
//double依赖于count,当count改变时,double自动改变
let double = useMemo(() => {
return count * 2
}, [count]);
回顾一下,函数组件什么时候发生重新渲染:
1. 组件重新被创建
2. 当前组件state的内存地址发生了变化,无论dom中是否使用了state
3. 父组件更新,子组件也会自动的更新
4. 组件更新时,会卸载所有function,并重新创建function (执行函数组件的所有逻辑)
注意第三点,父组件更新,子组件也会自动的更新
这意味着,如果父组件有变量a,b,并将b传递给了子组件;如果修改了a的值,会触发父组件的更新,此时子组件也会自动更新,例如:
// 父组件
import React, { useState, useMemo } from "react";
import Son from "./components/Son";
function Father() {
const [count, setCount] = useState(100);
//useMemo()可以实现类似于Vue中的计算属性的功能,还可以用来缓存数据
const obj = {
name: "zhangsan",
};
//下面的obj2在函数组件每一次重新渲染的时候都是同一个对象,没有重新初始化
const obj2 = useMemo(() => {
return {
name: "zhangsan",
};
}, []);
return (
{
// 点击按钮以后,由于函数会重新执行,父组件中将创建一个新的obj,传递给子组件的obj也会改变
// 因此会触发子组件的自动更新
}
{
// 不会触发更新
}
);
}
export default Father;
memo() 是一个高阶组件,高阶组件其实就是一个函数,只不过这个函数的参数是一个组件,函数的返回值是一个新的组件
memo()可以实现类似于类组件的React.PureComponent功能
只要父亲传递给孩子的props发生了变化就应该刷新子组件(如果父亲没有给子组件传递props或者父亲给子组件传递的props没有改变,则子组件不应该刷新
父组件如果使用了useMemo,子组件一定要配套使用memo
函数
import React, { FC, useState, memo } from "react";
type IProps = Readonly<{
obj?: {
name: string;
};
}>;
const Son: FC = () => {
const [message, setMessage] = useState("hello");
return (
Son
);
};
export default memo(Son);
在下面这个例子中,父组件将一个函数传给了子组件,子组件通过props.onChange,将数据传递回父组件,然后父组件修改text数据:
// Father.tsx
import react, { FC, useState } from 'react'
import Son from './Son'
export default const Father: FC<{}> = () => {
const [text,setText] = useState("")
const changeHandler = (event:React.ChangeEvent)=>{
setText(event.target.value)
}
return (
<>
text文本为:{text}
>
)
}
// Son.tsx
import react, { FC, forwardRef, useImperativeHandle } from 'react'
export default const Son: FC<{
onChange?: (event: React.ChangeEvent) => void;
}> = memo((props) => {
return (
<>
>
)
})
但是,每一次子组件输入内容,父组件的text文本发生变化,造成父组件被重新渲染,从而造成父组件传递给子组件的changeHandler方法(props)发生了变化,从而造成子组件的重新渲染。
此时我们可以通过useCallback
来解决这个问题:
const changeHandler = useCallback((event:React.ChangeEvent)=>{
setText(event.target.value)
},[])
实际上useCallback
是基于useMemo
实现的,useMemo
是返回callback
执行后的结果
function useCallback(callback, args) {
return useMemo(() => callback, args);
}
跨级组件通信,实现同一子树下所有节点可统一共享子树根节点的数据
useContext接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的
的 value
决定。
当组件上层最近的
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。
基本用法:
#1.src/context/index.ts
import raect, { createContext, Context } from 'react'
type ContextType = {
name: string,
age: number
}
let context: Context = createContext(null)
// 暴露出来一个 context 对象
export default context
#2 index.tsx
import Context from './context/index'
root.render(
)
#3 components/Son.tsx
import Context from '../context/index'
const Son: FC<{}> = (props) => {
// 在子组件中获取根组件暴露的数据
const contextValue = useContext(MyContext);
return (
text文本为:{contextValue.name}
)
};
export default Son
首先看看这个hook的函数定义:
function useReducer>(
reducer: R,
initialState: ReducerState,
initializer?: undefined
): [ReducerState, Dispatch>];
必要的参数有:
Reducer
类型的reducer纯函数返回值为一个元组,表示为[state, dispath分发函数]
接下来看看reducer纯函数的定义,需要初始值以及action:
type Reducer = (prevState: S, action: A) => S;
所以,useReducer
是在函数组件中实现类似 Redux 功能的一个Hook。他接收两个参数,第一个参数是一个recuder(纯函数),第二个参数是state的初始值。
他返回一个状态 state和 dispath,state是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的。
注意:
import React, { useState, useReducer } from "react";
type StateType = Readonly<{
count: number;
}>;
//创建一个纯函数
const reducer = (state: StateType, action: any) => {
switch (action.type) {
case "ADD":
return { count: state.count + 1 };
case "SUB":
return { count: state.count - 1 };
default:
return state;
}
};
function App() {
// 使用useReducer(纯函数) ,得到state和dispatch
const [state, dispatch] = useReducer(reducer, { count: 1000 });
return (
{state.count}
);
}
export default App;
在六、七章中不难发现,我们可以创建一个全局的Context
对象,在这个对象中放入全局数据,比如:
import react, { createContext } from 'react'
let context = createContext({
name: 'Ryuko_黑猫几绛',
age: 18
})
export default context
然后父组件采用
的形式将数据暴露给后代,在子组件中只需要引入这个context,然后通过useState
将对象值取出就可以使用了。
如果想要更改这样的全局变量,第一反应是通过某个函数来触发更改。useReduce
恰好提供了这样的功能。
总的来说:
首先回顾useReduce
的函数定义:
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>];
// Dispatch>其实就是一个函数,这个函数接收一个参数泛型 A
type Dispatch<A> = (value: A) => void;
他可以返回一个包裹着state
和dispatch
函数的元组,这个函数接收ReducerAction
类型的泛型。
凭印象大体写出这样的代码:
import react, { createContext } from 'react'
let context = createContext()
export default context
通过查看createContext
的函数定义发现,这个方法需要传入一个参数表示默认变量值,同时这个方法接收一个泛型。
现在想想,我们需要使用到什么样的泛型定义呢?
为了满足useReducer
的元组返回类型,首先需要一个全局state
表示仓库的管理,同时还需要一个action
表示对数据的处理操作:
import react, { createContext, Dispatch } from 'react'
export type StateType = {
userInfo: {
name: string,
avater: string
},
position: string
}
// 声明Action的泛型
export type ActionType = {
type: string,
payload: any
}
export type ContextType = [
StateType,
Dispatch
]
// 可以把这里的 context 当作一个仓库,
let context = createContext([
{position:""},()=>{}
])
export const reducer = (state: StateType, action: ActionType) => {
switch (action.type) {
case "LOACTION":
return { ...state, position: action.payload.position }
// 切记 需要返回默认值
default:
return state
}
}
export default context
现在我们到根目录的index.tsx文件中进行配置。
也许你会想说,在这里直接引入Context
全局对象,使用useReducer
计算出state和dispath,然后通过context.provider
向子组件传递公共value={[state, dispatch]}就可以了。但需要注意的是,react hook 只能执行在函数式组件中:
// ...
import MyRedux from './components/MyRedux'
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
)
所以我们创建一个叫做MyRedux的对象,将这个对象作为中转容器,然后向他的子组件传递全局中的数据
import react, { FC, useReducer } from 'react'
import MyContext, { reducer } from '../context'
import Son from './Son.tsx'
const MyRedux: FC<{}> = () => {
const [state, dispatch] = useReducer(reducer, {position:""})
return (
<>
>
)
}
export default MyRedux
现在内容分发给了子组件,在组组件中直接通过useContext注册属性以及方法即可:
import react, { FC, useContext } from 'react'
import context from '../context'
const Son: FC<{}> = () => {
const [state, dispatch] = useContext(context)
return (
<>
{JSON.stringify(state)}
>
)
}
export default Son
import React, { useState, useCallback, useEffect } from "react";
export const useWinSize = () => {
// 1. 使用useState初始化窗口大小state
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const changeSize = useCallback(() => {
// useCallback 将函数缓存起来 节流
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
}, []);
// 2. 使用useEffect在组件创建时监听resize事件,resize时重新设置state (使用useCallback节流)
useEffect(() => {
//绑定一次页面监听事件 组件销毁时解绑
window.addEventListener("resize", changeSize);
return () => {
window.removeEventListener("resize", changeSize);
};
}, []);
return size;
};