React

用于构建 Web 和原生交互界面的库

Posted by page on May 16, 2023

React

中文文档:https://react.docschina.org/ 官方文档:https://react.dev/

安装

npm install react react-dom

react:包含 React 的核心功能,包括创建组件、管理组件的生命周期和处理状态等。它是你编写 React 组件所需的基础库。

react-dom:提供了将 React 组件渲染到 DOM中的特定方法。

框架构建

Next.jsnpx create-next-app@latest

create-react-appnpx create-react-app my-app

vitenpm create vite@latest my-app -- --template react

自动导入

plugins: [
  react(),
  AutoImport({
    imports: [
      "react",
      "react-router-dom",
      { react: ["Suspense", "createContext"] }, // 额外手动导入
    ],
    eslintrc: {
      enabled: true,
      filepath: "./eslintrc-auto-import.json",
    },
    dts: "auto-imports.d.ts",
  }),
]

基础

porps

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person=
      />
    </Card>
  );
}

条件渲染

使用 JavaScript 的 if 语句、&&? : 运算符来选择性地渲染 JSX;

渲染列表

filter() 筛选需要渲染的组件和使用 map() 把数组转换成组件数组。

属性

html标签与jsx组件

标签开头为小写时,会被 jsx编译为 html标签

标签开头为大写时,会被 jsx识别为组件

className

CSS style内联样式

事件

<button onClick={handleClick}>Button</button>

更新state

setMessage(e.target.value) 等价于 setmessage(() => e.target.value)

setState((index) => index + 100)

作为一个组件的记忆,state 不同于在你的函数返回之后就会消失的普通变量。state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于你的函数之外。当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!

触发渲染(状态更新添加队列) -> 渲染中(根据set赋值后计算快照) -> 提交(DOM)

更新对象

setPosition({ x: e.clientX, y: e.clientY });
const nextArtists = [...artists.slice(0, insertAt)];
setArtists(nextArtists);
import { useImmer } from 'use-immer';
const [person, updatePerson] = useImmer({})
updatePerson(draft => {
  draft.artwork.city = e.target.value;
});
updateMyList(draft => {
  const artwork = draft.find(a => a.id === id);
  artwork.seen = nextSeen;
});

state的保留与重置

每次重新渲染,组件状态对的保留与重置规则:

组件树上组件被移除,销毁状态;

组件树上位置未变化的组件的 state 会被保留下来,组件树决定状态(可使用key以及多个组件实现重置);

组件树上位置变化的组件会使 state 重置(旧的销毁,新的产生)

react中key更新,也会触发重新渲染

Reducer

function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    ......
    return [];
  }
}
import { useReducer } from 'react';
import { useImmerReducer } from 'use-immer';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

dispatch(
  {
    type: 'deleted',
    id: taskId,
  }
);

Context

创建 context

import { createContext } from 'react';

export const LevelContext = createContext(1);

使用 context

import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
  const level = useContext(LevelContext); // 访问上层最近
  // ...
}

提供 context

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>
  );
}

reducer + context

创建 context

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

state 和 dispatch 函数 放入 context

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        ...
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

接收使用

export default function TaskList() {
  const tasks = useContext(TasksContext);
  // ...
export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useContext(TasksDispatchContext);
  // ...
  return (
    // ...
    <button onClick={() => {
      setText('');
      dispatch({
        type: 'added',
        id: nextId++,
        text: text,
      });
    }}>Add</button>
    // ...

迁移逻辑

import { createContext, useContext, useReducer } from 'react';

const TasksContext = createContext(null);

const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

ref

使用 ref使组件记住某些信息(常规变量是记不住的),且让这些信息不会触发新的渲染

存储变量值

  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('你点击了 ' + ref.current + ' 次!');
  }

引用DOM

  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

访问组件的DOM

//  这让从上面接收的 inputRef 作为第二个参数 ref 传入组件
const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
}); 

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

effect

Effects 会在渲染后运行一些代码,实现副作用;

渲染逻辑代码 位于组件的顶层,最终返回你想在屏幕上看到的 JSX。渲染的代码必须是纯粹的——就像数学公式一样,它只应该“计算”结果,而不做其他任何事情。

事件处理程序是嵌套在组件内部的函数,而不仅仅是计算函数。事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品,或者将用户导航到另一个屏幕。事件处理程序包含由特定用户操作(例如按钮点击或键入)引起的“副作用”;

Effect 允许你指定由渲染本身,而不是特定事件引起的副作用

import { useEffect } from 'react';
function MyComponent() {
  useEffect(() => {
    // 每次渲染后都会执行此处的代码
  });
  return <div />;
}
import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);
  // 渲染后
  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

指定Effect依赖

useEffect(() => {
  // 这里的代码会在每次渲染后执行
});

useEffect(() => {
  // 这里的代码只会在组件挂载后执行
}, []);

useEffect(() => {
  //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);

:Effect 读取的每一个响应式值都必须在其依赖项中声明

添加清理(cleanup)函数

重复挂载组件,可以确保在 React 中离开和返回页面时不会导致代码运行出现问题

为了解决这个问题,可以在 Effect 中返回一个 清理(cleanup) 函数。

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => {
      connection.disconnect();    // 指定卸载组件时处理
    };
  }, []);

处理开发环境中 Effect 执行两次

控制非 React 组件

useEffect(() => {
  const dialog = dialogRef.current;
  dialog.showModal();
  return () => dialog.close();
}, []);

订阅事件

useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

触发动画

useEffect(() => {
  const node = ref.current;
  node.style.opacity = 1; // 触发动画
  return () => {
    node.style.opacity = 0; // 重置为初始值
  };
}, []);

获取数据

Effect 将会获取数据,清理函数应该要么 中止该数据获取操作,要么忽略其结果:

useEffect(() => {
  let ignore = false;

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId]);

memo

组件记忆化

React 组件应该始终具有 纯粹的渲染逻辑。这意味着如果其 props、state 和 context 没有改变,则必须返回相同的输出。通过使用 memo,你告诉 React 你的组件符合此要求,因此只要其 props 没有改变,React 就不需要重新渲染。即使使用 memo,如果它自己的 state 或正在使用的 context 发生更改,组件也会重新渲染。

import { memo, useState } from 'react';

// name值未发生更改时,父组件更新时跳过Greeting更新
const Greeting = memo(function Greeting({ name }) {
  return <h3>Hello{name && ', '}{name}!</h3>;
});

注:只有当你的组件经常使用完全相同的 props 重新渲染时,并且其重新渲染逻辑是非常昂贵的,使用 memo 优化才有价值。

收缩props变化:

import { useMemo } from "react";

// 仅当id和name改变,才意味着person对象变化了
const person = useMemo(() => { id, name }, [id, name]); 

return <Profile person={person} />;

const Profile = memo(function Profile({ person }) {
  // ...
});

记忆缓存

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // 仅当 todos 或 filter 发生变化,visibleTodos才需重新计算,否则不会重新执行 getFilteredTodos()
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}]);

callback

当props传递的值总是函数,那么 memo 包裹组件跳过重新渲染是不生效的;

因为外部传递的函数每次都是新的,此时使用 useCallback 有效解决函数重新计算:

function ProductPage({ productId, referrer, theme }) {
  // 在多次渲染中缓存函数
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // 只要这些依赖没有改变,使用缓存函数

  return (
    <div className={theme}>
      {/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

自定义Hook

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

传递响应值

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}
import { useEffect, useEffectEvent } from 'react';
// ...

// 传递值包含事件处理
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  // 防止每次组件重新渲染时聊天室就会重新连接
  const onMessage = useEffectEvent(onReceiveMessage); // useEffectEvent

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      onMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // ✅ 声明所有依赖
}

React哲学

一、拆解UI为组件层级结构

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

二、使用 React 构建一个静态版本

// 产品目录行
function ProductCategoryRow({ categoryName }) {
  // .....
}

// 产品行
function ProductRow({ product }) {
  // .....
}

// 表格
function ProductTable({ products }) {
  // .....
}

// 搜索过滤栏
function SearchBar() {
  // .....
}

// 可过滤的产品表格
function FilterableProductTable({ products }) {
  // .....
}

// 产品数据
const PRODUCTS = [
  { category: "Fruits", price: "$1", stocked: true, name: "Apple" },
  // .....
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

三、找出 UI 精简且完整的 state 表示

将 state 作为应用程序需要记住改变数据的最小集合。

组织 state 最重要的一条原则是保持它 DRY。计算出你应用程序需要的绝对精简 state 表示,按需计算其它一切。

  • 随着时间推移 保持不变?如此,便不是 state。
  • 通过 props 从父组件传递?如此,便不是 state。
  • 是否可以基于已存在于组件中的 state 或者 props 进行计算?如此,它肯定不是state!

四、验证 state 应该被放置在哪里

filterTextinStockOnlyfilterProducts

// 可过滤的产品表格
function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState("");
  const [inStockOnly, setInStockOnly] = useState(false);

  const filterProducts = products.filter((product) => {
    if (product.name.indexOf(filterText) === -1) return false;
    if (inStockOnly && product.stocked === inStockOnly) return false;
    return true;
  });

  return (
    <>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
      ></SearchBar>
      <ProductTable products={filterProducts}></ProductTable>
    </>
  );
}

五、添加反向数据流

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState("");
  const [inStockOnly, setInStockOnly] = useState(false);
  // ......
  return (
    <>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
        onFilterTextChange={setFilterText}
        onInStockOnlyChange={setInStockOnly}
      ></SearchBar>
      <ProductTable products={filterProducts}></ProductTable>
    </>
  );
}
// 搜索过滤栏
function SearchBar({ filterText, inStockOnly, onFilterTextChange, onInStockOnlyChange }) {
  return (
    <form>
      <div>
        <input
          type="text"
          value={filterText}
          placeholder="Search..."
          onChange={(e) => onFilterTextChange(e.target.value)}
        />
      </div>
      <div>
        <label>
          <input
            type="checkbox"
            id="stockCheckbox"
            value={inStockOnly}
            onChange={(e) => onInStockOnlyChange(e.target.checked)}
          />
          Only show products in stock
        </label>
      </div>
    </form>
  );
}