一边卷前端、一边卷上岸 —— 不惧中年危机
好记性不如烂笔头 —— 张薄
创建项目
使用 create-react-app 脚手架工具创建一个 TS 版本的 React 项目:
npx create-react-app 项目名 --template typescript
概念术语
钩子(hooks)
- 这是一类特殊的函数,为你的函数式组件注入特殊的功能,往往以
use开头命名。 - 代替无状态组件和高阶组件(HOC) 这两个组件复用的方案。
- 无状态组件:没有
state的函数式组件,仅依赖于props注入,但问题是没有生命周期和副作用,没有办法进行访问数据和异步更新。 - 高阶组件:直接在原先的组件外再套一层组件,但问题是加深了组件的嵌套性,容易产生类似回调地狱一样的 DOM 结构。
- 无状态组件:没有
副作用
- 纯函数(pure function):给一个函数同样的参数,它永远返回同样的值。例如在 React 组件输入相同的参数(props),渲染 UI 应该永远一样。
- 副作用(side effect):一个函数处理了与返回值无关的事情。例如函数中修改了全局变量,函数中进行了 Ajax 调用,修改了 DOM 元素,甚至打印了
console.log等等。
全局数据传递
原生 Context
父组件(index.tsx)中,三个步骤,见下面代码注释:
import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
// Step1. 创建上下文对象时必须定义一个初始值
const defaultContextValue = {
username: "测试用户"
}
// Step2. 创建上下文对象,并且这个上下文关系对象将会在 index.tsc 以外被使用,所以需要 export
export const appContext = React.createContext(defaultContextValue)
ReactDOM.render(
<React.StrictMode>
{/* Step3. 要传递数据给 App 组件及其子组件,就要用 Provider 包裹起来,并将要传递的数据注入 value */}
<appContext.Provider value={defaultContextValue}>
<App/>
</appContext.Provider>
</React.StrictMode>,
document.getElementById("root")
)
子组件的子组件(Robot.tsx),两个步骤,见下面代码注释:
import React from 'react';
// Step1. 引入上下文关系对象
import { appContext } from '../index';
interface RobotProps {
id: number;
name: string;
email: string;
}
const Robot: React.FC<RobotProps> = ({ id, name, email }) => {
return (
// Step2. 用 Consumer(消费者,对应生产者 Provider)来包裹所有的 jsx 代码
<appContext.Consumer>
{/* Consumer 组件内部用花括号+箭头函数来 return 原先的 jsx,参数 value 是上下文关系对象的取值 */}
{(value) => {
return (
<div>
<p>{name}</p>
<p>{email}</p>
{/* 使用 value. 来访问上下文关系对象中的值 */}
<p>{value.username}</p>
</div>
);
}}
</appContext.Consumer>
);
};
export defalut Robot;
useContext()
随着 hooks 的引入,使用 useContext() 可以简化上面的操作。
父组件不变,子组件的子组件(Robot.tsx)中不再需要 Consumer 组件来包裹,见下面代码注释(看上去大步骤多了一步,实际上使用的时候清爽很多了):
// Step1. 引入 useContext 这个钩子函数
import React, {useContext} from 'react';
// Step2. 引入上下文关系对象
import { appContext } from '../index';
interface RobotProps {
id: number;
name: string;
email: string;
}
const Robot: React.FC<RobotProps> = ({ id, name, email }) => {
// Step3. 直接在函数式组件内部使用 useContext 访问上下文关系对象
// 传入的参数指定了使用哪个上下文关系对象
const value = useContext(appContext)
return (
{/* return 中可以直接使用 value 了,不再需要 Consumer 来包裹 */}
<div>
<p>{name}</p>
<p>{email}</p>
{/* 直接使用 value. 来访问上下文关系对象中的值 */}
<p>{value.username}</p>
</div>
);
};
export defalut Robot;
封装全局状态管理
为了让项目结构更清晰,一般会将上下文关系对象拎出来做成一个单独的组件,而不是混写到其它组件中,例如 AppState.tsx:
import React, {useState, PropsWithChildren} from 'react';
// 定义 ContextValue 的类型
interface AppStateValue {
username: string;
shoppingCart: { items: {id: number, name: string}[] }
}
// 定义 ContextValue 的默认值
const defaultContextValue: AppStateValue = {
username: "测试用户",
shoppingCart: { items: [] }
};
// 创建上下文对象并导出给别的组件用
export const appContext = React.createContext(defaultContextValue)
// 创建 Context 组件,它就是用来包裹其它组件的 Provider
// children 表示所有子组件都被包裹,且为它们提供全局数据支持
// React.FC 的泛型是固定写法,react18 + ts 时,使用 children 就要这么写
// 继续添加的泛型 <{}> 是当前组件自定义的类型属性,当前组件没有定义就用空花括号
export const AppStateProvider: React.FC<PropsWithChildren<{}>> = (props) => {
const [state, setState] = useState(defaultContextValue);
return (
<appContext.Provider value={state}>
{props.children}
</appContext.Provider>
);
};
使用时,在父组件中(index.tsx)使用上面定义的 Provider 组件:
import React from 'react';
import ReactDom from 'react-dom';
import App from './App';
// 引入自定义的 Provider 组件
import { AppStateProvider } from './AppState';
ReactDOM.render(
<React.StrictMode>
{/* 用 Provider 包裹起来 */}
<AppStateProvider>
<App/>
</AppStateProvider>
</React.StrictMode>,
document.getElementById("root")
);
使用时,在子组件中(Robot.tsx)引入自定义的 appContext:
import React, {useContext} from 'react';
// 引入上下文关系对象
import { appContext } from '../AppState';
interface RobotProps {
id: number;
name: string;
email: string;
}
const Robot: React.FC<RobotProps> = ({ id, name, email }) => {
const value = useContext(appContext)
return (
<div>
<p>{name}</p>
<p>{email}</p>
<p>{value.username}</p>
</div>
);
};
export defalut Robot;
全局状态更新
全局状态(state)的更新需要使用到 setState 这个钩子函数,为了能够共享这个钩子,就需要创建一个新的 context 来连接这个 setState 函数。
修改 AppState.tsx 代码,有两个步骤:
import React, {useState, PropsWithChildren} from 'react';
interface AppStateValue {
username: string;
shoppingCart: { items: {id: number, name: string}[] }
}
const defaultContextValue: AppStateValue = {
username: "测试用户",
shoppingCart: { items: [] }
};
export const appContext = React.createContext(defaultContextValue)
// Step1. 创建一个新的 context,因为初始化的是函数,所以可以传入 undefined 作为初始化值
// 因为是 ts,需要传入一个联合类型(setState 函数的类型,鼠标悬浮获取后复制过来,并加上 undefined 类型)
export const appSetStateContext = React.createContext<
React.Dispatch<React.SetStateAction<AppStateValue>> | undefined>(undefined);
export const AppStateProvider: React.FC<PropsWithChildren<{}>> = (props) => {
const [state, setState] = useState(defaultContextValue);
return (
<appContext.Provider value={state}>
{/* Step2. 继续添加 Provider,传入 setState 函数 */}
<appSetStateContext.Provider value={setState}>
{props.children}
</appSetStateContext.Provider>
</appContext.Provider>
);
};
在子组件(Robot.tsx)中修改 context 中的值,有三个步骤:
import React, {useContext} from 'react';
// Step1. 引入 appSetStateContext
import { appContext, appSetStateContext } from '../AppState';
interface RobotProps {
id: number;
name: string;
email: string;
}
const Robot: React.FC<RobotProps> = ({ id, name, email }) => {
const value = useContext(appContext)
// Step2. 添加对 appSetStateContext 访问
const setState = useContext(appSetStateContext)
const addToCart = () => {
// 初始化的时候,setState 函数使用的是 undefined,需要判断一下
if (setState) {
setState(state => {
return {
...state,
shoppingCart: {
items: [...state.shoppingCart.items, {id, name}]
}
}
})
}
}
return (
<div>
<p>{name}</p>
<p>{email}</p>
<p>{value.username}</p>
{/* Step3. 使用 appSetStateContext */}
<button onClick={addToCart}>加入购物车</button>
</div>
);
};
export defalut Robot;