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源码

相关推荐
万少10 分钟前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL15 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl0231 分钟前
java web5(黑马)
java·开发语言·前端
Amy.Wang32 分钟前
前端如何实现电子签名
前端·javascript·html5
海天胜景34 分钟前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼35 分钟前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿37 分钟前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再39 分钟前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling55543 分钟前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架
拾光拾趣录1 小时前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css