React 组件与状态

  • 组件名必须大写开头
  • 组件应该在顶层定义。不要在组件函数里再定义其他组件。
  • 标签必须闭合,例如使用<br/>而非<br>
  • 组件返回只能返回一个标签,如需要多个可以包裹到一个空的父级里,如<>...</>(但只有其完整形式 <Fragment/> 可以接收 key 属性):
import { Fragment } from 'react';

const listItems = people.map(person =>
  <Fragment key={person.id}>
    <h1>{person.name}</h1>
    <p>{person.bio}</p>
  </Fragment>
);

组件渲染流程

  1. 组件初次渲染,或组件或其祖先的状态发生了改变,触发组件渲染
  2. 组件渲染:执行组件函数(不包括Hook),并在虚拟DOM树中进行diff运算
    注意,组件函数必须都是没有副作用的纯函数。
  3. 浏览器绘制(DOM渲染):此时DOM才真正更新
  4. useEffect回调触发

组件渲染的逻辑

每次组件渲染会重新执行组件函数,但不包括其中的 Hook 内容

纯函数

组件应当都是纯函数,只负责自己的任务,不改变外部变量,且相同的输入总得到相同的输入。以此防止组件每次渲染时造成副作用。

Hook 内容不会在组件再次渲染时重复执行

父组件传入的 props 如通过useState镜像,则父组件稍后传递不同的props时,该state不会更新:

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);
}

props

  • 父组件可以通过 prop 传递数据、方法给子组件。
  • 子组件通过解构赋值来接收父组件传递进来的数据和方法。
  • 组件定义时本身嵌套的JSX内容会作为该组件的children prop(作用类似Vue的slot):
function Card({children}) {
  return <div>hello {children}</div>
}

export default function Profile() {
  return <Card><b>world</b></Card>
}
//最后会渲染成<div>hello <b>world</b></div>
  • 将本该子组件使用的state提升到父组件,再由父组件将state、setState通过 props 传入子组件供调用,这种操作称为状态提升
function MyApp() {
    function speak(name) {
        console.log("name", name);
    }
    return (
        <>
            <Child name="VV" speak={(name) => speak(name)} />
        </>
    );
}


function Child({ name, speak }) {
    return (
        <>
            <div>我的名字是{name}</div>
            <button onClick={() => speak(name)}>说话</button>
        </>
    );
}
  • 组件也可以将自己从父组件获得的props再传递给自己的子组件
    可以通过扩展运算符全部传递:
export default function Father(props) {
  return (
    <Child
      {...props}
    />
  );
}
function Child({ savedContact, onSave }) {
  ...
}

事件

  • 原生事件名在JSX中需要改为驼峰命名,如onClick。需要在捕获阶段触发的事件在最后额外加上Capture,如onClickCapture
  • 事件处理程序通常命名为 handle 接事件名,例如:onClick={handleClick}
  • 当需要指定函数入参时,可以使用箭头函数:onClick={() => handleClick(100)}
  • 父组件可以把事件通过 props 传递给子组件,通常在 props 中的命名为 on 开头的大驼峰
  • 通过原生的e.stopPropagation()阻止冒泡,e.preventDefault()阻止默认行为。

Hook 与状态管理

以 use 开头的函数被称为 Hook,用于管理状态。

  • Hook 只能在组件或其他 Hook(包括自定义Hook)的顶层调用,且不能出现在条件语句或循环中。
    Hook 不能被包裹在非组件且非 Hook 的普通自定义函数内。
  • 可以将一些组件的公用逻辑抽出来,作为自定义 Hook。自定义 Hook 共享的是状态逻辑,而不是状态本身(即被不同组件引用时,内容不互通)。
useState
const [state, setState] = useState(初始值)
  • state 状态变量,独立于组件保存,组件多次渲染不会重定义该值。
  • setState 函数,用于更新变量并触发再次渲染
    函数入参可以是一个更新函数(必须为纯函数,只用于计算下一个状态。多个更新函数会在渲染期间依次调用),或一个固定值(本质上是更新函数返回固定值的简写)

注意,state在每次渲染中是固定的(快照),setState不会改变当前渲染中的state,只会改变下一次渲染的值!
如想要在当前渲染中就改变,可使用普通变量或useRef
一个事件处理函数会全部执行完再进行渲染,称为批处理

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(10);
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(n=>n + 1);
        setNumber(n=>n + 1);
        console.log(number);//0
      }}>点击后变成3</button>
    </>
  )
}
使用flushSync函数同步更新DOM

传入flushSync执行的setState操作会提前触发DOM渲染(但快照中的state保持不变),而不会等待批处理完成:

const [number, setNumber] = useState(0);
return (
   <>
       <h1 id="h1">{number}</h1>
       <button onClick={() => {
           flushSync(() => {
               setNumber(1);
               console.log(number);//0
               console.log(document.getElementById("h1").innerHTML);//0
           });
           console.log(number);//0
           console.log(document.getElementById("h1").innerHTML);//1
       }}>点我</button>
   </>
);
state 不保存在 JSX 标签里

state 与树中放置该 JSX 的位置相关联。因此在渲染树中相同位置的相同组件,其内部状态会得到保留:

//以下<Counter/>组件内部的state,在 isFancy 切换时会得到保留
{isFancy ? (
  <Counter isFancy={true} /> 
) : (
  <Counter isFancy={false} /> 
)}

//以下<Counter/>组件内部的state,在 isPlayerA 切换时会重置(因位置不同)
{isPlayerA &&
  <Counter person="Taylor" />
}
{!isPlayerA &&
  <Counter person="Sarah" />
}

重置其内部状态的几个方式:

  • 渲染树中不同位置
    相同渲染树需要:上级组件相同,且前置兄弟组件数量相同(<></>是一个组件,DOM元素也视为一种组件,一段JSX{}也算一种组件)
  • 不同组件
  • 销毁该组件后不立刻重新渲染该组件
  • 使用不同 key
突变 mutation

通过 setState 以外的方式造成的 state 数据变化称为突变(例如通过原生JS直接修改对象、数组的某个成员)。突变的结果会在下次渲染中显示,但突变本身不会触发渲染。
应当避免突变的发生


useReducer

当需要在多处分别对同一state做出不同修改时,可以通过useReducer代替useState进行统一管理。

  • useReducer可传入两个入参,第一个是reducer函数,第二个是state初始值。
  • reducer函数必须为纯函数,只用于计算下一个状态。(严格模式下会调用两次)。该函数的返回值用于更新状态。
  • 返回的dispatch函数用来 “派发” 用户操作给 reducer 函数,调用时传入一个自定义参数(通常约定会有 type 属性),传给 reducer 函数的第二个参数:
import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 1:
      return{
        ...tasks,
        age: action.age
      }
    default: {
      throw Error('未知 action: ' + action.type);
    }
  }
}

useRef(脱围)

类似useState,返回一个ref实例,用于在组件渲染间保留信息。
不要在渲染期间(即组件主体)读取或写入ref,应该移到事件处理程序或者 Effect 中。

  • ref实例是一个具有current属性的对象,其更新不会触发组件渲染,也不是个快照,可以直接对current属性赋值以修改信息。
  • ref实例通常用于储存 timeout ID、DOM元素、或其他不需要在JSX中渲染计算的内容:
import { useRef } from 'react';
const ref = useRef(0);
ref.current = ref.current + 1;//1
  • JSX中DOM节点的ref属性,可直接赋值为ref实例以绑定DOM元素,也可以传入一个方法,在方法内手动绑定DOM元素。
    当传入方法时,每当组件重新渲染,先前的函数将被调用并传递 null 作为参数,并且下一个函数将被调用并传递对应 DOM 节点作为参数:
import { useRef } from 'react';
export default function Form() {
    const inputRef = useRef(null);
    const divRefs = useRef(new Map());
       return (
        <>
            <input ref={inputRef} />
            <button onClick={() => inputRef.current.focus()}>
                聚焦输入框
            </button>

            {
                [0, 1, 2, 3, 4, 5].map(i => (
                    <div key={i} ref={node => {
                        if(node){
                            divRefs.current.set(i, node)
                        }else{
                            divRefs.current.delete(i)
                        }
                    }}
                        onClick={() => {
                            divRefs.current.get(i).style.color = "red";
                        }}>
                        我是第{i}个,点我会变红
                    </div>
                ))
            }
        </>
    );
}

上述方法默认只能用于浏览器原生元素,当用于JSX中的自定义组件时,该组件需要由forwardRef方法创建(在此方法中还可以通过useImperativeHandle限制暴露的内容):

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

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

  return (
    <>
      <MyInput ref={inputRef} />
    </>
  );
}


useEffect(脱围,由渲染引起的副作用,与React之外的系统同步)
  • 处理 Effect 的函数(必传)
    • 该函数在严格模式+开发环境下会调用两次。
    • 该函数可以返回一个清理函数,会在组件卸载,或Effect再次执行前调用。
      注意清理函数在Effect函数内是个闭包,因此调用时起内容只影响其声明时的Effect内变量:
//此处返回值内的ignore可以让同一次调用中的fetch回调失效,但不影响下一次的Effect
//可用于防止竞态条件,保证是最后一次触发的生效
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);
  • 依赖数组(非必传)
    • 若不传,则会在组件每次渲染并调用触发DOM渲染后调用函数。
    • 若传入依赖数组(可传入stateprops组件体内声明的利用前两者计算得出的其他响应式变量),且依赖内容均和上次渲染时相同,则跳过本次Effect函数执行。
      传入依赖数组后,函数只能使用依赖数组内包含的响应式变量。
      • 类似location.pathname这样的外部可变值不是响应式变量,应改用 useSyncExternalStore 来读取和订阅。
      • 当前组件的ref不是响应式变量,因为其是有意可变的、稳定的。其变化不会触发重新渲染
      • useState创建的setState方法不是响应式变量
import { useEffect } from 'react';
useEffect(() => {
  // 这里的代码会在每次渲染后执行
});

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

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

注意:组件内声明的对象、数组、函数等(包括父组件传递下来的props中的以上内容),因为每次渲染都不全等,如果传递给useEffect作为依赖则会导致每次渲染都触发useEffect。此时需要使用useMemouseCallback,或将该声明移动到组件外或Effect内。
而当某个响应式变量既需要参与useEffect内容,又不想因其变化而导致触发Effect,则可以使用useEffectEvent

useMemo 和 useCallback

useMemo 返回值,useCallback 返回函数。两者均用于避免组件每次渲染时的重复计算。

  • 需传入依赖数组,若依赖内容均和上次渲染时相同,则返回和上一次全等的结果。
  • 会在JSX渲染环节(即DOM更新前)生效,因此都必须是纯函数。
import { useMemo, useState, useCallback } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ 除非 todos 或 filter 发生变化,否则不会重新执行 getFilteredTodos()
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}
useEffectEvent

用于提取Effect中部分逻辑,以实现如下功能:存在某个响应式变量,既要在Effect中使用,又不想充当Effect的依赖项。

相比于useCallback的区别:

  1. 不用显式声明依赖
  2. 即使依赖变了,fn的引用也不变(与原来全等)
  3. 只能在useEffect内部使用,且不需要加入useEffect依赖
//在这里,onVisit 内的 url 对应 最新的 url(可能已经变化了),但是 visitedUrl 对应的是最开始引起这个 Effect(并且是本次 onVisit 调用)运行的 url 。
import { experimental_useEffectEvent as useEffectEvent } from 'react';

const onVisit = useEffectEvent(visitedUrl => {
  logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
  setTimeout(() => {
    onVisit(url);
  }, 5000); // 延迟记录访问
}, [url]);

使用 Immer 库简化 useState 和 useReducer

Immer 库可代替useStateuseReducer,以避免突变,并简化对象、数组的更新操作。

npm install use-immer
useImmer

提供的修改方法,其参数同样可以是一个更新函数或一个固定值。

  • 为固定值时效果同useState
  • 为更新函数时(通常以update开头命名),其参数(通常命名为draft)是一个Proxy而不是原始的state(类似 Vue3 实现了数据劫持),因此可以直接修改其中属性:
import { useImmer } from 'use-immer';

const [person, updatePerson] = useImmer({
  name: 'Niki de Saint Phalle',
});

updatePerson(draft => {
  draft.name = e.target.value;
});
useImmerReducer

可以返回,也可以直接修改 draft 对象属性。
注意switch中如果不使用return,要使用break分隔case

import { useImmerReducer } from 'use-immer';

const [tasks, dispatch] = useImmerReducer(tasksReducer, [{id:1},{id:2},{id:3}]);
function tasksReducer(draft, action) {
  switch (action.type) {
    case 'changed': {
      const index = draft.findIndex((t) => t.id === action.task.id);
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('未知 action:' + action.type);
    }
  }
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

使用 Context 深层传递参数

用于祖先元素向其深层后代传递信息,以代替 props 逐级下传

// 单独的文件 MyContext.js
import { createContext } from 'react';
export const MyContext = createContext("默认值")

// 父组件提供Context,Context.Provider的后代元素都可以获得距离最近的值
import { MyContext } from './Context.js';
export default function App() {
  return (
    <>
      <MyContext.Provider value={100}>
      <List/>
      </MyContext.Provider>
    </>
  )
}

// 子组件获取Context
import { useContext } from 'react';
import { MyContext } from './MyContext.js';
function List() {
  const data = useContext(MyContext)
  return <div>{data}</div>
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容