react 学习笔记 | JSX - React组件 - 钩子函数 - redux - RTK - Hook

文章目录

react学习笔记

学习中get到的新用法

  1. Date类的toLocalString方法,可以更为灵活的处理Date类。

  2. 标签属性中闭包的使用
    举例:仅在删除状态时使用id,不需要单独传递id属性。

    const logItemDate = logsData.map(item=> <LogItem  onDelLog ={()=> delLog(item.id)}>)
    
  3. 移动端适配 rem + vw
    可以使用vw获取视口宽度,将font-size设置单位为vw,然后结合rem做适配。
    1vw = 视口宽度的1% -> 100vw = 视口的宽度

    一般设置html的font-size值 = 屏幕宽度/设计稿宽度,但移动端比如375px计算出的font-size值小于12px会造成一些错误和奇怪的问题,因此把比例扩大100倍

    为了使比例不变,相应的设计图元素使用时设计图元素大小/100 rem

    根html的font-size值 = 屏幕宽度/设计稿宽度*100 
    font-size = 100vw/设计稿宽度*100
    
  4. 对象的连续解构

    const props = {stu:{id:1,attributes:{name:xxx,age:xxx}}}
    const {stu:{id,attributes:{name,age}}} = props
    

入门

概述

AJAX+DOM可以实现网页的局部刷新,但是新数据不能直接在网页中显示,需要通过DOM将数据转换为网页中的节点。

react帮助我们根据不同的数据来快速构建用户项目,同时在构建过程中确保其流畅度。

react特点

1.使用虚拟DOM而不是真正的DOM

2.声明式编码(声明式:结果为导向,不关心结果 命令式:一行代码一个命令)

3.支持服务器端渲染

React 基础案例HelloWorld

入门案例采用外部引入脚本使用(正常开发使用包管理器)

  • react.development.js reactreact核心库,只要使用react就必须要引入。下载地址
  • react-dom.development.js react-domreactdom包,使用react开发web应用时必须引入。下载地址
  • babel.min.js 浏览器不能识别JSX,利用该babelJSX转换为JS代码。下载地址

1.引入脚本

<script src="../script/react.development.js"></script>
<script src="../script/react-dom.development.js"></script>

2.创建一个React元素

React.createElement(组件名/元素名,元素中的属性,元素的子元素/内容)

const reactDiv = React.createElement('div',{},'我是react创建的div'); 

3.获取根元素对应的React元素

ReactDOM.createRoot(Dom元素);

// html
<div id="root"></div>
// js
const root = ReactDOM.createRoot(document.getElementById('root'));

4.将reactDiv渲染到React根元素中

root.render(reactDiv)
三个API介绍
  • React.createElement(type,[props],[...children]) 用来创建React元素(并不是ReactDom,所以这里使用React调用)

    • class属性需要使用className属性代替。

    • type如果是标签名(元素)需要全小写,首写母大写会被认为是组件

    • 在设置属性时,事件名应遵守驼峰命名法,事件值需要是一个函数,不能是console.log(xx)这种表达式。如果直接写一个函数调用语句,则在绑定事件时就会被调用(之后事件不会被触发)

    • React元素是一次性的,一旦创建就无法修改,只能使用新创建的元素进行替代

  • ReactDOM.createRoot(container[,options]);用来创建React的根容器,根容器用来放置React元素

    • 将参数的DOM元素转换为React根元素
  • ReactDOM实例.render(ReactElement)React元素渲染到根元素中

    • DOM根元素中所有的内容都会被删除(不会修改DOM根元素本身),被React元素转换而成的DOM元素替换
    • 重复调用render(),React会将两次虚拟DOM进行对比,确保只修改发生变化的元素,对DOM做最少修改。首次调用时,容器节点里的所有DOM都会被替换,后续的调用则会使用ReactDOM差分算法(diff)进行更新

JSX

上述方法中React.createElement('button', {}, '我是按钮')还是命令式编码方法,告诉reactcreateElement去创建一个button按钮,该按钮没有属性,内容为我是按钮。

声明式编程结果导向,告诉结果,不关系过程怎么样。

const button = <button>我是按钮</button>; // 告诉react我需要一个button按钮元素,不关心react如何创建

React中可以通过JSXJavaScript Syntax Extension)来创建React元素,JSX 让我们以类似于HTML 的形式去使用 JSJSXReact中声明式编程的体现方式。

JSX需要被翻译为JS代码,才能被React执行。 要在React中使用JSX,必须引入babel来完成“翻译”工作。

  • JSX就是React.createElement()的语法糖,最终都会转换为以调用React.createElement()创建元素的代码。

  • JSX在执行之前都会被babel转换为JS代码

    <!-- 引入babel -->
    <script src="script/babel.min.js"></script>
    <!--设置js代码被babel处理-->
    <script type="text/babel">
        const div = <div>
            我是一个div
            <button>我是按钮</button>
        </div>;
     	const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(div);    
    </script>
    
  • JSX不是字符串,不需要加引号

const div =  <div>我是一个div</div>  // 正确写法   
  • JSXhtml标签应该小写开头,React组件应该大写开头
<div> // 小写html标签
<Div> // 大写组件
  • JSX有且只有一个根标签

  • JSX的标签必须正常结束(自结束标签必须写/)

const input = <input type="text" / >
  • JSX中使用{}嵌入表达式(有值的语句就是表达式)
const name = "ranran"
const div = <div>{name}</div> // 才会显示ranran,没有括号会把name识别为字符串
  • 如果表达式值为空值、布尔值、undefined,将不会显示

  • JSX属性可以直接在标签中设置

    • 事件绑定需要是一个函数,而不能直接是函数调用(绑定时就会被触发,不会延迟触发)
    • className代替class
    • style必须使用对象设置,属性名必须用驼峰命名法
const div = <div onClick="()=>{console.log('ranran')}" 
 style={{backgroundColor: "yellowgreen", border: '10px red solid'}}
></div> // 外面的大括号表示style必须使用对象设置,里面的对象表示给他设置的值是一个对象(有多个样式)
  • 在语句中可以操作JSX

    const name = 'ranran';
    const lang = 'cn';
    
    let div;
    if(lang === 'en'){
        div = <div>hello {name}</div>;
    }else if(lang === 'cn'){
        div = <div>你好 {name}</div>;
    }
    const root = ReactDOM.createRoot(document.getElementById('root'))
    root.render(div)
    
JSX 解构数组

JSX在解构{}的内容时,如果内容是数组则会自动将其展开。

//页面:孙悟空猪八戒沙和尚
const data = ['孙悟空', '猪八戒', '沙和尚'];
const div = <div>{data}</div> 
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(div)

/*
· 孙悟空
· 猪八戒
· 沙和尚
*/
const data = ['孙悟空', '猪八戒', '沙和尚'];
const list = <ul>{data.map(item => <li>{item}</li>)}</ul>;
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(list)//页面:孙悟空猪八戒沙和尚
const data = ['孙悟空', '猪八戒', '沙和尚'];
const div = <div>{data}</div> 
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(div)

/*
· 孙悟空
· 猪八戒
· 沙和尚
*/
const data = ['孙悟空', '猪八戒', '沙和尚'];
const list = <ul>{data.map(item => <li>{item}</li>)}</ul>;
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(list)

React通过虚拟DOMReact元素和原生DOM元素进行映射

当我们调用root.render时。页面就会发生重新渲染
React通过diff算法将新的虚拟DOM和旧的比较,找到发生变化的元素,并且只对变化的元素进行修改。

数组中(当前数组)每一个元素都需要设置一个唯一key
重新渲染页面时,Reactkey值会比较key值相同的元素,没key值会按照顺序进行比较。

  1. 开发中一般会采用数据的 id 作为 key
  2. 尽量不使用元素的 index 作为 key 索引会跟着元素顺序的改变而改变,所以使用索引做 key 跟没有 key 是一样的。 唯一的不同就是,控制台的警告没了。 当元素的顺序不会发生变化时,用索引做 key 也没有什么问题。
const data = ['孙悟空', '猪八戒', '沙和尚'];
const list = <ul>{data.map(item => <li key={ item }>{ item }</li>)}</ul>;
// const list = <ul>{data.map((item,index) => <li key={ index }>{ item }</li>)}</ul>;
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(list)

创建react项目(手动)

React官方为了方便开发,提供react-scripts包(①打包②测试服务器-根据代码变化自动刷新避免改一点就重新打包),包中提供了项目开发中的大部分依赖。
由于提供了配置好的工具,我们一些操作就要符合约定。

使用包管理器管理项目,没有办法直接放在网页中运行。需要经过webpack打包,才能在浏览器中正常执行。

  1. 创建React
根目录
    - public(可以web直接访问的文件,不用打包就可以浏览器访问的静态资源)
        - index.html (入口文件,必须有,首页模板打包时以此为模板生成最终的index/html | 添加标签 <div id="root"></div>- src(源码,JS源代码)
        - index.js(必须,webpack打包文件的入口,该文件会被自动引入public/index.html中)
  1. pnpm init 初始化项目,生成package.json文件(大部分时候这一步可以省略)
  2. pnpm install react react-dom react-scripts 安装项目依赖
  3. 编写代码src/index.js
// 引入ReactDOM
import ReactDOM from 'react-dom/client';

// 创建一个JSX
const APP = <div><h1>这是一个react项目</h1></div>

// 获取一个根元素
const root = ReactDOM.createRoot(document.getElementById('root'));
// 将APP渲染进根容器
root.render(APP);
  1. 运行项目
  • pnpm react-scripts build 打包项目,一般开发完成之后需要上线时使用该命令进行打包。
    初次需要输入y确认。打包时需要默认配置,会询问是否添加默认配置。

在这里插入图片描述

正常情况,右键打开会报错。因为打包好的文件需要部署在服务器上运行,而不是直接使用浏览器打开。每次打包后路径都是这样需要手动修改。

在这里插入图片描述

  • pnpm react-scripts start 开发中使用的命令
    通过webpack启动内部的测试服务器,可以实时对更新代码进行编译。这个命令太长,可以在package.jsonscripts 选项中配置命令,下次可以使用命令pnpm start

    "scripts": {
      	"start": "react-scripts start"
    }
    

react 一定需要两个文件

  • public/index.html:入口文件,首页模板打包时以此为模板生成最终的index/html - 提供dom root根节点
  • src/index.jswebpack打包文件的入口,该文件会被自动引入public/index.html中 - 将root转化为react根节点元素后,将react元素挂载到react根节点中

创建React项目(自动) | create-react-app

命令:npx create-react-app 项目名

除了public/index.htmlsrc/index.js必须保留外,其他的东西都是可以删除的。

/*
  reate-react-app 创建index.js
  其中<React.StrictMode>使用严格模式渲染React元素 - 可以不使用
*/
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    aaa
  </React.StrictMode>
);


事件处理

react 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。

  • 使用 JSX 语法时需要传入一个函数作为事件处理函数。事件绑定需要是一个函数,而不能直接是函数调用(绑定时就会被触发,不会延迟触发,等于将函数的返回值给了该事件)

    // 传统 HTML
    <button onclick="activateLasers()">
      Activate Lasers
    </button>
    // React
    <button onClick={activateLasers}>  
       Activate Lasers
    </button>
    
  • React事件通过会传递事件对象event,但其不同于原生的事件对象,是React包装后的事件对象,该对象已经处理了跨浏览器的兼容性问题。

    React中事件回调函数不能通过返回false阻止默认行为,必须显式地使用event事件对象的preventDefault方法

    // 传统 HTML
    
    <form οnsubmit="console.log('You clicked submit.'); return false">
      <button type="submit">Submit</button>
    </form>
    
    // React
    function Form() {
      function handleSubmit(e) {
        e.preventDefault();    
        console.log('You clicked submit.');
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <button type="submit">Submit</button>
        </form>
      );
    }
    

React中的CSS样式

内联样式 | 内联样式中使用state (不建议使用)

style必须使用对象设置,属性名必须用驼峰命名法

const StyleDemo = () => {
    return (
        <div style={{color:'red', backgroundColor:'#bfa', fontSize:20, borderRadius:12}}>
            我是Div
        </div>
    );
};

export default StyleDemo;

当样式过多,JSX会比较混乱,可以使用变量去保存对象

import React from 'react';

const StyleDemo = () => {
    const divStyle = {color: 'red', backgroundColor: '#bfa', fontSize: 20, borderRadius: 12}

    return (
        <div style={divStyle}>
            我是Div
        </div>
    );
};

export default StyleDemo;

内联样式中使用state
当样式是动态时,可以在样式中使用state变量。

import React, {useState} from 'react';

const StyleDemo = () => {

    const [showBorder, setShowBorder] = useState(false);

    const divStyle = {
        color: 'red',
        backgroundColor: '#bfa',
        fontSize: 20,
        borderRadius: 12,
        border: showBorder?'2px red solid':'none'
    };

    const toggleBorderHandler = ()=> {
      setShowBorder(prevState => !prevState);
    };

    return (
        <div style={divStyle}>
            我是Div
            <button onClick={toggleBorderHandler}>切换边框</button>
        </div>
    );
};

export default StyleDemo;
外部样式表 | CSS Module

外部样式是指将样式编写到外部的css文件中,直接通过import引用。

直接import引入的样式都是全局样式,其他组件也看得见这个样式。如果不同的样式表中出现了相同的类名,会出现相互覆盖情况。

import './index.css'

CSS Module
使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一

如果引用同一个模块,计算出来的类名是相同的。

CSS ModuleReact中已经默认支持(前提是使用了react-script)

  1. 文件样式的文件名为xxx.module.css
  2. 在组件中引入样式的格式为import xxx from './xxx.module.css'
  3. 设置类名时通过xxx.yyy的形式来设置
/*
StyleDemo.module.css
*/
.myDiv{
    color: red;
    background-color: #bfa;
    font-size: 20px;
    border-radius: 12px;
}

/*
StyleDemo.js
*/
import Styles from './StyleDemo.module.css';

const StyleDemo = () => {
    return (
        <div className={Styles.myDiv}>
            我是Div
        </div>
    );
};

export default StyleDemo;

React组件

组件需要遵守的规则

  • 组件名首字母必须大小(小写字母开头的组件会被视为原生DOM标签)
  • 组件中只能有一个根元素

函数式组件和类组件

React中定义组件有两种方式

  • 基于函数的组件 - 函数式组件(推荐) :函数组件是返回JSX普通函数
  • 基于类的组件 - 类组件

函数式组件

函数组件是返回JSX普通函数

//1.创建函数式组件 App.js
const App = () => {
  return <div>我是App组件!</div>
};
// 2.导出App
export default App;


// index.js
// 3.引入App
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
// 4.React组件可以直接通过JSX渲染
root.render(<App/>);	//root.render(App()); 也可以,只是<App/>内部做了更多的事情。

类组件

1.创建一个ES6 class,并继承于React.Component

2.添加一个render方法,方法的返回值为JSX

import React from "react"
//1.创建类组件  必须要继承React.Component
class App extends React.Component{
    constructor(props){ // 参数props接受父组件的传值
        this.state = 'xxx' //state的使用
    }
    // 2.添加render方法
    render(){
        return <div>我是一个类组件{this.props}</div>
    }
}

// index.js
// 3.引入App
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
// 4.React组件可以直接通过JSX渲染
root.render(<App/>);	//root.render(App()); 也可以,只是<App/>内部做了更多的事情。

props、state、ref

  • 类组件的props存储在类的实例对象中,可以通过this.props访问。

  • 类组件中state统一存储到了实例对象的state属性中,可以通过this.state来访问,通过this.setState()修改。

    • 通过this.setState修改state,只修改设置了state的属性,并不会修改没设置的第一层属性。
  • 通过React.createRef()(函数式为useRef)创建属性存储DOM对象,同样通过对象.current获取

  • 事件回调函数需要定义为类的方法,建议使用箭头函数,这样this指向的是react实例。否则函数里的this会执行设置事件的dom元素

import React,{ Component } from 'react'

class App extends Component{
 	state = {
        count:0,
        age:{} 
    }  
    divRef = React.createRef();
    // 事件回调函数需要定义为类的方法,建议使用箭头函数,这样this指向的是react实例。否则函数里的this会执行设置事件的dom元素
    clickHandler = ()=>{
        // 写法1:this.setState({count:this.state.count+1})
        // 写法2
        this.setState(prevCount => {
            return {
                count:prevCount+1;
            }
        })
    }
    retnder(){
        return <div>
            <h1 ref={ divRef }>this.props<h1>
            <h1 @onClick={this.clickHandler}>this.state.count<h1>   
        </div>
    }
}
生成一组标签/组件

react中对于根据数组数据产生一组标签或者一组组件,没有类似vuev-for指令,一般使用{ data.map(JSX) }的语法进行生成。

const App= () => {
	const data = [{title:"1",id:"0"},{title:"2",id:"1"},{title:"3",id:"2"}];
    return <div>
    { data.map(item => <Button key={item.id} titile={item.title}></Button >) }
    </div> 
    
    /*
     写法1 
     return <div> { data.map(item => <Button key={item.id} titile={item.title} />) }  </div> 
    写法2:将对象的每个属性都传递
    return <div> { data.map(item => <Button {...item}>) </div>
	}
    */
};

export default App;

props 父组件给子组件传属性/方法

使用场景模式

  • 父组件负责请求和处理数据
  • 子组件没有自己的状态,主要负责展示数据,和调用父组件修改数据的方法
  • 数据的改变发生在父组件里,父组件数据改变后,子组件重新渲染

通信方式
父 -> 子:子组件props接受,props是只读属性是无法修改
子 -> 父:父组件props传递函数,子组件里调用函数并传参

// 父组件
export const Father = ()=>{
  const [state,setState] = useState('state')
	return <Child state={state} setState={setState}/>
}

// 子组件
const Child = (props)=>{
const { state,setState } = props
  const onButtonClick = () => {
    setState('后代知道了')
  }
return(
	{state}
	<button onClick={onButtonClick}>后代知道了</button>
	}
}
给组件设置className样式不生效

原因

className会被认为是一个属性传递给子组件,需要在子组件的根元素使用className={props.className}接收。

state 维护组件的响应式状态

React中,当组件渲染完毕后,再修改组件中的变量,不会使组件重新渲染。state相当于一个变量,只不过在React中进行了注册。React会监控整个变量的变化,当state发生变化时,会自动触发组件的重新渲染。

页面的渲染靠的是render函数

state概述

stateprops类似,都是一种存储属性的方式。

  • state只属于当前组件(组件的私有属性),其他组件无法使用。
  • state的值是对象,当其内容发生变化相关组件会一起刷新
useState(stateInitValue)

通过钩子函数useState(stateInitValue)创建stateReact中钩子函数只能用于函数组件或自定义钩子。

  • 参数是整个state变量的初始值

  • 函数返回一个数组[stateVariable,setStateFunction],第一个元素是state变量的初始值(只用于显示),第二个元素是修改该变量的函数(函数的参数为新值)。调用修改函数修改state变量的值(state值发生变化)会触发组件的重新渲染,直接修改state变量不会触发组件的重新渲染。

    import { useState } from 'React'
    const [stateVariable,setStateFunction] = useState(1);
    

组件重新渲染等同于函数重新执行,函数体的代码会再次调用(赋值语句等会重新赋值)。setState()是由钩子函数useState()生成的,useState()会保证数组的每次渲染都会获取到相同的setState()

注意点

  1. state值是一个对象时,setState()修改时,使用新的对象去替换已有对象。
const [user, setUser] = useState({name:"ranran",age:18})
user.name = "xxx"; 
serUser(user); // user是对象,对象的地址没有发生变化,所以不会引起组件重新渲染

/* 
解决方案:将其拷贝给另一个新对象,修改新对象的属性
*/
setUser({...user,name:"xxx"}) // 后面的name会覆盖前面的name
  1. 通过setState()去修改一个state时,并不表示修改当前的state,修改的是组件下一次渲染的state

  2. setState()会触发组件的异步渲染(并不是马上调用就渲染,放入事件循环队列中等待执行),所以当调用setState()需要使用state值时,可能出现计算错误。

    因为setState()修改的是下一次渲染的state,如果下一次渲染还没进行前又调用了setState(),此时state还是旧值,所以就会出现计算错误。

    解决办法 : 通过传递回调函数的形式修改state

    回调函数的返回值会成为新的state值,回调函数执行时React会将最新的state值作为参数传递。
    总结:如果setState()中需要用到旧值,参数都采用函数的形式。

    setCount(state => state+1); // 传递参数,React会保证参数的state是最新值
    

Ref 获取DOM对象

Refreference的简写,用来获取真实DOM的引用。

  • 使用useRef()钩子函数获取DOM对象
    • 1.通过useRef()钩子函数返回一个普通JS对象,React会自动将DOM对象传递到该对象的current属性中。
    • 2.被引用的DOM元素上添加ref属性,值为上述的对象。
      根据描述,直接创建一个有current属性的普通JS对象可以实现相同的效果。

两种方法的不同点

  • 自定义对象方法,组件每次重新渲染,都会创建一个新对象
  • 使用useRef()函数返回的对象的声明周期和组件的声明周期一致,所以每次重新渲染,该ref对象都是原来的。
import {useRef} from 'react';

const MyComponent = () => {

    const divRef = useRef();
	/*
	const divRef = {current:null}
    */
    const clickHandler = () => {
        console.log(divRef);
    };

    return (
            <div ref={divRef} onClick={clickHandler}>一个div</div>      
    );
};

export default MyComponent;

非受控组件与受控组件

非受控组件:表单中的数据来源于用户填写的组件,表单元素的值不会更新state,输入数据都是现用现取的。

受控组件:使 Reactstate 成为唯一数据源,由state控制表单。

数据的双向绑定

将表单的value绑定为state数据,表单的onChange事件触发时,通过事件对象event获取到新值,然后使用setState修改state的值为新值。

import { useState } from 'react';
import './index.css';

const Demo = () => {
    // 如果有多个表单,可以将表单数据设置为一个对象
    const [inputValue, setInputValue] = useState('');
    return (
        <>
            <input
                type="text"
                className="inputDemo"
                value={inputValue}
                onChange={e => {
                    setInputValue(e.target.value);
                }}
            />
        </>
    );
};

export default Demo;
子组件给父组件传值 = props传递函数 + 子组件调用函数
  1. 在父组件中,使用props给子组件传递一个自定义事件
  2. 在子组件中将需要传递的数据作为函数参数,调用函数
// 父组件
<LogsItem onSavaLog={ savaLogHandler }>

// 子组件
const LogsItem = (props) => {
	props.savaLogHandler("需要传递的数据");
}

关于传递setState函数给子组件的一些说法:尽量不要这样做,state在哪里,setState尽量就在哪里。

vue中v-if与v-show的React写法
  • v-if-v-else配对出现 可以使用条件判断
  • v-show/仅有v-if 可以使用&&
// v-if/v-else 可以使用条件判断
控制变量 ? v-if显示的 : v-else显示的

// v-show/仅有v-if 可以使用&& 
控制变量 && v-show显示的

如果显示出来的组件内部需要修改外部的控制变量,react中一般的做法时将函数作为参数传递。因为控制变量在外部,内部只需要调用该函数,外部修改控制变量的值。

Portal 将元素渲染到指定位置

React中,父组件引入子组件后,子组件会直接在父组件内部渲染。换句话说,React元素中的子组件,在DOM中,也会是其父组件对应DOM的后代元素。

问题描述
每个组件都是相同的构成(想象成一个列表),组件内部包含一个子组件,该子组件的作用是生成一个遮罩覆盖全局。
组件1开启相对定位,遮罩开启固定定位(不一定是和这个例子相同的定位方式,这里举例)
由于组件1组件2组件3的 z-index:1,后面的组件会覆盖前面的。所以组件1中的遮罩出现时,覆盖不了组件2组件3,即使遮罩的z-index:999(理解为在组件1内部元素的层级中占比很高,但不影响组件1的层级),但组件1和其他兄弟组件层级相同(父元素组件1都被覆盖了子元素肯定被一起覆盖)。


结构问题:遮罩需要遮住视图不应该作为组件123的子组件,如果必须这样写,解决办法是使用Portal 将组件渲染到指定位置

ReactDOM.createPortal(需要渲染的元素,传送到的指定位置):渲染元素时将元素渲染到网页中的指定位置
1.在index.html中添加一个新的元素

<div id="root"></div>
<!--这个容器用来专门渲染遮罩层-->
<div id="backdrop"></div>

2.在组件中通过ReactDOM.createPortal()将元素渲染到新建的元素中

const backdropDOM = document.getElementById('backdrop');

// 在其他组件内部正常使用Backdrop组件,但是该组件渲染时会被传送到专门渲染遮罩层的容器中渲染,会脱离原来的结构
const Backdrop = () => {
  return ReactDOM.createPortal(
  <div>
  	{props.children}
  </div>,
      backdropDOM
  );
};

Fragment 组件

React中,JSX必须有且只有一个根元素,这导致在某些情况需要添加一个额外的父元素(并没有实际意义)

  • React提供了Fragment组件,Fragment可以让你聚合一个子元素列表,并且不在DOM中增加额外节点
  • <></>Fragment的语法糖,<></> 语法不能接受键值或属性,但Fragment可以传递 key 属性
import React from 'react';

const MyComponent = () => {
    return (
        <React.Fragment>
            <div>我是组件1</div>
            <div>我是组件2</div>
            <div>我是组件3</div>
        </React.Fragment>
        /*
        <>
            <div>我是组件1</div>
            <div>我是组件2</div>
            <div>我是组件3</div>
        </>
        */
    );
};

export default MyComponent;

Context 祖先组件向子孙组件传值

Context相当于一个公共的存储空间

说明

  • context容器里的值一旦变化,所有依赖该 context 的组件全部都会 force update
使用步骤

1.创建Context容器对象

// context.ts
import { createContext } from 'react';
/*
 提供共享数据时,需要<MyContext.Provider>使用,如果不想写这么长,可以使用
 const {Provider }= createContext() 先将其解构出来,直接使用<Provider>
*/	
export const MyContext = createContext();

2.为Context容器提供共享的数据。在祖先组件中(或者是祖先组件中需要数据的子组件里)使用<Context.Provider value={}> value属性的值就是放在容器中的共享数据,在<Context.Provider>包裹下的所有子孙组件,都可以获取到容器中的共享数据。

Provider设置在外层组件中,通过value属性来指定Context的值。这个Context值在所有Provider的子组件中都可以访问。

// ancestor.ts
import { MyContext } from './context.ts'

const Ancestor = ()=>{
	const [state, setState] = useState("state")
	return(
		<MyContext.Provider value={{state, setState}}> 
			<Father/>
		<MyContext.Provider >
	)
}

3.子孙组件使用Context里的共享数据

方式1:使用钩子函数useContext(哪一个context)获取到该context的共享数据,该钩子函数会返回Context中的数据(常用)。

// 某一个子孙组件.ts
import React, {useContext} from 'react';
import { MyContext } from './context.ts'

const MyComponent = () => {
    const ctx = useContext(MyContext);
    return (
   		<h1>ctx.state</h1>
    );
};

方式2:通过Consumer标签来访问到Context中的数据(不常用)

该组件内部必须使用函数,解析时会调用该函数,将创建的defaultValue作为该函数的参数传递。

import React from 'react';
import { MyContext } from '../store/test-context';

const MyContext = () => {

    return (
        <MyContext.Consumer>
            {(ctx)=>
                return (
                	 <h1>ctx.state</h1>
                );
            }}
        </MyContext.Consumer>

    );
};
export default MyComponent;

Effect 副作用

组件每次重新渲染,组件的函数体就会执行。

有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是不能直接写在函数体中。

例如,如果直接将修改state的逻辑编写到了组件之中,每次函数体执行设置基础值,state变量又引起组件的更新,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。

setState()在函数组件中的执行流程

setState()会调用dispatchSetDate()方法,dispatchSetDate()方法的主要逻辑

  • 判断组件当前处于什么阶段(渲染阶段 |非渲染阶段 )

    • 处于渲染阶段:不会检查state值是否相同,在此时直接将setState设置的值放入渲染队列等待渲染

    • 处于非渲染阶段:检查setState设置的值与之前的值是否相同。如果值不同,对组件进行重新渲染;如果值相同,则不对组件进行重新渲染。

处于渲染阶段案例

const App = () => {
	const [count,setCount] = uesState(0);
    // 会触发Too many re-renders报错 
    // 调用的时候处于渲染阶段,因为div没有渲染到页面上,所以会引发重新渲染,再次调用组件函数。也就是说无限循环,不会退出渲染阶段。
	setCount(0); 
    return (
    	<div>
        	{count}
        </div>
    )
}

待解决问题:之前学习说setState()会触发组件的异步渲染,但setCount的时候组件还处于渲染阶段,所以不会等待本次渲染完成(或者说不会渲染本次),直接开始重渲染?
=> 或者说之前学习说的太笼统了,现在对组件的状态进行了区分,明确了setState的执行流程?

处于非渲染阶段案例

第一次点击按钮count = 0 -> 1,组件重新渲染。

第二次点击按钮count = 1 -> 1,组件重新渲染。

第三次点击按钮count = 1 -> 1,组件没有重新渲染。

这是因为当值相同时,React在某些情况下(通常发生在值第一次相同时)会继续执行当前组件的渲染(这里指的时组件函数执行并更新页面),这次渲染不会产生实际效果(这里应该仅重新执行组件函数并不更新页面,不触发刷新没有什么用??)并且不会触发子组件的渲染。

const App = () => {
	const [count,setCount] = uesState(0);

	const clickHandler = ()=>{
        setCount(1); 
    }
    return (
    	<div onClick={ clickHandler }>
        	{count}
        </div>
    )
}
React.StrictMode

脚手架自动生成的index.jx中使用了该组件,该组件表示react自身开启严格模式,开启后react会自动去检查组件中是否有副作用的代码(并不是很智能)。

root.render(
	<React.StrictMode>
    	<App/>
    </React.StrictMode>
)

React的严格模式,在开发模式下,会主动重复调用一些函数,以使副作用出现。这些函数会被调用两次,如果安装了React Developer Tool,调试作用的第二次调用会显示为黑色。

  • 类组件的 constructor, render, 和 shouldComponentUpdate 方法
  • 类组件的静态方法 getDerivedStateFromProps
  • 函数组件的函数体
  • 参数为函数的setState
  • 参数为函数的useState, useMemo, useReducer
useEffect()

useEffect(()=>{},[])是一个钩子函数,可以将会产生副作用的代码编写到useEffect函数的回调中

  • 参数函数会在组件每次渲染完毕(dom渲染完毕)后执行。
  • 在数组中可以指定Effect的依赖项,指定后,只有当依赖发生变化时,Effect才会被触发。通常会将Effect中使用的所有变量都设置为依赖项。
    • 如果依赖项设置为空数组,那么Effect只会在组件初始化时触发一次。
    • 不过不设置依赖项,每次渲染都会执行

useEffect的参数只能是一般函数,不能是异步函数(async)。如果在useEffect里使用异步函数请求数据,需要其外部包装一个一般函数并调用。

Effect的清理函数

useEffect的回调函数中,可以返回一个函数,该函数被称为清理函数,该函数会在下次Effect执行前调用。

可以在清理函数中,清除上一次Effect执行所带来的影响。

const Filter(){
    // 初始化 先Effect回调再清理函数
    // 其他情况,先清理函数再Effect
    const [keyword,setKeyword] = useState();
    useEffect(()=>{ 
        const timer = setTimeout();//初始化时,先设置一个定时器A
    	// 清理函数
        return ()=>{
            /*
            这里形成了一个闭包,timer是定时器A的值。
            下一次Effect执行前,先清理定时器A再生成新的定时器
            */
            clearTimeout(timer);     
        }
	},[keyword])
}

Reducer 整合器 钩子函数

Reducer提供新使用State的方式 ,适用于复杂的State

语法: const [state, dispatch] = useReducer(reducer, initialArg, init);

  • 参数

    • reducer整个函数,对State的所有操作都应该定义在该函数中,该函数的返回值设置为state的新值。
      • reducer函数执行时会受到两个参数,第一个参数为最新的state,第二个参数action对象,该对象会存储dispatch发送的指令(也可以通过不同的key传参)。
    • initialArgstate的初始化值,作用和useState()中值一样
  • 返回值

    • state用于获取state的值
    • dispatch是修改state的派发器,通过派发器发送操作State的命令。

然后执行reducer函数。

// 整合器根据不同的指令执行不同的操作
const [count,countDispatch] = useReducer((state,action)=>{
    if(action.type === 'ADD'){
        return state+1;
    }else if(action.type === 'SUB'){
        return state-1;
    }
    return state; //防止state丢失
},1)
const addHandler = () => {
    countDispatch({type:'ADD'})
}
const subHandler = () => {
    countDispatch({type:'SUB'})
}

问题useReducer在组件中定义,所以每次渲染useReducer都会执行,reducer函数都会重新创建一次(即使之后会回收,但是会重复创建)。

解决办法:为了避免reducer函数的重创建,通常将reducer函数定义在组件外部

React.Memo 缓存组件

React组件会在两种情况下发生重新渲染

  1. 当组件自身的state发生变化
  2. 当组件的父组件重新渲染

React.memo()该方法是一个高阶函数,参数是一个组件A,返回包装过的新组件B。

包装过的新组件B具有缓存功能(类似vue中的keepAlive),只有组件A的props发生变化,才会触发组件重新渲染。

useCallback(回调函数) 创建回调函数

点击parent的增加按钮,改变count的值导致组件重渲染(parent函数被重新执行),导致重新创建clickHandler函数,child依赖的props发生变化,最终memo缓存失效。

const parent = ()=>{
	const [count,setCount] = useState(1)
	const clickHandler = () => {
		setCount(prevState => prevState+1)
	}
	return (
	  <div>
	    <button onClick={clickHandler }>增加 </button>
		<Child onAdd={clickHandler}><Child/>
	 </div>
	)
}

const Child  = (props)=>{
	return <button onClick = {props.onAdd}>增加</button>
}
export default React.memo(Child)

语法:const clickHandler=useCallback(()=>{},依赖数组)
useCallback返回一个回调函数。

  • 当依赖数组中的变量发生变化时,回调函数才会被重新创建。
  • 如果不指定依赖数组,则组件每次渲染时都会重新创建回调函数。
  • 传递空数组,只在组件初始化时创建一次。

在组件重新渲染时,setState()每次返回的都是同一个,所以不需要添加进依赖数组。

自定义钩子

React中的钩子函数只能在函数组件内部直接或自定义钩子中调用。

// App是一个函数组件
function App(){
	// 1.use 使用钩子 √
	function fn(){
		//2.use 使用钩子 ×
	}
}

当需要将React中钩子函数提取到一个公共区域时,可以使用自定义钩子。
自定义钩子的本质是一个普通函数,我们约定该 函数以use开头。

案例

mport {useEffect, useState} from "react";

export default const useFetch = (reqObj) => {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    async function fetchData(){
        try{
            setLoading(true);
            setError(null);
            const res = await fetch(reqObj.url,{
				method:reqObj.method || 'get',
				headers:{
					"Content-type":reqObj.type || "application/json"
				}
			});
            if(!res.ok){
                throw new Error('数据加载失败!');
            }
            const data = await res.json();
            setData(data.data);
        }catch (e){
            setError(e);
        }finally {
            setLoading(false);
        }
    }
    return {data, loading, error, fetchData};
};
// 在app.js中调用
const {data:stuData, loading, error, fetchData} = useFetch();

redux 状态管理

Redux中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作state,而不能直接修改state

Redux可以理解为是reducercontext的结合体,使用Redux可管理复杂的state,又可以在不同的组件间方便的共享传递state

ReduxJS应用的状态容器,并不是只能在React使用,而是可以应用到任意的JS应用中。

在网页中直接使用 redux核心包-不推荐

假设有一个需求:一个计时器,可以通过点击添加按钮增加,也可以通过点击按钮减少
使用Redux的步骤

1.引入redux,引入该js后,提供核心类Redux

<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>

// 2.页面代码
<button id="btn01">减少</button>
<span id="counter">1</span>
<button id="btn02">增加</button>

2.创建reducer整合函数
reducer是一个函数,是state操作的整合函数,每次修改state时都会触发该函数,它的返回值会成为新的state。(功能类似Reducer)

/*
* state表示当前state,当它是对象时返回值也需要是对象
* action是一个js对象,对象中保存操作信息,比如操作类型、操作函数的参数
*/
const countReducer = (state = {count:0}, action) => {
    switch (action.type){
        case 'ADD':
            return {count:state.count+action.addNum};
        case 'SUB':
            return {count:state.count-1};
        default:
            return state
    }
};

3.通过reducer对象创建store
Redux.createStore(reducer, [preloadedState], [enhancer])

  • preloadedState就是state的初始值,可以在这里指定也可以在reducer的参数state中指定。(一定要有初始值)
  • enhancer增强函数用来对state的功能进行扩展。
const store = Redux.createStore(reducer)

store对象创建后,对state的所有操作都需要通过store进行

  • 读取state:store.getState()
  • 修改state:store.dispatch({type:'ADD'})

4.对store中的state进行订阅,当store立的state发生变换时订阅的回调函数会自动执行


store.subscribe(()=>{
    // store中state发生变化时自动触发
});
多个Reducer

如果state数据比较复杂,那么数据处理函数会非常多。
1.先将state分组,写成多个Reducer
2.使用combineReducer将合并多个reducer,合并后传递进createStore来创建store

// 创建Reducer1
const stuReducer = (state = {
    name: '孙悟空',
    age: 18,
    gender: '男',
    address: '花果山'
}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_AGE':
            return {
                ...state,
                age: action.payload
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        case 'SET_GENDER':
            return {
                ...state,
                gender: action.payload
            };
        default :
            return state;
    }

};
// 创建Reducer2
const schoolReducer = (state = {
    name: '花果山一小',
    address: '花果山大街1号'
}, action) => {
    switch (action.type) {
        case 'SET_SCHOOL_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_SCHOOL_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        default :
            return state;
    }

};

// 合并多个Reducer为一个reducer 
const reducer = combineReducers({
    stu:stuReducer,
    school:schoolReducer
});

// 创建store
const store = createStore(reducer);

//通过state.stu读取学生数据,通过state.school读取学校数据

Redux Toolkit(RTK)

1.如果state数据比较复杂,那么数据处理函数会非常多
可以对state进行分组,参考上述多个Reducer
2.返回state时需要对原来的state数据进行复制(解构),再设置新的值。

Redux工具包(RTK):帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作。

安装
RTK或者Redux,在React中使用时都需要安装react-redux
所以RTK在React中使用需要安装两个包。

npm install react-redux @reduxjs/toolkit -S
// -s 相当于 --save,局部安装到dependencies环境中
createSlice(configuration object) 创建reducer切片(小reducer)

语法:createSlice(configuration object)
参数:对象,对象中不同属性指定reducer的配置信息(RTK中几乎所有方法都是)

参数描述
initialStatestate的初始值
namereducer的名字,会作为action中type属性的前缀(case后面的常量前缀),不要重复
reducersreducer的具体方法,需要一个对象作为参数,对象里的方法操作state值。
方法的参数state:代理对象,通过该代理对象修改该切片的state,所以这里可以直接操作代理对象里的属性(不需要完整赋值,可以仅局部修改某属性)

返回值:使用createSlice创建切片后,切片会自动根据配置对象生成包含actionreducer属性的对象

属性描述
action对象action创建器函数组成的对象
创建器函数调用后返回action对象{type:stu/setName,payload:创建器函数的第一个参数}
通常将创建器函数导出,在调用处生成action对象作为dispatch的参数。
reducer需要传递给configureStore方法,使其在仓库生效。

编码规范:一般一个切片一个文件夹

const stuSlice= createSlice({
    name:'stu',
    initialState:{
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    },
    reducers:{
        setName(state, action){
        	// 可以局部修改,该state是一个代理对象
            state.name = action.payload
        }
    }
});

切片对象会根据配置对象中的reducers中的方法自动创建action对象{type:name/方法名},创建器函数的参数
①通过切片对象.actions获取action创造器函数
②创造器函数调用返回action对象

stuSlice.actions; // {setName: ƒ}
// 1.setName接收的是slice自动生成的action创建器函数
const {setName} = stuSlice.actions;
// 2.action创建器函数调用后返回action对象(包含操作state的操作信息)
console.log(setName(参数))
//action对象:type:name前缀/方法名  payload为创建器函数的参数。该对象用于dispatch时的参数
{type:stu/setName,payload:undefiend}
完整案例

前提:现在全局注入store,使用Provider标签

import {Provider} from 'react-redux'
root.render(
	<Provider store={store}>
	</Provider>
)

1.使用createSlice(配置对象)创建切片,并抛出setNameaction创建器对象

const stuSlice= createSlice({
    name:'stu',
    initialState:{
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    },
    reducers:{
        setName(state, action){
        	// 可以局部修改,该state是一个代理对象
            state.name = action.payload
        }
    }
});

const {setName} = stuSlice.actions; 

2.使用configureStore(配置对象)创建store对象

const store = configureStore({
	// 单个切片
    reducer: stuSlice.reducer,
	// 多个切片使用对象写法
    reducer: {
        student: stuSlice.reducer,
    }
});

3.useSelector获取数据与useSelector触发修改
使用useSelector(state => state.name)用来加载state数据,这里的name是store的reducer对象里的key
使用useDispatch()返回dispatch派发函数,通过dispatch触发修改,dispatch函数的参数是action创建器返回的action对象。

const App = () => {
const stu = useSelector(state => state.student);
  const dispatch = useDispatch();
  
  return <div onClick={()=>{
		dispatch(setName('xxx'))
	}}>
  </div>
}
RTK Query 同步数据库与RTK state数据

RTK Query是一个强大的数据获取缓存工具
RTK 简化state操作的同时,提供RTK Query进行数据处理与缓存。

Web应用中加载数据时需要处理的问题

  • 根据不同的加载状态显示不同UI组件
  • 减少对相同数据重复发送请求
  • 使用乐观更新(仅数据加载成功更新,没加载成功使用原始数据),提升用户体验
  • 在用户与UI交互时,管理缓存的生命周期(数据变化后重新更新)

=> RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。
=> RTKQ在发送请求时会根据请求不同的状态返回不同的值,可以通过这些值来监视请求发送的过程并随时中止。

这部分内容跳过了看着很复杂(本次项目没有使用该技术)、可以从2023 react生态系统中寻找替代方案

Hook

  1. React中的钩子函数只能在函数组件内部直接或自定义钩子中调用
  2. 钩子不能在函数或其他语句(if、switch、while、for)中使用

React中自带的钩子函数

钩子函数作用其他
useCallback(回调函数,依赖数组)缓存函数
useMemo(回调函数,依赖数组)缓存函数执行结果,减少函数执行次数
缓存函数组件(函数组件也是一个函数,执行结果是组件内的return)
当缓存组件时
useMemo在父组件中定义(因为需要其执行结果)
React.memo在子组件中定义
useImperativeHandle(ref,回到函数)控制组件ref对象current属性的返回值,可以是值也可以是dom与React.forwardRef()结合使用
通常暴露子组件的方法供父组件调用
useDebugValue用于给自定义钩子设置标签,标签会在React开发工具中显示,用来调试自定义钩子,不常用。

useCallback 缓存函数 与 useMemo 缓存函数执行结果

useCallback 缓存回调函数
语法:const clickHandler=useCallback(()=>{},依赖数组)
useCallback返回一个回调函数。
1.当依赖数组中的变量发生变化时,回调函数才会被重新创建。
2.如果不指定依赖数组,则组件每次渲染时都会重新创建回调函数。
3.传递空数组,只在组件初始化时创建一次。

useMemo 缓存函数执行结果,减少函数执行次数

语法:const result = useMemo(()=>{},依赖数组)
1.当依赖数组中的变量发生变化时,回调函数才会被重新创建。
2.如果不指定依赖数组,则组件每次渲染时都会重新创建回调函数。
3.传递空数组,只在组件初始化时创建一次。

依赖数组的规则基本一致

案例引入
点击按钮触发setCount执行,APP组件重新渲染,sum函数重新执行。每次组件渲染时(APP函数重新执行),sum函数都会执行。假设sum函数非常复杂,执行速度非常慢(会阻塞后面代码的执行),但其实在参数没变的情况下sum函数的结果是不变的(不是每次渲染都需要执行)。为了避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果。

function sum(a,b){ 
	return a+b;
}

const APP = () => {
	const [count,setCount] = useState(1);
	let a = 123;
	let b = 456;
	const result = sum(a,b);
	/*
	const result = useMemo(()=> return sum(a,b),[a,b])
	*/
	// useMemo在父组件中定义,缓存组件 React.memo在子组件中定义
	const some = useMemo(()=>{
			return <Some a={10} b={22}/>
	},[])
	return {
		<div>
			<h1>result: { result }</h1>
			<h2>{count}<h2>
			{some}
			<button onClick={()=>setCount(pre => pre+1)}></button>
		</div>
	}
}

useImperativeHandle() 和 React.forwardRef() 控制组件ref对象current属性的返回值

案例
一个输入框,一个按钮。点击按钮获取输入框的值。(读可以使用这种方式,不过写尽量不要直接修改dom)

const Some= () => {
	// 1.获取输入框的dom
	const inputRef = useRef();
	const clickHandler = ()=>{
		// 3.获取文本
		console.log(inputRef.current.value)
	}
	// 2.在input上绑定ref
	return (
		<div>
			<input ref={inputRef} type="text" />
			<button onClick={clickHandler}>btn<button>
		</div>
	)
}

假如想在some的父组件app中获取到该input值,组件无法直接使用useRef获取到dom,因为一个组件内部有很多dom,不知道返回哪一个。

  • 方法1:React.forwardRef()包裹some组件,组件被包装之后,会多一个ref参数。该参数表示,在外部 使用useRef获取组件时, 返回的dom(将some内部组件的哪个dom结构抛出去)。 --> 这个做法不好,因为通过外部组件直接操作内部组件了(可以读,不要改)

    const Some= React.forwardRef((props,ref) => {
    const inputRef = useRef();
    const clickHandler = ()=>{
    	console.log(inputRef.current.value)
    }
    // 将button的dom抛出去,外部对some组件使用useRef时,获取到button的dom结构
    return (
    	<div>
    		<input ref={inputRef} type="text" />
    		<button ref={ref} onClick={clickHandler}>btn<button>
    	</div>
    )
    })
    
  • 方法2:useImperativeHandle()React.forwardRef()结合使用

    useImperativeHandle()决定 外部使用useRef获取组件时 组件返回的东西(可以是值也可以是dom) --> 子对象给父对象传函数的感觉?
    useImperativeHandle(ref,回调函数) 这里的ref等同于forwardRef参数函数的第二个参数ref,回调函数的返回值会成为ref对象current的值

    // 有点劫持forwardRef的参数ref,修改其返回值的意思
    const Some =React.forwardRef((props,ref)=>{
      	useImperativeHandle(ref,()=>{
      		return{//  一般返回操作dom的函数,不会直接返回dom }
      	})
      })
      // 父组件
      const someRef = useRef();
      useEffect(()=>{
      	someRef.xx // 调用函数
      })
      return(
      	<Some ref = {someRef }>
      )
    

useEffect(回调函数,依赖数组) | useInsertionEffect(回调函数,依赖数组) | useLayoutEffect(回调函数,依赖数组)

三个钩子的执行时机

  • 绘制屏幕指令(异步执行)发出后useEffect就执行,不用等待绘制完毕再执行,所以useEffect里的操作不会影响到组件的渲染。
  • useLayoutEffect由于在DOM改变之后执行,所以可以获取到最新的DOM布局,并在增加动画等
    实际开发中,在useEffect中需要修改元素样式出现闪烁现象时可以使用useLayoutEffect进行替换。
  • useInsertionEffect在DOM改变之前执行,所以可以在此钩子里修改dom结构,比如新增元素,修改样式等。 --> 访问不到ref

react18里useEffect的机制进行了修改,有时候会提前执行,所以这三个钩子之间的差别越来越小了。默认先尝试useEffect,出现状况再使用其他两个钩子。
在这里插入图片描述

useDeferredValue(state) 与 useTransition() 降低state的优先级-不常用

  • useDeferredValue(state )参数是一个state值,为该state创建一个延迟值。
  • useTransition可以降低setState的优先级。
useDeferredValue(state)为该state创建延迟值

useDeferredValue(state )参数是一个state值,为该state创建一个延迟值。

当为state设置了延迟值,每次state修改时都会触发两次组件重新渲染(组件内打印输出语句执行两次)。
这两次重渲染对于其他部分没有区别,对于延迟值来说第一次重渲染是state的旧值,第二次是state的新值。

const [count,setCount] = useState(1);
const deferredCount = useDeferredValue(count);
console.log(count,deferredCount) 
/*
初始化时打印1,1
修改count+1后 第一次重渲染2,1 第二次重渲染2,2
修改count+1后 第一次重渲染3,2 第二次重渲染3,3
*/

一个state需要在多个组件中使用。一个组件的渲染比较快,而另一个组件的渲染比较慢。这样我们可以为该state创建一个延迟值,渲染快的组件使用正常的state优先显示。渲染慢的组件使用延迟值,慢一步渲染。当然必须结合React.memo或useMemo才能真正的发挥出它的作用。

使用场景
在父组件过滤子组件的数据。

存在问题:一个state值被两个组件使用,
假如一个组件(app)的渲染比较快,而另一个组件的渲染比较慢(比如studentList中设置循环很长时间)。当输入1之后,由于在等待StudentList渲染,所以输入框一直没有显示1,这不对input的修改的速度应该非常快,这里被StudentList组件拉慢了速度。

// studentList.js
const STU_DATA = [];
for(let i=0;i<100;i++){
  STU_DATA.push('学生'+i)
}
export const StudentList = (props)=>{
  // 过滤数组
  const stus = STU_DATA.filter(item => item.indexOf(props.filterWord) !== -1)
  return (
    <ul>
      {
      stus.map((item)=>{
        return <li key={item}>{item}</li>
      })
    }
    </ul>
  )
}

// APP.js 通过输入框过滤学生
const [filterWord,setFilterWord] = useState("")
const changeHandle = (e) =>{
  setFilterWord(e.target.value);
}
<input type="text" value ={filterWord} onChange={changeHandle}/>
<StudentList filterWord={filterWord}/>

这种情况可以使用延迟值,由于使用延迟值组件会执行两次。
第一次组件渲染时,deferredFilterWord等于老的filterWord(空),而state已经是新的值了(输入值),所以input显示输入值。
第二次组件渲染时,对于其他部分没影响,对于deferredFilterWord等于新的state值(输入值),实现过滤效果。

StudentList组件是否重新渲染和deferredFilterWord 值关系不大。只要app组件重新渲染,StudentList组件就会重新渲染。如果app组件重新渲染两次,StudentList组件也会重新渲染两次。只要StudentList组件渲染就会降低速度,所以还需要使用React.memouseMemo控制StudentList的渲染次数

// StudentList.js 如果传入的参数变了才重新渲染
export default React.memo(StudentList) 
// APP.js 通过输入框过滤学生
const [filterWord,setFilterWord] = useState("")
const deferredFilterWord = useDeffedValue(filterWord);
const changeHandle = (e) =>{
  setFilterWord(e.target.value);
}
<input type="text" value ={filterWord} onChange={changeHandle}/>
<StudentList filterWord={deferredFilterWord }/>
useTransition()可以降低setState的优先级

使用上述案例,这里在父组件创建两个state,还是会存在卡顿的问题。

// studentList.js
const STU_DATA = [];
for(let i=0;i<100;i++){
  STU_DATA.push('学生'+i)
}
export const StudentList = (props)=>{
  // 过滤数组
  const stus = STU_DATA.filter(item => item.indexOf(props.filterWord) !== -1)
  return (
    <ul>
      {
      stus.map((item)=>{
        return <li key={item}>{item}</li>
      })
    }
    </ul>
  )
}

// APP.js 通过输入框过滤学生  使用两个state
const [filterWord,setFilterWord] = useState("")
const [filterWord1,setFilterWord2] = useState("")
const changeHandle = (e) =>{
  setFilterWord(e.target.value);
  setFilterWord2(e.target.value)
}
<input type="text" value ={filterWord} onChange={changeHandle}/>
<StudentList filterWord={filterWord1}/>

setFilterWord的速度很快,但是setFilterWord2的速度很慢,所以我们希望setFilterWord2自己卡,不要影响到其他数据。setState是异步执行,我们希望setFilterWord执行完成后再执行setFilterWord2(???这里没理解),使用useTransition

startTransition回调函数中设置的setState会在其他的setState修改成功后(组件渲染完成后)生效(组件再次渲染)

// 修改 APP.js 
const changeHandle = (e) =>{
  setFilterWord(e.target.value);
  startTransition(()=>{
  	setFilterWord2(e.target.value);
  })
}
// 写法2
const [isPending,startTransition] = useTransition();
const changeHandle = (e) =>{
  setFilterWord(e.target.value);
  startTransition(()=>{
  	setFilterWord2(e.target.value);
  })
}
方法参数返回值区别
useTransition钩子返回一个数组[isPending,startTransition函数]
isPending会记录startTransition中setState方法的执行状态
需要监控方法执行状态时使用
startTransition函数回调函数

useId

useId()钩子函数返回一个唯一id,但不适用于列表的key。

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
react-native-redux-router是一个用于在React Native应用中管理路由和状态的库。它结合了React Native、ReduxReact Navigation,提供了一种方便的方式来处理应用程序的导航和状态管理。 下面是一个简单的示例,演示了如何在React Native应用中使用react-native-redux-router: 1. 首先,安装所需的依赖项。在终端中运行以下命令: ```shell npm install react-native react-redux redux react-navigation react-native-router-flux --save ``` 2. 创建一个Redux store,并将其与React Native应用程序的根组件连接起来。在App.js文件中,添加以下代码: ```javascript import React from 'react'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import rootReducer from './reducers'; import AppNavigator from './navigation/AppNavigator'; const store = createStore(rootReducer); export default function App() { return ( <Provider store={store}> <AppNavigator /> </Provider> ); } ``` 3. 创建一个导航器组件,并定义应用程序的导航结构。在navigation/AppNavigator.js文件中,添加以下代码: ```javascript import { createAppContainer } from 'react-navigation'; import { createStackNavigator } from 'react-navigation-stack'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Actions } from 'react-native-router-flux'; import HomeScreen from '../screens/HomeScreen'; import DetailsScreen from '../screens/DetailsScreen'; const MainNavigator = createStackNavigator({ Home: { screen: HomeScreen }, Details: { screen: DetailsScreen }, }); const AppNavigator = createAppContainer(MainNavigator); const mapStateToProps = state => ({ // 将Redux状态映射到导航器组件的props中 }); const mapDispatchToProps = dispatch => bindActionCreators(Actions, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(AppNavigator); ``` 4. 创建屏幕组件,并在其中使用导航和Redux状态。在screens/HomeScreen.js文件中,添加以下代码: ```javascript import React from 'react'; import { View, Text, Button } from 'react-native'; import { Actions } from 'react-native-router-flux'; const HomeScreen = () => { return ( <View> <Text>Welcome to the Home Screen!</Text> <Button title="Go to Details" onPress={() => Actions.details()} /> </View> ); } export default HomeScreen; ``` 5. 创建其他屏幕组件,并在其中使用导航和Redux状态。在screens/DetailsScreen.js文件中,添加以下代码: ```javascript import React from 'react'; import { View, Text, Button } from 'react-native'; import { Actions } from 'react-native-router-flux'; const DetailsScreen = () => { return ( <View> <Text>Welcome to the Details Screen!</Text> <Button title="Go back" onPress={() => Actions.pop()} /> </View> ); } export default DetailsScreen; ``` 这是一个简单的示例,演示了如何在React Native应用中使用react-native-redux-router来管理路由和状态。你可以根据自己的需求进行扩展和定制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值