React进阶之React状态管理&CRA

React状态管理&CRA

状态管理

理论讲解

如何针对 effect -> 对action的触发 -> 影响到UI

怎么针对上述链路进行更好的抽象

还有,redux,mobx,vuex,pinia

重要的就是:state

不维护好就会造成:

  1. 冗余代码
  2. 预期之外的触发action
  3. 代码可维护性更差

有限状态自动机:各种各样的状态管理,通过事件触发一个行为,行为是固定的,相同的输入指向相同的输出,类似于纯函数

组件之间传达状态的方式:

状态提升,parent <=> childA,childB

reducer,类似触发器,是处理状态变更的,将不同组件中的不同状态合并起来

useReducer

javascript 复制代码
const [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

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;
}
  1. 日常开发中,这里的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:微应用

  1. 全局注入:从主应用下发的,mainAppContext,包含 主应用的currentSubApp,当前子应用的信息,主应用注入到全局上的数据
  2. 当前应用/用户信息:user,permissions,menu菜单,针对当前登录用户的鉴权,权限处理,这里也会创建 PermissionContext状态/RBACContext 权限管理的状态,不同用户的权限看到不同的内容,使用前面说的Context维护
  3. 当前运行时态:主题包,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的区别:

  1. 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>
      )
    }  
  2. ref一般用来存储大对象

  3. ref绑定到dom节点上

  4. 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,是不能使用的

javascript 复制代码
export 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整个执行机制中会有两个阶段:

  1. render 内存中渲染判断我们需要展示哪些节点
  2. 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 这两种

没有人用,没有人维护

怎样去做一套自己的脚手架?

  1. commander
  2. inquirer

CLI 使用命令行的交互方式,获取到 config,拿到config交由给node脚本执行

pnpx/npx create-react-app my-app
pnpm create vite vite-react


awesome vite

里面有 react 的各种模板,这是社区的模板,就不是官方内的模板

类似使用react里的的这个:

后台管理模板

CRA现在用的少了,大部分使用的是 vite
vite源码

相关推荐
一路向前的月光1 小时前
React(6)
前端·javascript·react.js
众智创新团队1 小时前
singleTaskAndroid的Activity启动模式知识点总结
前端
祁许2 小时前
【Vue】打包vue3+vite项目发布到github page的完整过程
前端·javascript·vue.js·github
我的86呢!2 小时前
uniapp开发h5部署到服务器
前端·javascript·vue.js·uni-app
小爬的老粉丝2 小时前
基于AIOHTTP、Websocket和Vue3一步步实现web部署平台,无延迟控制台输出,接近原生SSH连接
前端
程序员晚天2 小时前
算法中的冒泡排序
前端
~央千澈~2 小时前
大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
前端·测试工具·postman
LBJ辉2 小时前
3. CSS中@scope
前端·css
懒人村杂货铺3 小时前
forwardRef
前端
115432031q3 小时前
基于SpringBoot养老院平台系统功能实现十七
java·前端·后端