React Around
redux
实现
// reducer.js 纯函数,计算出新的store
const initialState = {
count: 0
}
export function reducer(state = initialState, action) {
switch (action.type) {
case 'add':
return {
...state,
count: state.count + 1
}
case 'subtract':
return {
...state,
count: state.count - 1
}
default:
return initialState
}
}
type Actions = {
type: string
payload?: any
}
export const createStore = (
reducer: (state: any, action: Actions) => any,
heightener?: (any) => any
) => {
// heightener是一个高阶函数,用于增强createStore
//如果存在heightener,则执行增强后的createStore
if (heightener) {
return heightener(createStore)(reducer)
}
let currentState = {} // 公共状态
const observers = []
function getState() {
// getter
return currentState
}
function dispatch(action: Actions) {
// setter
currentState = reducer(currentState, action)
observers.forEach((fn) => fn())
}
function subscribe(fn) {
// 发布订阅
observers.push(fn)
}
dispatch({ type: '@@REDUX_INIT' }) //初始化store数据
return {
getState,
dispatch,
subscribe
}
}
const logger = (store) => (next) => (action) => {
console.log('logger1')
let result = next(action)
return result
}
const thunk = (store) => (next) => (action) => {
console.log('thunk')
const { dispatch, getState } = store
return typeof action === 'function'
? action(store.dispatch)
: next(action)
}
const logger2 = (store) => (next) => (action) => {
console.log('log2')
let result = next(action)
return result
}
// 组合函数
function compose(...fns) {
if (fns.length === 0) return (arg) => arg
if (fns.length === 1) return fns[0]
return fns.reduce(
(res, cur) =>
(...args) =>
res(cur(...args))
)
}
const applyMiddleware =
(...middlewares) =>
(createStore) =>
(reducer) => {
const store = createStore(reducer)
let { getState, dispatch } = store
const params = {
getState,
dispatch: (action) => dispatch(action)
}
const middlewareArr = middlewares.map((middleware) =>
middleware(params)
)
dispatch = compose(...middlewareArr)(dispatch)
return {
...store,
dispatch
}
}
const store = createStore(reducer, applyMiddleware(logger, thunk, logger2))
export default function () {
return (
<Provider store={store}>
<TestApp title="this is title props" />
</Provider>
)
}
// Provider.tsx
import React from 'react'
export const ReduxContext = React.createContext(null)
export function Provider(props) {
return (
<ReduxContext.Provider value={props.store}>
{props.children}
</ReduxContext.Provider>
)
}
// connect.tsx
import React, { useContext, useEffect, useRef, useState } from 'react'
import { ReduxContext } from '@src/store/redux/Provider'
export function connect(mapStateToProps, mapDispatchToProps) {
return function (Component) {
return function Connect(props) {
const store = useContext(ReduxContext)
console.log(store)
const [state, setState] = useState(store.getState())
useEffect(() => {
console.log('useEffect running')
store.subscribe(() => {
// 根据mapStateToProps把state挂到this.props上
setState(mapStateToProps(store.getState()))
})
}, [])
return (
<Component
{...props}
{...state}
//{ ...mapStateToProps(store.getState()) }
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{...mapDispatchToProps(store.dispatch)}
/>
)
}
}
}
// 容器组件示例
const addCountAction = {
type: 'add'
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
function addCountActionAsync(dispatch) {
setTimeout(() => {
dispatch({ type: 'add' })
}, 1000)
}
const mapDispatchToProps = (dispatch) => {
return {
addCount: () => {
dispatch(addCountAction)
},
addCountAsync: () => {
setTimeout(() => {
dispatch(addCountActionAsync)
}, 1000)
}
}
}
function App(props) {
return (
<div>
<p>{props.title}</p>
<p>count: {props.count}</p>
<Divider />
<button onClick={() => props.addCount()}>add</button>
<Divider />
<button onClick={() => props.addCountAsync()}>addCountAsync</button>
</div>
)
}
export default connect(mapStateToProps, mapDispatchToProps)(App)react-redux 可以将 react 组件分为,展示组件和容器组件,容器组件的数据来源可以来自redux,修改视图可以使用 dispatch 向 redux 派发 action, 使用connect高阶组件完成connect(mapStateToProps, mapDispatchToProps)(App),通过mapStateToProps函数,可以对全局状态进行过滤,而展示型组件不直接从global state获取数据,其数据来源于父组件。
本质上是利于了 React 的Context Api,可以跨组件通信,而connect就是获取context的值,通过 props 传给组件。而 react-redux 实现了发布订阅模式,在 dispatch 的时候,可以触发回调函数 的执行,所以只需要将更新 react 视图的方法添加到observers即可。
immutable.js
持久化数据结构和结构共享
Immutable Data是一种利用结构共享形成的持久化数据结构,一旦有部分被修改,那么将会返回一个全新的对象,并且原来相同的节点会直接共享。
每次修改一个 immutable 对象时都会创建一个新的不可变的对象,在新对象上操作并 不会影响到原对象的数据。
具体点来说,「immutable」 对象数据内部采用是多叉树的结构,凡是有节点被改变,那么它和与它相关的所有上级节点都更新。

- Javascript 引用类型复用,不正确的操作导致复用前的数据也改变
- 使用深拷贝,有各种问题,比如性能,循环引用的处理,
key里面getter,setter以及原型链上的内容如何处理,React 使用时导致的不必要的重复渲染 - immutable: 创建一个被 deepClone 过的数据,新的数据进行有副作用 (side effect) 的操作都不会影响到之前的数据。其实就是创建全选的父引用,复用之前的引用类型,当对应的引用类型 的数据改变是,才创建新的复制。
即 2 个特点:
- 将所有的原生数据类型(Object, Array 等)都会转化成
immutable-js的内部对象(Map,List等),并且任何操作最终都会返回一个新的immutable的值。 - 在
immutable-js的数据结构中,深层次的对象 在没有修改的情况下仍然能够保证严格相等,即深层嵌套对象的结构共享
React Css 方案
- CSS in React Server Components Understanding the future of CSS-in-JS and React
| 方案 | 一句话定位 | 优点 | 痛点 | 典型场景 |
|---|---|---|---|---|
| 普通 CSS / PostCSS | 最熟悉、零学习成本 | 原生、可配合 autoprefixer、cssnano | 全局污染、命名冲突、难以 tree-shaking | 小项目、一次性页面 |
| CSS Modules | 文件级局部作用域 | 不写框架特有语法、与构建无关 | 文件名哈希、无法动态主题、样式复用差 | 中后台、组件库 |
| Sass / Less / Stylus | 变量、mixin、嵌套一把梭 | 功能强大、生态成熟 | 全局作用域、构建时间、包体 | 已有大量遗留样式 |
| Styled-Components | CSS-in-JS 扛把子 | 完全动态、主题系统、SSR 支持好 | 运行时开销、TypeScript 类型体操、包体 | 高度品牌化、需要主题切换 |
| Emotion | Styled-Components 平替 | 体积小、支持 css 属性写法、runtime 可选 | API 稍碎、同运行时成本 | 需要更细粒度控制 |
| Linaria | 零运行时 CSS-in-JS | 构建时抽离成 .css、无运行时 | 需 Babel、webpack 配置、动态值限制 | 追求 0 运行时 + CSS-in-JS 语法 |
| Vanilla Extract | TypeScript 写样式,也是零运行时 | 100% 静态、类型安全、tree-shaking | 构建插件、动态值需 CSS 变量 | 大型 TS 代码库、设计系统 |
| Tailwind CSS | 原子类一把梭 | 开发飞快、设计 token 化、体积小 | 类名冗长、记忆曲线、HTML 膨胀 | 快速原型、设计系统已固化 |
| UnoCSS / Windi | Tailwind JIT 超集 | 按需生成、插件生态、极速 HMR | 社区新、生态碎片化 | 想要更轻更快、Vite 优先 |
| Chakra UI / MUI / Ant Design | 组件级主题 | 现成组件、主题系统、无障碍 | 样式黑盒、定制成本高 | 业务优先、少设计资源 |
- css modules
类似 Vue 的scoped,可以解决 CSS 变量名冲突
- css in js
写在 js 里,灵活,支持模板字符串,props 传参,很好的实现了作用域隔离。 但没有很难使用预处理器变量;另外代码冗余代码体积变大
1、样式写在 js 文件里,降低 js 对 css 文件的依赖。 2、样式可以使用变量,更加灵活。 3、使用方便,不需要配置 webpack、开箱即用。 4、SSR 类框架处理 CSS Modules 变量相当棘手,所以使用 styled-components 作方案
react 如何实现 keep-alive
什么是状态保存?
假设有下述场景:
移动端中,用户访问了一个列表页,上拉浏览列表页的过程中,随着滚动高度逐渐增加,数据也将采用触底分页加载的形式逐步增加,列表页浏览到某个位置,用户看到了感兴趣的项目,点击查看其详情,进入详情页,从详情页退回列表页时,需要停留在离开列表页时的浏览位置上
类似的数据或场景还有已填写但未提交的表单、管理系统中可切换和可关闭的功能标签等,这类数据随着用户交互逐渐变化或增长,这里理解为状态,在交互过程中,因为某些原因需要临时离开交互场景,则需要对状态进行保存
在 React 中,通常会使用路由去管理不同的页面,而在切换页面时,路由将会卸载掉未匹配的页面组件,所以上述列表页例子中,当用户从详情页退回列表页时,会回到列表页顶部,因为列表页组件被路由卸载后重建了,状态被丢失
解决方式
手动保存状态
手动保存状态,是比较常见的解决方式,可以配合 React 组件的 componentWillUnmount 生命周期通过 redux 之类的状态管理层对数据进行保存,通过 componentDidMount 周期进行数据恢复
在需要保存的状态较少时,这种方式可以比较快地实现所需功能,但在数据量大或者情况多变时,手动保存状态就会变成一件麻烦事。
通过路由实现自动状态保存(通常使用 react-router)
- 重写
<Route>组件,可参考react-live-route
重写可以实现想要的功能,但成本也比较高,需要注意对原始 <Route> 功能的保存,以及多个 react-router 版本的兼容
- 重写路由库,可参考react-keeper
重写路由库成本是一般开发者无法承受的,且完全替换掉路由方案是一个风险较大的事情,需要较为慎重地考虑
基于
<Route>组件现有行为做拓展,可参考react-router-cache-route
由于 React 会卸载掉处于固有组件层级内的组件,所以需要将 <KeepAlive> 中的组件,也就是其 children 属性抽取出来,渲染到一个不会被卸载的组件<Keeper>内,再使用 DOM 操作将 <Keeper> 内的真实内容移入对应 <KeepAlive>
点击查看代码
import React, {
createContext,
useState,
useEffect,
useRef,
useContext,
useMemo
} from 'react'
const Context = createContext(null)
interface KeepState {
id: {
id: string
children: React.ReactChildren
}
}
export function AliveScope(props) {
const [state, setState] = useState<KeepState | {}>({})
const ref = useMemo(() => {
return {}
}, [])
const keep = useMemo(() => {
return (id, children) =>
new Promise((resolve) => {
setState({
[id]: { id, children }
})
setTimeout(() => {
//需要等待setState渲染完拿到实例返回给子组件。
resolve(ref[id])
})
})
}, [ref])
return (
<Context.Provider value={keep}>
{props.children}
{Object.values(state).map(({ id, children }) => (
<div
key={id}
ref={(node) => {
ref[id] = node
}}
>
{children}
</div>
))}
</Context.Provider>
)
}
function KeepAlive(props) {
const keep = useContext(Context)
useEffect(() => {
const init = async ({ id, children }) => {
const realContent = await keep(id, children)
if (ref.current) {
ref.current.appendChild(realContent)
}
}
init(props)
}, [props, keep])
const ref = useRef(null)
return <div ref={ref} />
}
export default KeepAlive使用
点击查看代码
import React, { useState } from 'react'
import KeepAlive, { AliveScope } from './Keep-Alive'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
count: {count}
<button onClick={() => setCount((count) => count + 1)}>add</button>
</div>
)
}
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<div>
<button onClick={() => setShow((show) => !show)}>Toggle</button>
<p>无 KeepAlive</p>
{show && <Counter />}
<p>有 KeepAlive</p>
{show && (
<KeepAlive id="Test">
<Counter />
</KeepAlive>
)}
<hr />
{show && (
<KeepAlive id="Test2">
<Counter />
</KeepAlive>
)}
</div>
</AliveScope>
)
}
export default App