目标:react + ts + scss + eslint + husky
yarn create @vitejs/app hello-react --template react-ts
cd hello-react
rm package-lock.json
yarn add sass husky prettier pretty-quick doctoc -D
yarn
.eslintrc.js
.huskyrc.json
.prettierrc.json
.commitlintrc.json
声明 props 的时候所有属性都设置成 readonly
不直接修改(或改变底层数据)这种方式和前一种方式的结果是一样的,这种方式有以下几点好处:
- 简化复杂的功能
- 不可变性使得复杂的特性更容易实现。在后面的章节里,我们会实现一种叫做“时间旅行”的功能。“时间旅行”可以使我们回顾井字棋的历史步骤,并且可以“跳回”之前的步骤。这个功能并不是只有游戏才会用到——撤销和恢复功能在开发中是一个很常见的需求。不直接在数据上修改可以让我们追溯并复用游戏的历史记录。
- 跟踪数据的改变
- 如果直接修改数据,那么就很难跟踪到数据的改变。跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。
- 跟踪不可变数据的变化相对来说就容易多了。如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。
- 确定在 React 中何时重新渲染
- 不可变性最主要的优势在于它可以帮助我们在 React 中创建 pure components (性能优化)。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。
如果你想写的组件只包含一个 render
方法,并且不包含 state,那么使用函数组件就会更简单。我们不需要定义一个继承于 React.Component
的类,我们可以定义一个函数,这个函数接收 props
作为参数,然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐,很多组件都可以使用函数组件来写。
// bad: component 写法
export class Square extends React.Component {
render() {
return (
<div className="square" onClick={() => this.props.onCustomClick()}>
{this.props.value}
</div>
);
}
}
// good: 函数组件写法
function Square({ value, onCustomClick }: PropsType) {
return (
<button className="square" onClick={onCustomClick}>
{value}
</button>
);
}
// 或
const Square: FC<PropsType> = ({ value, onCustomClick }) => {
return (
<div className="square" onClick={onCustomClick}>
{value}
</div>
);
};
🤔 good 中的两种写法有什么区别呢?应用场景是?
key
是 React 中一个特殊的保留属性(还有一个是 ref
,拥有更高级的特性)。
每次构建动态列表的时候,都要指定一个合适的 key。但是如果想要对列表进行重新排序、新增、删除操作时,把数组索引作为 key 是有问题的。显式地使用 key={i}
来指定 key 确实会消除警告,但是仍然和数组索引存在同样的问题,所以大多数情况下最好不要这么做。
组件的 key 值并不需要在全局都保证唯一,只需要在当前的同一级元素之前保证唯一即可。
为什么 key 是必须的?详见「diff 算法」
props 和 state 可能会异步更新(react 可能会把多个setState()
性能优化合并成一个调用),所以要依赖上一个 props 或 state 的值做更新,得让 setState() 接受一个函数,详见「state 的更新可能是异步的」
🤔 setState() 性能优化合并如何做的呢?防抖吗?
💙 见 react 的批处理机制(isPending)
props 是 readonly 的,state 仅在本组件内使用
🤔 react 中有没有双向数据流的插件呢?输入数据,输出事件。类比 angular 中的
[(data)]="data" 等同于 [data]="data" (dataChange)="data = $event"
🤔 「受控组件」和「非受控组件」有没有简化的写法呢?
受控组件用双向数据流插件
非受控组件用类似 formGroup 的方式
🤔 「状态提升」看起来也可以用 cloneDeep 传递的双向数据流解决
各个生命周期适合的使用的场景
- mount 过程
constructor
:设置初始化 state 以及绑定类方法render
:将 props 和 state 作为输入返回需要渲染的元素componentDidMount
:此时 dom 节点已生成,可发起异步请求去 API 获取数据赋值 state 触发重新渲染- update 过程
getDerivedStateFromProps(nextProps, prevProps)
:props 更新时触发,触发一些动画/页面跳转shouldComponentUpdate(nextProps, nextState)
:props、state 更新时触发,return false 就能阻止更新,用于性能优化(部分更新)getSnapshotBeforeUpdate(prevProps, prevState)
:该生命周期的任何返回值都会作为参数传递给componentDidUpdate
,在组件更改前可捕获些信息,可用于获取聊天窗口中的滚动位置render
:创建虚拟 dom,进行 diff 算法,更新 dom 树componentDidUpdate(prevProps, prevState)
:更新 dom 后立即调用,首次渲染不会执行此方法,可获取 dom 节点- Unmount 过程
- componentWillUnmount:组件被移除前的调用,可做些清理工作
💙 可以用 pureComponent 来自动判断 props 是否发生变化(省略 child 组件中对shouldComponentUpdate
的判断)
不能返回false
来阻止默认行为,得显式使用 event.preventDefault 等
最好都写成 onClick={() => this.customFn()}
避免 this 指向的混乱
-
短路表达式
{<condition> && <h2>is show this dom</h2>}
,当 condition 为 false 时不渲染此 dom -
render 返回 null 时不渲染,详见「阻止组件渲染」
成熟解决方案:formik
直接用{props.children}
,类比 angular 的<ng-content><ng-content>
和 vue 的 slot
const Square: FC<PropsType> = ({ children }) => {
return (
<div className="square">
{children}
</div>
);
};
也可以指定洞名,不用 children,详见 https://codepen.io/gaearon/pen/gwZOJp?editors=0010
任何东西都可以作为 props 进行传递
复用纯逻辑非 UI 的功能,可以抽离一个单独的 js 模块,组件可以直接 import,无须通过 extend 继承
🤔 类似 angular 中的 abstract class 或者 interface,还是需要 extends 的吧?为什么只用 import 呢?
- vite
- webpack 5
- react-fresh
- Tailwind CSS
类似 ,当没有任何属性时,可用 <></> 短语法表示,暂时 Fragment 的属性只支持 key
const OtherComponent = React.lazy(() => import('./OtherComponent'));
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
如果一个 class 组件中定义了 static getDerivedStateFromError()
或 componentDidCatch()
这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息
React.lazy 目前只支持export default 的导致,如果要重命名导出,可以创建一个中间模块重命名
export {MyComponent as default} from './ManyComponent';
// 或者
import {MyComponent} from './MangComponent';
export default MyComponent;
优点:很多不同层级的组件需要访问同样的数据
缺点:会使组件的复用性变差
适用场景:管理当前的 locale、theme、缓存数据等
介绍:
// 声明
const ThemeContext = React.createContext('light'); // light 为默认值
// 使用
<ThemeContext.Provider value="dark">
<OtherComponent />
</ThemeContext.Provider>
function OtherCompoent() {
return <ThemedButton />
}
function ThemedButton() {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
this.contextType = ThemeContext;
return <Button theme={this.context} />
}
如果只是想避免层层传递一些属性,组件组合比 context 更好
例如 user 和 avatarSize 数据仅顶层的 Page 组件知晓,需要在底层的 Avatar 组件中获取 user 信息进行渲染,中间组件无须知道 user 和 avatarSize,可以直接在 Page 中将 Avatar 直接渲染好,将 Avatar 层层传递下去,这样就将多个数据的 props 合成一个 children 的 props 进行传递,有效减少需要传递的 props 数量
优点:减少了应用中需要层层传递的 props 数量
缺点:
- 将子组件渲染逻辑提升到组件树的更高层次来处理,会使得高层组件变得复杂
- 可能会传递多个子组件,然后为多个子组件封装多个单独的 slots,可能用 props.children 去获取对应的 slot
如果children 在渲染前需要和父组件进行交流,可以用 render props
React.forwardRef()
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
HOC 在 React 的第三方库中很常见,例如 Redux 的 connect
和 Relay 的 createFragmentContainer
HOC 不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能
HOC 为组件添加特性。自身不应该大幅改变约定。HOC 返回的组件与原组件应保持类似的口,HOC 应该透传与自身无关的 props