React状态管理&CRA
- 状态管理
-
- 理论讲解
- [context 上下文](#context 上下文)
- 结合状态来维护todoList
- [Escape 脱围机制](#Escape 脱围机制)
- CRA
状态管理
理论讲解
如何针对 effect -> 对action的触发 -> 影响到UI
怎么针对上述链路进行更好的抽象
还有,redux,mobx,vuex,pinia
重要的就是:state
不维护好就会造成:
- 冗余代码
- 预期之外的触发action
- 代码可维护性更差
有限状态自动机:各种各样的状态管理,通过事件触发一个行为,行为是固定的,相同的输入指向相同的输出,类似于纯函数
组件之间传达状态的方式:
状态提升,parent <=> childA,childB
reducer,类似触发器,是处理状态变更的,将不同组件中的不同状态合并起来
useReducer
javascriptconst [type,dispatch]=useReducer( typeReducer, initType ) payload //荷载 rest函数创建出来的对象 { ...rest }
案例
举例:
- src
- App.js
- AddTask.js
- TaskList.js
App.js
javascript
import React, { useReducer } from 'react';
import AddTask from './AddTask';
import TaskList from './TaskList';
// 初始任务
const initialTasks = [
{ id: 0, text: '参观卡夫卡博物馆', done: true },
{ id: 1, text: '看木偶戏', done: false },
{ id: 2, text: '列依墙图片', done: false }
];
// 任务Reducer
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [
...tasks,
{ id: action.id, text: action.text, done: false }
];
case 'changed':
return tasks.map(t =>
t.id === action.task.id ? action.task : t
);
case 'deleted':
return tasks.filter(t => t.id !== action.id);
default:
throw new Error('未知操作: ' + action.type);
}
}
export default function App() {
// 状态维护的核心
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// 添加任务
function handleAddTask(text) {
const newTask = { id: Date.now(), text }; // 使用时间戳作为ID
dispatch({ type: 'added', ...newTask });
}
// 修改任务
function handleChangeTask(task) {
dispatch({ type: 'changed', task });
}
// 删除任务
function handleDeleteTask(taskId) {
dispatch({ type: 'deleted', id: taskId });
}
return (
<div>
<h1>布拉格行程</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</div>
);
}
AddTask.js
javascript
import React, { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
// 处理输入框变化
function handleChange(event) {
setText(event.target.value);
}
// 提交表单
function handleSubmit(event) {
event.preventDefault();
if (text.trim() !== '') {
onAddTask(text);
setText(''); // 清空输入框
}
}
return (
<div>
<input
type="text"
value={text}
onChange={handleChange}
placeholder="请输入任务"
/>
<button onClick={handleSubmit}>添加任务</button>
</div>
);
}
TaskList.js
javascript
import React, { useState } from 'react';
export default function TaskList({ tasks, onChangeTask, onDeleteTask }) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
const [newText, setNewText] = useState(task.text);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
type="text"
value={newText}
onChange={e => setNewText(e.target.value)}
/>
<button onClick={() => {
onChange({ ...task, text: newText });
setIsEditing(false);
}}>
保存
</button>
</>
);
} else {
taskContent = (
<>
<input
type="checkbox"
checked={task.done}
onChange={() => onChange({ ...task, done: !task.done })}
/>
<span>
{task.text}
</span>
<button onClick={() => onDelete(task.id)}>删除</button>
<button onClick={() => setIsEditing(true)}>编辑</button>
</>
);
}
return <div>{taskContent}</div>;
}
initType初始值,像是tasks列表,需要去维护的状态
,是状态集
的概念,不会区分谁是哪个状态
需要去修改这个数组的时候,才会去通过动作或者手段选取其中某条执行,这时候需要借助于action,通过action去找到某一条处理这个task
上述是比较简单的例子,如果在复杂情况的话,需要区分文件,那么文件列表可能是这样:
- src
- reducer
- dispatch.js
- action.js
- state.js
- reducer
context 上下文
在vdom中树状结构就是通过上下文去维护的
本质:递归
通过一个参数,parentId 传递给下面的child,他们之间有一个关联的脐带:contextId,找到roots之间的关联方式
在实际开发中,props层级超过一层的话,就不能再通过props来传递了,除非是一些特殊的情况
举例:
- src
- index.js
- App.js
- Section.js
- Heading.js
- LevelContext.js
- index.css
index.js
javascript
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
App.js
javascript
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
{/* 当前层级下的level,根据最近层级下的provider去获取的 */}
{/* level+1 => LevelContext.Provider value=1 */}
<Heading>大标题</Heading>
<Section>
{/* level+1 => LevelContext.Provider value=2 */}
<Heading>二级标题</Heading>
<Heading>二级标题</Heading>
<Heading>二级标题</Heading>
<Section>
{/* level+1 => LevelContext.Provider value=3 */}
<Heading>三级标题</Heading>
<Heading>三级标题</Heading>
<Heading>三级标题</Heading>
</Section>
</Section>
</Section>
);
}
Heading.js
javascript
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
switch(level) {
case 0:
throw new Error('标题必须在 section 内!');
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw new Error('未知级别:' + level);
}
}
Section.js
javascript
import { useContext } from "react";
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
LevelContext.js
javascript
import { createContext, useState } from "react";
export const LevelContext = createContext(0);
index.css
css
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.section{
padding: 10px;
margin: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
- 日常开发中,这里的reducer是和context并行去维护的
结合状态来维护todoList
- src
- index.js
- App.js
- TaskList.js
- TasksContext.js
- AddTask.js
index.js
javascript
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
App.js
javascript
import AddTask from './AddTask.js';
import Tasklist from './TaskList.js';
import { TasksProvider } from './TasksContext.js';
export default function App() {
return (
<TasksProvider>
<h1>在京都休息一天</h1>
<AddTask />
<Tasklist />
</TasksProvider>
);
}
TaskList.js
javascript
import { useState, useContext } from 'react';
import { useTasks, useTasksDispatch } from "./TasksContext";
export default function TaskList() {
const tasks = useTasks();
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task task={task} />
</li>
))}
</ul>
);
}
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useTasksDispatch();
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
dispatch({ type: 'changed', task: { ...task, text: e.target.value } });
}}
/>
<button onClick={() => setIsEditing(false)}>保存</button>
</>
);
} else {
taskContent = (
<>
{task.text}
<button onClick={() => setIsEditing(true)}>编辑</button>
</>
)
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={e => {
dispatch({ type: 'changed', task: { ...task, done: e.target.checked } });
}}
/>
{taskContent}
<button onClick={() => {
dispatch({ type: 'deleted', id: task.id });
}}>
删除
</button>
</label>
);
}
TasksContext.js
javascript
import { createContext, useContext, useReducer } from 'react';
// 创建两个 context
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
// 初始任务数据
const initialTasks = [
{ id: 0, text: '哲学家之路', done: true },
{ id: 1, text: '参观寺庙', done: false },
{ id: 2, text: '喝抹茶', done: false }
];
// 任务 reducer (用于更新任务状态)
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [
...tasks,
{
id: action.id,
text: action.text,
done: false
}
];
case 'changed':
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t
}
});
case 'deleted':
return tasks.filter(t => t.id !== action.id);
default:
throw new Error('未知操作: ' + action.type);
}
}
// TasksProvider 组件,提供任务和 dispatch
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
// 嵌套 Provider是非常常见的
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
// 自定义 Hook 用于获取任务数据
export function useTasks() {
return useContext(TasksContext);
}
// 自定义 Hook 用于获取 dispatch 函数
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
AddTask.js
javascript
import { useState } from 'react';
import { useTasksDispatch } from './TasksContext'; // 导入 dispatch
export default function AddTask({onAddTask}) {
const [text, setText] = useState(''); // 管理输入框的文本状态
const dispatch = useTasksDispatch(); // 获取 dispatch 函数
const handleAddClick = () => {
if (text.trim() === '') return; // 防止添加空任务
setText(''); // 清空输入框
dispatch({
type: 'added', // 确保与 reducer 中的 action type 一致
id: nextId++, // 使用 nextId
text: text,
});
};
return (
<>
<input
placeholder="添加任务"
value={text}
onChange={(e) => setText(e.target.value)} // 修正了 onChange 的语法错误
/>
<button onClick={handleAddClick}>添加</button>
</>
);
}
let nextId = 3; // 初始 id,从 3 开始
javascript
// 嵌套 Provider是非常常见的
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
场景:
ToB:微应用
- 全局注入:从主应用下发的,mainAppContext,包含 主应用的currentSubApp,当前子应用的信息,主应用注入到全局上的数据
- 当前应用/用户信息:user,permissions,menu菜单,针对当前登录用户的鉴权,权限处理,这里也会创建 PermissionContext状态/RBACContext 权限管理的状态,不同用户的权限看到不同的内容,使用前面说的Context维护
- 当前运行时态:主题包,i18n语法,pc/mobile runtimeContext运行时态下的
createModel:就是provider,只不过是封装了一层
状态库 hox
飞冰
飞冰状态管理
Hox 根节点不需要创建多个provider,只需要一个就够了,可以看看源码
Redux store就是state,provider就是通过useReducer创建的provider
Escape 脱围机制
ref
ref:值更新了,组件没更新。不会更新重新渲染,一般是在与视图更新无关的时候使用
尽量避免使用与交互无关的
可以通过 useRef 去创建一些新的 ref
const ref = useRef(123)
使用:ref.current
ref 的使用:
javascript
import { useRef } from 'react';
export default function App() {
let ref = useRef(0);
// 使用useState创建的话,需要使用setState来触发,会导致重新渲染re-render
function handleClick() {
//这里是js原生的,这块更新不会触发组件的更新,值更新了,组件没更新
// ref能够脱逃到当前的状态下的,也就是说,不会导致重新渲染re-render
ref.current = ref.current + 1
alert('你点击了 ' + ref.current + ' 次!');
}
return (
<button onClick={handleClick}>
点击我!
</button>
)
}
state的使用:
javascript
// state使用
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
你点击了 {count} 次!
</button>
)
}
ref 和 state的区别:
-
ref 不会重新渲染,与视图无关的时候使用,如果与视图相关的使用ref,则不会达到想要的效果
javascript// ref作与视图相关的 import { useRef } from "react"; export default function App() { const count = useRef(0); //这里也可以使用useState,使用useState创建的话,需使用setState触发 function handleClick() { count.current=count.current+1; } // 这里点击后触发,只是值更新了,组件没更新。setState会导致重新渲染re-render,但是ref不会 return ( <button onClick={handleClick}> 你点击了 {count.current} 次! </button> ) }
-
ref一般用来存储大对象
-
ref绑定到dom节点上
-
ref使用在,timeoutId定时器绑定
拓展:
在react 19中,ref 已经作为props参数去传递了
例如,操作dom的时候
javascript// ref 用作 props import { useRef } from "react"; export default function App() { const inputRef = useRef(null) function handleClick() { inputRef.current.focus() } return ( <> <input ref={inputRef} /> <button onClick={handleClick}>聚焦输入框</button> </> ) }
scrollIntoView,打开某个页面后,翻到某个位置,重新刷新后,还能够定位到上次翻到的那个位置上,实现锚点效果
这里注意:img上绑定的ref必须在外层定义,这样才能使用到,如果在map等函数内部定义的ref,是不能使用的
javascriptexport default function App() { const firstCatRef = useRef(null); const secondCatRef = useRef(null); const thirdCatRef = useRef(null); function handleScrollToFirstCat() { firstCatRef.current.scrollIntoView({ behavior: 'smooth', block: "nearest", inline: "center" }); } function handleScrollToSecondCat() { secondCatRef.current.scrollIntoView({ behavior: 'smooth', block: "nearest", inline: 'center' }); } function handleScrollToThirdCat() { thirdCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: "center" }); } return ( <> <nav> <button onClick={handleScrollToFirstCat}>Neo</button> <button onClick={handleScrollToSecondCat}>Millie</button> <button onClick={handleScrollToThirdCat}>Bella</button> </nav> <div style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}> <ul style={{ display: 'inline-flex', listStyle: 'none', padding: 0, margin: 0 }}> <li style={{ marginRight: '10px' }}> <img src="https://placecats.com/neo/300/200" alt="Neo" ref={firstCatRef} /> </li> <li style={{ marginRight: '10px' }}> <img src="https://placecats.com/millie/300/200" alt="Millie" ref={secondCatRef} /> </li> <li> <img src="https://placecats.com/bella/300/200" alt="Bella" ref={thirdCatRef} /> </li> </ul> </div> </> ); }
在react整个执行机制中会有两个阶段:
- render 内存中渲染判断我们需要展示哪些节点
- commit 缓存,缓存给dom,告诉dom这次需要更新哪些东西。
因此,需要添加ref,必须保证是在commit阶段去添加的。ref必须关联在真实的节点上的,像这种,在物理空间中还没有存在真实的节点,ref是绑定不上的。
错误示例:
javascript
export function App(){
xxxxx
const list=[1,2,3]
return list.map((item)=>{
const ref=useRef(item);
console.log(ref.current)
return <div ref={ref}>{item}</div>
})
}
避免react副作用
react 性能优化:
re-render 减少re-render 减少setState触发,escape useState
类似 useMemo,useCallback,保证不会导致depends变化,
在react中,没有必要的状态更新就不要加在状态更新中
React 19中,React Compiler 平常代码中 useMemo,memo,useCallback就不用写了,自己实现了尽可能保证当前状态和其他状态解耦,并不是所有场景都需要使用effect
forwardRef(不建议使用)
React 19中已经被废弃掉了,
官网
类似于HOC的方式
能够通过ref进行传参
如果在不同目录下,ref真正关联组件的节点是不清楚的
使用forwardRef可以,但是目录层级不能超过两层
新的项目不建议直接使用 mobx 和 redux,使用 reducer 和 useContext 这两个足够了
CRA
国内一般使用的是 CRA 和 vite 这两种
没有人用,没有人维护
怎样去做一套自己的脚手架?
- commander
- inquirer
CLI 使用命令行的交互方式,获取到 config,拿到config交由给node脚本执行
pnpx/npx create-react-app my-app
pnpm create vite vite-react
里面有 react 的各种模板,这是社区的模板,就不是官方内的模板
类似使用react里的的这个:
CRA现在用的少了,大部分使用的是 vite
vite源码