我在公司花两个小时分享的React基础教程

201 阅读18分钟

React基础教程

目录:

  1. react背景介绍和优势
  2. 如何看一个网站是用vue、react、jquery 开发
  3. 数据驱动视图
  4. jsx语法
  5. 常用React写法
  6. React内置组件
  7. React生命周期
  8. useState和immer
  9. React常用hooks
  10. React自定义Hook 函数方法
  11. 组件传值
  12. React 第三方hook推荐

中文文档

点这里

全球最火爆的前端框架

  • 全球范围最火的前端框架 (国内 Vue 最火爆)

  • 国内大厂很多用 React

  • React在npmjs下载量 (平均每周下载量2000w+)

对比其他框架

  • Angular - 老牌框架,国内已不用 ----- 早期 vue模仿对象 ----
  • Vue3 - 国内常用,和 React 越发趋同(Composition API 和 JSX)
  • Svelte ,Solid.js - 后起之秀,国内尚未形成规模

生态和库

  • 状态管理:useState/useReducer(useContext(用于小的全局状态管理), Redux , Zustand

  • Vite 用于客户端 React 应用 (比wepack打包速度) SWC是rust编写的全新编译器

  • 风格和格式 - eslint prettier husky(git hooks) 等

  • Utility-First-CSS- tailwindcss 95%的义务只需写HTML, 无须写css

  • UI组件库 Ant Design(国内最推荐) Material UI(最受欢迎)Mantine UI(最推荐)Chakra UI (最推荐)Radix (无样式设计系统)

  • CSS-in-JSStyled Components(最受欢迎)、Emotion(备选方案)

  • 移动应用:ReactNative

  • 服务器端渲染的 React 应用: Next.js 实现服务器端渲染(SSR)和客户端渲染(CSR)之间的无缝切换

  • 桌面应用:Electron tauri

  • 国际化: React-i18next FormatJS 如果用一些UI库仅UI内部字段支持国际化 (例如:antd)

  • 部署和托管 Netlify 或 Vercel(尤其是 Next.js)是流行的解决方案

  • 单元/集成测试:Jest/Vitest + React Testing Library(最受欢迎)

  • VR/AR react-three-fiber react-360 aframe-react

React在跨境电商前端中的重要性 90%以上主流跨境电商使用React开发---其他vue开发---jquery几乎没有

如何看一个网站是用vue、react、jquery 开发
查看方式

1.简单的方式就是直接打开控制台查看(有很大误差,有些网站大部分网站自定义id)

2.在Chrome 应用商店查找

(1)vue开发工具:vue.js devtools

(2)react开发工具:React Developer Tools

3.固定拓展角标,检测当前网站检测到 拓展角标 自动会亮

vue.js、react.js、vue.js+nuxt.js ....

案例网站和技术栈:
https://chatgpt.com/ react+next+tailwindcss
https://www.tiktok.com/  react+next 
https://www.walmart.com/  react+next
https://www.temu.com/  react
https://www.ozon.ru/  vue3+nuxt
https://us.shein.com/ 前台vue 后台react

背景介绍

  • React 是一个用于构建用户界面的 JAVASCRIPT 库。
  • React 主要用于构建 UI,很多人认为 React 是 MVC 中的 V(视图)(控制器视图),但并不是。
  • React 起源于 Facebook 的内部项目,用来架构 Instagram 的网站,并于 2013 年 5 月开源。
  • React 以其高性能和简洁的代码逻辑著称,因此吸引了越来越多的开发者关注和使用。

语法

  • Vue 定义了 template 语法,如 v-if 等,而 JSX 更多依赖于 JS 语法。

  • 所以,JS 语法熟练的基础上,React 更简单

数据驱动视图

公式 UI = f(state) (开始可能不理解)

例如,要增加一个 todo item (修改、删除,用 DOM 操作更麻烦)

<ul>
    <li>吃饭</li>
    <li>睡觉</li>
</ul>
// 用 jQuery 的代码逻辑 - DOM 操作
const $li = $('<li>new todo</li>')
$ul.append($li)

用 React

<ul>
    { list.map(todo => <li>{todo}</li>) }
</ul>
// 用 React 的代码逻辑 - JS 操作
const [list, setList]= useState(['吃饭', '睡觉'])
setList(val => val.concat('new todo'))

数据驱动视图的好处

  • 只关注业务数据,解放 DOM 操作,提高开发效率
  • 适合大型复杂的前端项目 (否则,光 DOM 操作也受不了)

开始Demo

创建 React SPA单页面 项目

SPA优点 页面内容在单个 HTML 页面中动态加载和更新,而不是每次导航时重新加载整个页面
  • 快速响应:页面不需要完全刷新,用户体验更流畅。
  • 减少服务器负担:只在首次加载时请求页面框架,之后只请求数据。
  • 更好的用户体验:能够实现动态和交互丰富的用户界面。
  • 客户端渲染:页面逻辑和渲染都在客户端完成,减少服务器压力。

react三种文件后缀名

  • js 、jsx 、 tsx

vue中也可以支持 jsx 、 tsx写法

npm init vite 包名      // 创建vue项目自己定义模版一定要选择 Customize with create-vue 

vscode插件推荐(js/ts)

  • Simple React Snippets Typescript React code snippets

创建方式很多种(参考一下几种方式)

  • vite创建React项目 VRA (个人强烈推荐) Typescript推荐SWC技术支持:SWC是一个基于Rust编写的JavaScript和TypeScript转换器,相比传统的Babel转换,单线程下其性能提高了约20倍 ,多线程下快70倍
npm create vite@latest
  • 传统创建建方式 CRA (npmjs上核心包)(官方推荐 使用最多的脚手架 使用 webpack 作为打包工具)

! npx -----> Nodejs 工具命令,查找并执行后续的包命令

npx create-react-app react-cra --template typescript (模版名)
npx create-next-app@latest --template typescript (模版名)

eject 命令存在的意义就是更改 webpack 配置
web-vitals 性能优化用
安装基础包
npm install antd --save
npm install @ant-design/icons --save
npm install classnames
npm install immer --save

jsx语法

jsxjs语法的拓展,浏览器本身不能识别,需要通过解析工具做解析之后才能在浏览器上运行,(babel , SWC)

Babel是一个广泛使用的转码器,是前端工程化中重要的部分,可以将ES6代码转为ES5代码,从而在现有环境执行。

SWC 是一个基于 Rust 的可扩展平台,适用于下一代快速开发工具。 它被 Next.js、Parcel 和 Deno 等工具以及 Vercel、字节跳动、腾讯、Shopify 等公司使用

// 编译前  Babel
input.map(item => item += 1);
// 编译后   ES5++ 以上写法会转为 ES5写法
input.map(function (item) {
  return item += 1;
});
// react中   SWC
<div>hello word!</div>
|--> babel--->
|--->浏览器控制台Sources中查看

jsx语法---综合案例

一个案例讲完jsx
import React, { useState, Fragment } from "react";
import { Button, Input } from "antd";
import "./style.css";
// import DynamicallyChange from "./view/DynamicallyChange";
// import TestClass from "./view/TestClass";
// import SuspenseComponent from "./components/SuspenseComponent";
// import SimpleLifecycleClass from "./view/SimpleLifecycleClass";
// import SimpleLifecycleFunc from "./view/SimpleLifecycleFunc";
// import TestImmerState from "./view/TestImmerState";
// import EffectFuncHooks from "./view/EffectFuncHooks";
// import IconSwitcher from "./view/IconSwitcher";
// import SquareRootCalculator from "./view/SquareRootCalculator";
// import TestUseId from "./view/TestUseId";

function App() {
  const [count, setCount] = useState(0);
  const [isFlag, setIsFlag] = useState(false);
  const userList = [
    { id: 1, name: "John" },
    { id: 2, name: "Jane" },
    { id: 3, name: "Bob" },
  ];

  // 自定义组件首字母大写
  const ChildComponent = () => {
    return <h5>首字母大写子组件</h5>;
  };

  // 内联样式 --- 对象写法
  const styles = {
    context: {
      backgroundColor: "lightblue",
      fontSize: "16px",
      color: "white",
    },
    text: {
      backgroundColor: "darkblue",
      fontSize: "16px",
      marginBottom: "10px",
      color: "white",
    },
  };

  // 四个子组件动态切换
  const Test1 = () => {
    return <h5 style={{ color: "red",fontSize:"20px"}}>Test1</h5>;
  };
  const Test2 = () => {
    return <h5 style={{ color: "orange" ,fontSize:"20px"}}>Test2</h5>;
  };
  const Test3 = () => {
    return <h5 style={{ color: "green",fontSize:"20px"}}>Test3</h5>;
  };
  const Test4 = () => {
    return <h5 style={{ color: "blue" ,fontSize:"20px"}}>Test4</h5>;
  };

  const components = {
    1: <Test1 />,
    2: <Test2 />,
    3: <Test3 />,
    4: <Test4 />
  };
  const TestComponentUp = ({ props }) => components[props] || null;

  return (
    <>
      {/* 标签必须闭合 img  br hr .... */}
      {/* <hr> */}
      <h2>APP</h2>
      {/* Fragment 标签好处是不浪费多余的标签,代码更加简洁 */}
      {/* {userList.map((val) => {
        return (
          <Fragment key={val.id}>
            <p>{val.name}</p>
          </Fragment>
        );
      })} */}
      {/* 组件首字母大写 */}
      {/* <ChildComponent /> */}

      {/* className  案例 */}
      <p className="appSpan">这是一个测试className标签</p>
      {/* style 内联样式标签上面写法 案例 --- key大写 --- */}
      <p style={{ color: "red", fontSize: "20px", backgroundColor: "yellow" }}>
        这是一个测试---内部---style标签
      </p>
      {/* style 内联样式外部写法 案例 --- key大写 ---  */}
      <p style={styles.context}>这是一个测试---外部---context标签</p>
      <div style={styles.text}>这是一个测试---外部---text标签</div>
      {/* css in js 写法--- 略 */}

      {/* label标签for改htmlFor */}
      <label htmlFor="name">Name:</label>
      <input id="name" type="text" />

      {/* 事件案例  onClick  onChange */}
      <div>
        <Button
          type="primary"
          style={{ marginTop: "20px" }}
          onClick={() => setCount(count + 1)}
        >
          count is: {count}
        </Button>
        <Input
          placeholder="请输入...."
          onChange={(e) => console.log("99++++++", e.target.value)}
        />
      </div>

      {/* 以下是  ------> JS 表达式 案例 */}

      {/* 条件渲染 三元表达式案例 */}
      {count > 3 ? (
        <p style={{ color: "green", fontSize: "20px" }}>count大于0</p>
      ) : (
        <p style={{ color: "red", fontSize: "20px" }}>count小于0</p>
      )}
      {/* 多个变量或组件条件渲染时 */}
      <TestComponentUp props={count} />

      {/* 列表点击谁谁变色 案例  动态绑定class方法 */}
      {/* <DynamicallyChange /> */}

      {/* <TestClass /> */}

      {/* 组件显示隐藏生命周期 */}
      {/* <Button type="primary" onClick={() => setIsFlag(!isFlag)}>
        {isFlag ? "隐藏" : "显示"}
      </Button>
      {isFlag && <TestClass />} */}

	  {/* 内置组件Suspense 案例 */}
	  {/* <SuspenseComponent/> */}   

 	  {/* immer 案例 */}
      {/* <TestImmerState/> */}

  	  {/* 类组件生命周期 */}
      {/* <SimpleLifecycleClass/> */}
      {/* 函数组件生命周期 */}
      {/* <SimpleLifecycleFunc/> */}

      {/* useEffect  副作用hooks案例 */}
      {/* <EffectFuncHooks/> */}

	  {/* useTransition 案例  */}
      {/* <IconSwitcher/> */}

      {/* useMemo 案例  */}
      {/* <SquareRootCalculator/> */}

      {/* useId 案例  */}
      {/* <TestUseId/> */}

    </>
  );
}

export default App;
style.css
.appSpan{
    font-size: 20px;
    font-weight: bold;
    color: #008080;
}

.appSpan:hover{
    color: red;
    text-decoration: underline;
    cursor: pointer;
}

ul{
    list-style-type: none;
    padding: 0;
    margin: 0;
}

li{
    margin: 0;
    padding: 0;
    font-size: 20px;
    font-weight: bold;
    color: #008080;
    margin-top: 10px;
}

.userClass1{
    color:rgb(3, 155, 229);
}

.active{
    color: red;
    text-decoration: underline;
    cursor: pointer;
}
标签:
1.每一段 JSX 只能有一个根节点,或者使用 `<></>` `<React.Fragment></React.Fragment>` `<Fragment></Fragment>`
使用显式 <React.Fragment> 语法声明的片段可能具有 key   而< key={item.id}></> 会报错
----------- 同于Vue <template></template>
2.自定义组件首字母大写
3.{} 可以在 JSX 中使用花括号来嵌入动态内容、变量、函数调用等。  ---- vue中插值表达式{{}}
4.标签必须闭合状态 eg: img br hr
属性 :

CSS-in-JSStyled Components(最受欢迎)

  • CSS-in-js 并不是内联 style (重要!!!),它会经过工具的编译处理,生成 CSS class 的形式
! 和 HTML 属性基本一样,但有些和 JS 关键字冲突了
 class 要改为 className
 style 要写成JS对象(不能是string),key采用驼峰写法
 4种形式写法:(1.引入css,2.标签style直接写 3.className,4.css in js模式
   
! label中 for 要改为 htmlFor
   <label htmlFor="Name">Name:</label>
   <input id="name" type="text" />
! // css 对象写法
const styles = {container: {},text: {},};
const MyComponent = () => (
  <div style={styles.container}>
    <p style={styles.text}>This is a styled paragraph.</p>
  </div>
);
事件
1.onClick ={()=>{console.log('9999')}}
2.onClick ={handleClick}
3.onChange ....
参数传递:onClick={(e)=>handleClick(e,userList)}
JS 表达式 ( 判断 循环....)
运算符: 三元 ....
  {flag && <p>hello</p>}
  {flag ? <p>你好</p> : <p>再见</p>}
多条件:多个变量或组件条件渲染时   { handletest5(item.id)
const handletest5 = (val) => {
    if(val === 1){
      return <Test />
    }else{
      return <Test2 />
      ....
    }
  }
! vue2/vue3中使用组件component-----> <component :is="currentComponent" ></component>
    
 循环:{ XXX.map((item)=> return ....)}
 动态绑定:
     1.className className={`userClass1 ${state.Type === item.id && 'active'}`}
     2.classNames库

! 注意:同级别 `key` 必须唯一,尽量不用 index,`key` 用于优化 VDOM diff 算法   diff(oldNode, newNode)....

动态绑定classclassNames npmjs 地址 (千万周下载量)

npm install classnames --save   // 安装classnames

DynamicallyChange.jsx   // 案例 
// 传统写法
className={`userClass1 ${selectedUserId === item.id ? 'active' : ''}`}
// classNames 写法
className={classNames('userClass1', { 'active': selectedUserId === item.id })}


import React, { useState } from 'react';
// import '../style.css';
import classNames from 'classnames';

const DynamicallyChange = () => {
  const [selectedUserId, setSelectedUserId] = useState(null);

  // 点击动态绑定类名
  const handleUserClick = (id) => {
    setSelectedUserId(id);
  };

  const userList = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' },
    { id: 3, name: 'Doe' },
    { id: 4, name: 'Smith' },
  ];

  return (
    <>
      <ul>
 {/* 一般动态绑定class方式 */} 
        {userList.map((item) => (
          <li
            key={item.id}
            onClick={() => handleUserClick(item.id)}
            className={`userClass1 ${selectedUserId === item.id ? 'active' : ''}`} 
          >
            <span>{item.id}</span>: <span>{item.name}</span>
          </li>
        ))}
      </ul>
    </>
  );
};

// className方式
// className={classNames('userClass1', { 'active': selectedUserId === item.id })} 
export default DynamicallyChange;

常用React写法

// 类组件(Class Components) 
TestClass.jsx

import React, { Component } from 'react'
export default class ParentComponent extends Component {
  render() {
    return (
      <div>ParentComponent</div>
    )
  }
}

// 函数组件(Function Components) 
rsf    TestFunc.jsx
import React from 'react';
function ParentComponent(props) {
  return (
    <div>
      
    </div>
  );
}
export default ParentComponent;
// 无状态组件(Stateless Components)
const MyComponent = (props) => {
  return (<div>{props.message}</div>);
};
// Hooks 的函数组件  模拟请求  (常用的)
rfuc   TestHooks.jsx
import React, { useState, useEffect } from 'react';

const ExampleComponent = () => {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);

  useEffect(() => {
    // 挂载(componentDidMount)时执行的逻辑
    console.log('组件挂载----', count);

    // 模拟数据获取
    fetchData();

    // 卸载(componentWillUnmount)时执行的逻辑
    return () => {
      console.log('组件卸载执行----', count);
    };
  }, []); // 空数组作为依赖项,确保只在挂载和卸载时执行

  // useEffect Hook 用于在 count 变化时执行副作用
  useEffect(() => {
    console.log('组件更新:', count);

    // 可以在这里处理 count 变化的副作用,如更新文档标题
    document.title = `Count: count`;
  }, [count]); // 仅在 count 变化时执行

  // 模拟数据获取函数
  const fetchData = async () => {
    // 假设从 API 获取数据
    const response = await fetch('http://localhost:4000/api/users?pageNum=1&pageSize=5');
    const result = await response.json();
    setData(result);
  };

  // 处理按钮点击事件的函数
  const handleClick = () => {
    setCount(prevCount => prevCount + 1);
  };

  console.log('执行render函数-----', count);

  return (
    <div>
      <h1>ExampleComponent</h1>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <div>
        {data ? (
          <pre>{JSON.stringify(data, null, 2)}</pre>
        ) : (
          <p>Loading data...</p>
        )}
      </div>
    </div>
  );
};

export default ExampleComponent;

React内置组件

Fragment 上面讲过--- 略....

Profiler 渲染时间记录函数(编程式测量渲染性能 )

 const onRenderCallback = (
    id, // Profiler树的id,指定的id属性的值
    phase, // 当前渲染阶段:'mount'(挂载) 或 'update'(更新)
    actualDuration, // 本次更新的实际渲染时间
    baseDuration, // 最理想情况下的渲染时间
    startTime, // 本次渲染开始的时间
    commitTime, // 本次渲染结束的时间
    interactions // 属于本次更新的交互
  ) => {
    console.log(`id: ${id}`);
    console.log(`phase: ${phase}`);
    console.log(`actualDuration: ${actualDuration}`);
    console.log(`baseDuration: ${baseDuration}`);
    console.log(`startTime: ${startTime}`);
    console.log(`commitTime: ${commitTime}`);
    console.log(`interactions:`, interactions);

    const renderDuration = commitTime - startTime;
    console.log("渲染这个组件所用的时间为", renderDuration, "毫秒");
  };
 <Profiler id="SimpleLifecycle" onRender={onRenderCallback}><组件/></Profiler> // 包裹组件调优

StrictMode 启用严格模式 !在开发环境会执行两次render函数

Suspense 允许在子组件完成加载前展示后备方案 (通常是----Loading组件、图片预加载组件、骨架屏组件)

// 案例片段 
import React, { useState, Suspense } from 'react';
import { Button } from 'antd';

// Loading 组件
const Loading = () => <h2>Loading...</h2>;

// 自定义 fetch 包装器
let data;
let promise;

// 异步加载数据
const fetchData = () => {
  if (!promise) {
    promise = fetch('http://localhost:4000/api/users?pageNum=2&pageSize=2')
      .then((res) => res.json())
      .then((result) => {
        data = result;
        return data;
      });
  }
  if (data) {
    return data;
  }
  throw promise;
};

// 数据展示 组件
const DataDisplay = () => {
  const data = fetchData();
  return <pre>${JSON.stringify(data, null, 2)}</pre>;
};

// Suspense 案例组件
const SuspenseComponent = () => {
  const [showData, setShowData] = useState(false);

  // 点击按钮发出请求
  const getHandleUserData = () => setShowData(true);

  return (
    <>
      <Button type='primary' onClick={getHandleUserData}>发出请求</Button>
      {showData && (
        <Suspense fallback={<Loading />}>
          <DataDisplay />
        </Suspense>
      )}
    </>
  );
};

export default SuspenseComponent;

React生命周期

类组件:

import React, { Component } from 'react';

class SimpleLifecycleClass extends Component {
  // 类组件的一个特殊方法,用于在组件实例化时初始化状态和绑定事件处理程序
  constructor(props) {
    super(props); // 调用父类的构造函数component 便于访问this.props,以便正确初始化组件实例
    this.state = { count: 0 };
    console.log('01构造函数---constructor');
  }

  // 组件将要挂载时候触发的生命周期函数
  componentWillMount() {
    console.log('02组件将要挂载---componentWillMount');
  }

  // 组件挂载完成时候触发的生命周期函数
  componentDidMount() {
    console.log('04组件将要挂载---componentDidMount');
  }

  // 是否要更新数据,如果返回 true 才会更新数据 (控制组件是否应该更新。通过对比当前的 props 和 state 与新的 props 和 state 来决定是否重新渲染组件)
  shouldComponentUpdate(nextProps, nextState) {
    console.log('是否要更新数据');
    console.log(nextProps); // 父组件传给子组件的值,这里没有会显示空
    console.log(nextState); // 数据更新后的值
    return true; // 返回 true,确认更新
  }

  // 静态方法--getDerivedStateFromProps---在组件实例化时和接收到新属性时调用。它用于更新 state 以响应 prop 的变化
  static getDerivedStateFromProps(nextProps, prevState) {
    console.log('prop changed------getDerivedStateFromProps', nextProps, prevState);
    // 根据 props 更新 state
    // 返回 null 表示不更新 state
    return null;
  }

  // 你在父组件里面改变 props 传值的时候触发的函数
  componentWillReceiveProps() {
    console.log('父子组件传值,父组件里面改变了props的值触发的方法');
  }

  // 将要更新数据的时候触发的
  componentWillUpdate() {
    console.log('update-------------组件将要更新');
  }

  // 更新数据时候触发的生命周期函数
  componentDidUpdate() {
    console.log('update-------------组件更新完成');
  }

  // 组件将要销毁的时候触发的生命周期函数,用在组件销毁的时候执行操作
  componentWillUnmount() {
    console.log('componentWillUnmount');
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    console.log('render函数---render');
    return (
      <>
        <h1>SimpleLifecycleClass Demo</h1>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Increment</button>
      </>
    );
  }
}

export default SimpleLifecycleClass;

函数组件:

// 父组件控制组件挂载/卸载
 const [showCounter, setShowCounter] = useState(true);
  const toggleCounter = () => {
    setShowCounter(prevShowCounter => !prevShowCounter);
  };
<Button type="primary" onClick={toggleCounter}>isShow</Button>
{showCounter&&
<SimpleLifecycle/>}

import React, { useState, useEffect } from 'react';

const SimpleLifecycleFunc = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 挂载(componentDidMount):通过传递一个空数组作为 useEffect 的依赖数组
    console.log('componentDidMount');
    // 更新页面标题
   document.title = `Count: count`;

    // 设置一个定时器
    const timer = setInterval(() => console.log('tick'), 1000);

    // 卸载(componentWillUnmount):通过返回一个清理函数来模拟
    return () => {
      console.log('componentWillUnmount');
      // 清理定时器
      clearInterval(timer);
    };
  }, [count]); // 依赖项数组包含 count

  useEffect(() => {
    // 更新(componentDidUpdate):每次组件更新后执行 useEffect。第一次渲染也会执行
 document.title = `Count: count`;   console.log('componentDidUpdate');
  }); // 没有依赖数组,每次渲染都会执行(每次执行render都会执行) 空数组作为依赖项,只有在组件挂载和卸载时执行

  const handleClick = () => {
    setCount(count + 1);
  };

  console.log('render----函数式组件渲染');
  return (
    <div>
      <h1>Simple Lifecycle Demo</h1>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

export default SimpleLifecycleFunc;

react render函数在开发环境渲染两次问题描述

在开发模式下,即使没有使用 React.StrictMode,React 仍然可能在某些情况下多次调用组件的渲染和生命周期钩子函数。这种行为在 React 18 中尤其明显,因为 React 18 在开发模式下会引入额外的检查和重渲染,以帮助开发者发现潜在的问题。

具体到你的代码,componentDidUpdate 打印两次的原因主要有两个方面:

  • 双重调用:在开发模式下,React 18 会为了帮助开发者发现副作用的问题,故意调用某些生命周期钩子两次。这个行为在生产模式下是不会发生的。
  • 依赖数组问题:你的第一个 useEffect 钩子依赖于 count,因此每次 count 变化时,该 useEffect 钩子都会重新执行并触发 componentDidMountcomponentWillUnmount 的模拟。同时,因为没有依赖数组的第二个 useEffect 钩子会在每次渲染后执行,所以它也会触发 componentDidUpdate

为了确认是否是开发模式导致的,你可以构建一个生产版本并查看结果是否相同。

// 创建生产构建:
npm run build
// 启动生产服务器:``
npm install -g serve
serve -s build

React内置Hook 函数

(常用的) 针对函数组件 ,类组件没有

列举:

  • useState 、useEffect 、useTransition 、useRef 、useMemo、useId、useContext、useLayoutEffect、useReducer

React 从 16开始,全面推广函数式组件和 Hooks ,随后 Vue3 也开始参考这种形式。

文档地址

useState
useState 管理状态的hook(两种方式表达,统一管理/单一管理) ------ vue ----- data

useState和普通JS变量不同的是,状态变量一旦发生变化, 组件的视图UI也会跟着变化(数据驱动视图/数据驱动UI更新)

  • 单一管理 ------ vue3--- Components
1.const [isShow, setIsShow] = useState(false);    setIsShow(!isShow);
  • 统一管理 (多个状态) ------ vue--- Options
  const [state, setState] = useState({
    isShow: false,
    flag: true,
    count:1,
  });
 setState((prevState) => ({ // 修改状态
      ...prevState, // 保持其他状态不变
      isShow: !prevState.isShow,
    }));
render中: {{state.count}}
!不能直接修改,eg: state.count ++ ,数据变,视图不会更新
模拟vue表单双向绑定:(表单受控绑定) --- vue --- v-model --- 效果
import React, { useState, useMemo } from 'react';
import { Input } from 'antd';

const TestVModel = () => {
  const [value, setValue] = useState('');
  return (
    <div style={{ padding: '10px' }}>
      <Input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="输入内容...."
        style={{ width: '250px', marginBottom: '10px' }}
      />
      <p>内容: {value}</p>
    </div>
  );
};
export default TestVModel;
Immer 简化了不可变数据结构的处理 immer文档地址
npm install immer --save   // 安装依赖immer
// useState和immer修改state对比案例
TestImmerState.jsx

import React, { useState } from 'react';
import { Button } from 'antd';
import { produce } from 'immer';

const TestImmerState = () => {
  const [state, setState] = useState({
    isShow: false,
    flag: true,
    count: 1,
    useList: [
      {
        id: 1,
        name: '张三',
      },
      {
        id: 2,
        name: '李四',
      },
      {
        id: 3,
        name: '王五',
      },
    ],
  });

  // 修改简单的还好 一个...扩展运算符
  const handleClick = () => {
    setState((prevState) => ({
  // 扩展 prevState 对象中的所有属性
      ...prevState,
  // 更新 count 属性
      count: prevState.count + 1,
    }));
  };

  // 使用 immer 点击修改状态
  // 修改复杂的 --- 把 name: "XXX",改成 name: "Tom"
  const handleClickNamed = (id) => {
    setState(
      // 使用 produce 方法创建一个新的状态, 该函数会接收到一个 draft 对象
      // draft 对象是当前状态的一个代理, 你可以像修改可变对象那样修改它
      produce((draft) => {
        const user = draft.useList.find((val) => val.id === id);
        if (user) {
          user.name = 'Tom';
        }
      })
    );
  };

  return (
    <>
      <Button onClick={handleClick}>Increment</Button>
      <p>{state.count}</p>
      <>
        {state.useList.map((val) => {
          return (
            <div key={val.id}>
              <p onClick={() => handleClickNamed(val.id)}>{val.name}</p>
            </div>
          );
        })}
      </>
    </>
  );
};

export default TestImmerState;
useEffect 副作用hooks (类似 vue3中-----------------watchEffect)
// 案例
EffectFuncHooks.jsx

import React, { useState, useEffect } from 'react';

const SimpleLifecycleFunc = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 挂载(componentDidMount):通过传递一个空数组作为 useEffect 的依赖数组
    console.log('componentDidMount');
    // 更新页面标题
   document.title = `Count: count`;

    // 设置一个定时器
    const timer = setInterval(() => console.log('tick'), 1000);

    // 卸载(componentWillUnmount):通过返回一个清理函数来模拟
    return () => {
      console.log('componentWillUnmount');
      // 清理定时器
      clearInterval(timer);
    };
  }, [count]); // 依赖项数组包含 count

  useEffect(() => {
    // 更新(componentDidUpdate):每次组件更新后执行 useEffect。第一次渲染也会执行
 document.title = `Count: count`;   console.log('componentDidUpdate');
  }); // 没有依赖数组,每次渲染都会执行(每次执行render都会执行) 空数组作为依赖项,只有在组件挂载和卸载时执行

  const handleClick = () => {
    setCount(count + 1);
  };

  console.log('render----函数式组件渲染');
  return (
    <div>
      <h1>Simple Lifecycle Demo</h1>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

export default SimpleLifecycleFunc;
useLayoutEffect为有DOM操作的副作用hooks
useTransition (用于管理过渡transition状态,以便在状态更新时优化用户体验)
// IconSwitcher.jsx

import React, { useState, useTransition } from 'react';
import { Button, Space } from 'antd';
import { SmileOutlined, FrownOutlined } from '@ant-design/icons';

const IconSwitcher = () => {
  const [isHappy, setIsHappy] = useState(true);
 // 2000 代表过渡的最大持续时间为 2000 毫秒(2 秒)。如果过渡时间超过这个值,React 会优先处理高优先级的更新操作,并可能取消较低优先级的操作。
  const [isPending, startTransition] = useTransition(2000);

  const toggleIcon = () => {
    // 将状态更新标记为过渡更新
    startTransition(() => {
      setIsHappy((prevIsHappy) => !prevIsHappy);
    });
  };
  return (
    <div style={{ textAlign: 'center', marginTop: 50 }}>
      <Space direction="vertical" size="large">
        {/* isPending 表示是否有待处理的过渡更新 */}
        <Button type="primary" onClick={toggleIcon} loading={isPending}> 
          切换图标
        </Button>
        <div style={{ fontSize: 48 }}>
          {isHappy ? <SmileOutlined /> : <FrownOutlined />}
        </div>
      </Space>
    </div>
  );
};
export default IconSwitcher;
useRef (可以用来直接访问 DOM 元素。用来保持不引起重新渲染的可变值) -----vue2 refs.XXX ------ vue3中 ref()
import React, { useRef } from 'react';
const FocusInput = () => {
  // 创建一个 ref 对象
  const inputRef = useRef(null);
  const focusInput = () => {
    // 使用 ref 对象访问 DOM 元素并使其获得焦点
    inputRef.current.focus();
  };
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="点击按钮获取焦点" />
      <button onClick={focusInput}>Focus the input</button>
      {/* 当点击按钮时,input 元素将获得焦点 */}
    </div>
  );
};
export default FocusInput;
 
// vue3用法  eg:轮播图手动执行下一张操作
 <el-carousel indicator-position="none" :height="carouselHeight" ref="slideCarousel" :autoplay="autoplayTa"/>
import { ref } from "vue";
const slideCarousel = ref();
slideCarousel.value.next();
useMemo 用于处理
// SquareRootCalculator.jsx

import React, { useState, useMemo } from 'react';
import { Input } from 'antd';

const SquareRootCalculator = () => {
  const [number, setNumber] = useState(0);
  // 使用 useMemo 缓存平方根的计算结果
  const squareRoot = useMemo(() => {
    console.log('计算平方根....');
    return Math.sqrt(number);
  }, [number]);

  return (
    <div style={{ padding: '20px' }}>
      <Input
        type="number"
        value={number}
        onChange={(e) => setNumber(e.target.value)}
        placeholder="输入数字...."
        style={{ width: '200px', marginBottom: '20px' }}
      />
      <p>平方根: {squareRoot}</p>
    </div>
  );
};
export default SquareRootCalculator;
useId 生成唯一ID
  1. 保证 ID 的唯一性: 适用于服务端渲染(SSR)
  2. 更简洁的代码: 使用 useId 可以简化代码,不需要手动管理和生成 ID
import React, { useId } from 'react';

function TestUseId() {
  const inputId = useId(); // 生成唯一 ID

  return (
    <form onSubmit={(e) => e.preventDefault()}>
      <label htmlFor={inputId}>姓名:</label>
      <input id={inputId} type="text" name="name" />
    </form>
  );
}
export default TestUseId;
useReducer 通常用于封装逻辑复杂的自定义hooks -------- 略

React自定义Hook 函数

----- (举个栗子)

/hooks/useReCode.jsx  读取url任何键值hooks
import { useEffect, useState } from 'react';

const useReCode = (paramKey) => {
  const [recode, setRecode] = useState(null);
  useEffect(() => {
    const url = window.location.href;
    const searchParams = new URLSearchParams(new URL(url).search);
    const recodeValue = searchParams.get(paramKey);
    setRecode(recodeValue);
  }, [paramKey]);

  return [recode, setRecode];
};
export default useReCode;
// 使用  
// ?recode=12345&ffff=00000
import useReCode from "./hooks/useReCode";
// 事件调用
const [paramKey, setParamKey] = useState('recode'); // 默认属性可改为空
const [recode, setRecode] = useReCode(paramKey);
  const changeParamKey = () => {
    // 假设按钮点击时更改参数键值
    setParamKey('ffff');};
<Button type="primary" onClick={changeParamKey}>Change Param Key</Button>
<p>{recode}</p>
  • 有些自定义hooks类似在vue2中vue内置过滤器filters,在渲染list时,每一项都会执行后面的方法,vue3之后全面hooks化。

组件传值(常用方法)

  1. 通过 props 传递数据(父组件传递数据给子组件)
  2. 通过回调函数传递数据(子组件传递数据给父组件)
  3. 使用 Context API(适用于跨越多个组件层级的数据共享 useContext
  4. 使用状态管理库(如 Redux 或 Zustand)

父传子 ------ 案例

import React, { useState } from "react";

// 子组件
const ChildComponent = ({value}) => {
  return <div>{value}</div>;
};
// 通过 props 传递数据(props是一种用于在组件树中传递数据的机制)
// 父组件
const ParentComponent = () => {
  const [message, setMessage] = useState("Hello from Parent Component!");

  return (
    <div>
      <h1>父组件</h1>
      <ChildComponent value={message} />
    </div>
  );
};

export default ParentComponent;

子传父 ------ 案例

import React, { useState } from "react";

// 子组件
const ChildComponent = ({ onMessageChange }) => {
  const handleChange = (event) => {
    onMessageChange(event.target.value);
  };

  return (
    <div>
      <input type="text" onChange={handleChange} placeholder="请输入...." />
    </div>
  );
};
// 通过事件携带参数的形式传递props(属性)
// 父组件
const ParentComponent = () => {
  const [message, setMessage] = useState("");

  return (
    <div>
      <h1>父组件</h1>
      <p>子组件传递的值: {message}</p>
      <ChildComponent onMessageChange={setMessage} />
    </div>
  );
};

export default ParentComponent;

useContext 在组件树中访问 Context 对象的值。Context 使得你能够在组件树中共享数据,而不需要通过 props 逐层传递。 useContext 允许你在函数组件中轻松地访问 Context 提供的值。

!不管组件包裹多少层,都能通过Context 传递

import React, { createContext, useState, useContext } from "react";

// 创建 Context  这个对象包含了 Provider 和 Consumer 组件,用于在组件树中共享数据。
const MessageContext = createContext();

// 内层组件
const ChildComponent = () => {
  const { message, setMessage } = useContext(MessageContext);

  // 从内层组件修改外层组件的状态
  const handleChangeMessage = () => {
    setMessage("来自内层组件的新消息!");
  };

  return (
    <div>
      <div>{message}</div>
      <button onClick={handleChangeMessage}>内层点击更新消息</button>
    </div>
  );
};

// 中层组件
const MiddleComponent = () => {
  return (
    <>
      <div>
        中层组件
        <ChildComponent />
      </div>
    </>
  );
};

// 外层组件
const TextUseContextProvider = () => {
  const [message, setMessage] = useState("来外层组件的信息!");
  return (
    <>
      <MessageContext.Provider value={{ message, setMessage }}>
        <div>
          <h4>外层组件</h4>
          <MiddleComponent />
        </div>
      </MessageContext.Provider>
    </>
  );
};

export default TextUseContextProvider;
Provider的作用

Provider 组件的作用是提供 Context 的值,这个值会被其子组件(包括子孙组件)访问到。通常在树的高层次使用 Provider 来设置 Context 的值,并让下层组件能够访问和更新这些值。

Consumer的作用

Consumer 组件则用于在组件树中消费 Context 的值。它需要一个函数作为子元素,这个函数接收当前的 Context 值并返回一个 React 元素。

! 如果使用 Consumer,代码会变得稍微复杂一些。以下是如何用 Consumer 替代 useContext

import React, { createContext, useState } from "react";

// 创建 Context
const MessageContext = createContext();

// 内层组件
const ChildComponent = () => {
  return (
    // 需要一个函数作为子元素,这个函数接收当前的 Context 值并返回一个 React 元素
    <MessageContext.Consumer>
      {({ message, setMessage }) => (
        <div>
          <div>{message}</div>
          <button onClick={() => setMessage("来自内层组件的新消息!")}>
            内层点击更新消息
          </button>
        </div>
      )}
    </MessageContext.Consumer>
  );
};

// 中层组件
const MiddleComponent = () => {
  return (
    <>
      <div>
        中层组件
        <ChildComponent />
      </div>
    </>
  );
};

// 外层组件
const TextUseContextConsumer = () => {
  const [message, setMessage] = useState("来外层组件的信息!");
  return (
    <>
    {/* 向包裹的组件提供message, setMessage 内层组件可以访问和更新这些值 */}
      <MessageContext.Provider value={{ message, setMessage }}>
        <div>
          <h4>外层组件</h4>
          <MiddleComponent />
        </div>
      </MessageContext.Provider>
    </>
  );
};

export default TextUseContextConsumer;

  • 对比: Provider比Consumer写法更加简洁,更加灵活,可读性强

React第三方Hook

ahooks 国内流行

​ 比如:useMousePosition ,就有现成的

react-use 国外流行