0 引入
React组件有两种形式:类组件、函数组件。在React 16.8 版本推出之前,类组件是主力军,而函数组件往往只用来向 UI 渲染简单组件。
从版本16.8.0开始,React引入了hooks。使用React hooks,我们可以在函数组件中使用state和生命周期方法。hooks是添加了state和生命周期的函数组件,所以有了hooks,函数组件和类组件没有什么区别。
但是,现在人们在写 React 组件时,使用React hooks的函数组件更常用,因为它们使代码更短,更容易理解。
1 使用React Hooks的规则
在使用 React Hooks 时,有几个规则需要遵守(你可以在学完了重要的hooks之后再回来理解这些规则):
- 只在组件的顶层调用 Hooks:你不应该在循环、条件或嵌套函数中使用 Hooks。相反,总是在你的 React 函数的顶层使用 Hooks,在任何
return关键字之前。 - 只从React函数中调用Hooks:不要从普通的 JavaScript 函数中调用 Hooks。你可以:
✅ 从 React 函数组件中调用 Hooks
✅ 从自定义 Hooks 中调用 Hooks
2 常见React Hooks
到目前为止,React 有 10 个内置 Hooks。本文将介绍如下几个重要的hook:
- useState
- useEffect
- useRef
- useContext
- useReducer
3 useState
useState Hook 允许你在函数组件内创建、更新和操作state。state允许我们管理应用程序中不断变化的数据。它被定义为一个对象,我们在其中定义键-值对,指定我们希望在应用程序中追踪的各种数据。
useState 接受一个参数,该参数作为state的初始值。你可以提供任何值作为初始值,如number, string, boolean, object, array, null等。
useState 返回一个数组,它的第一个值是当前state的值。第二个值是我们将用于更新state的函数。
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); return (
<div>
Current Cart Count: {count}
<div>
<button onClick={() => setCount(count - 1)}>Add to cart</button>
<button onClick={() => setCount(count + 1)}>Remove from cart</button>
</div>
</div>
);
}
在上面的代码中,我们做了一些事:
- 首先从react包中使用对象解构引入useState
- 由于useState返回一个数组,我们可以用数组结构获取返回值,上述代码中count的初始值为0,setCount是一个函数
- button标签中,我们定义了一个事件,当button被点击时,会触发事件,即调用setCount(count - 1),React中规定,任何时候调用setCount函数,都会触发页面的重新渲染。
注意:useState的初始值只会在组件第一次渲染时生效。也就是说,之后触发的每次渲染,useState获取到的都是最新的而不是最初始的state值。
4 useEffect
useEffect Hook允许在函数组件中执行副作用(side effect)。
副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用。对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM、增加监听器等等)。
使用useEffect,必须提供一个函数作为参数。在组件第一次被渲染的时候,以及在随后的每次重新渲染/更新(如useState触发更新)时,React都会调用这个函数。React 首先更新 DOM,然后调用任何传递给 useEffect() 的函数。
const { useEffect, useState } = React
const CounterWithNameAndSideEffect = () => {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`You clicked ${count} times`)
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
在上面的代码中,我们做了一些事:
- 同样的,我们首先在react包中引入useEffect
- useEffect接收两个参数,其中第一个参数是必需的,并且必须是一个函数。当不使用第二个参数时,React将在第一次及后续每次渲染时调用这个函数。
4.1 useEffect的第二个参数
因为在随后的每次重新渲染 / 更新时,传递给 useEffect() 的函数都会被执行,所以出于性能上的考虑,我们可以告诉 React 在某些时候不要执行这个函数。为了实现这个目的,我们可以为 useEffect() 传入第二个参数,这个参数是一个数组,它的成员是需要监视的 state 变量。只有在这些 state 发生变化的时候,React 才会执行这个函数。
useEffect(() => {
console.log(`Hi ${name} you clicked ${count} times`)
}, [name, count])
类似的,你可以传入一个空数组,这会使 React 只在组件挂载(即首次渲染)时执行这个函数。
useEffect(() => {
console.log(`Component mounted`)
}, [])
4.2 useEffect清理功能
如果想要清理副作用 可以在useEffect的末尾return一个新的函数,在新的函数中编写清理副作用的逻辑。
清理函数可以防止内存泄漏,并删除一些不必要的和不需要的行为。
清理的执行时机为:
- 组件卸载时自动执行
- 组件更新时,下一个useEffect副作用函数执行之前自动执行
const UseEffectCleanup = () => {
const [size, setSize] = useState(window.innerWidth);
const checkSize = () => {
setSize(window.innerWidth);
};
useEffect(() => {
window.addEventListener('resize', checkSize);
return () =>window.removeEventListener('resize' , checkSize);
}, []);return (
<>
<h2>{size} PX</h2>
</>
);
};
4.3 useEffect的应用
useEffect() 非常适合添加日志,访问第三方 API 等。
5 useRef
使用useRef函数,可以在不re-render的状态下更新值,它主要被用来获取DOM进而控制DOM的行为。
5.1 什么是useRef?
const [renderCounter,setRenderCount] = useState(0)
//renderCounter:0
const renderCounter = useRef(0)
//renderCounter: {current:0}
useState会返回一个包含值的数组,第一个值是state,第二个值是更新state的函数。每次更新、renderCount改变,就会触发re-render。
而useRef传递一个值并返回一个对象,这个值更新,不会触发re-render。
5.2 使用useRef获取DOM
原生JS中,可以使用getElementById、querySelector等方法获取DOM,通过在React中使用useRef,我们仍然能像过去一样,类似 ID 的方式获取 DOM 元素。
const UseRefBasics = () => {
const refContainer = useRef(null); const handleSubmit= (e)=>{
e.preventDefault();
// console.log(refContainer);
console.log(refContainer.current)
}
return(
<>
<form className='form' onSubmit={handleSubmit}>
<div>
<input type='text' ref={refContainer} />
</div>
<button type='submit'>submit</button>
</form>
</>
);
};
在这段代码中,当点击提交时,触发handleSubmit函数,输出refContainer.current的值,为下面的DOM元素。
<input type='text' />
这样我们就通过 refContainer.current 获取到了当前的 DOM 元素了。
6 useContext
useContext Hook 与 React Context API 一起工作。它提供了一种方法,使整个应用程序中的组件通信变得简单,无论它们的嵌套有多深。
React 有一个单向的数据流,数据只能从父代传递到子代。要把数据(如 state)从父组件传给子组件,你需要根据子组件的嵌套深度,把它作为一个 prop,通过不同的级别向下传递。手动地在组件树上传递它们是很复杂的。
使用Context API包裹需要传值的组件,这个值会一直存在直到组件树的最底部。只需要三步:
step1. 使用React提供的createContext方法创建context,此方法返回两个组件Provider和Consumer。
const PersonContext = React.createContext(); //第一步,创建上下文,这会返回两个组件Provider和Consumer
step2. 用Provider组件包裹需要传值的组件(往往是根组件)
const ContextAPI = () => {
const [people, setPeople] = useState(data);
const removePerson = (id) => {
setPeople((people) => {
return people.filter((person) => person.id !== id);
});
};
return (
// 第二步,Provider包裹根组件,这里的value会all the way down
<PersonContext.Provider value={{ people, removePerson }}>
<h3>Context API/useContext</h3>
<List />
</PersonContext.Provider>
);
};
step3. 在需要值的组件内部使用useContext钩子接收值(常用对象解构接收)
const List = () => {
const { people } = useContext(PersonContext);
return (
<>
{people.map((person) => {
return <SinglePerson key={person.id} {...person} />;
})}
</>
);
};
const SinglePerson = ({ id, name }) => {
//第三步,在需要数据的组件处使用useContext
const { removePerson } = useContext(PersonContext);
return (
<div className="item">
<h4>{name}</h4>
<button onClick={() => removePerson(id)}>remove</button>
</div>
);
};
7 useReducer
useReducer Hooks 是 useState Hooks 的一个替代品。可以实现复杂逻辑修改,而不是像useState那样只是直接赋值修改。对于复杂的state操作逻辑,嵌套的state的对象,官方推荐使用useReducer。
准确来说,useReducer是useState的原始版,因为在React源码中,useState就是由useReducer实现的。
7.1 useReducer基本用法
const [state, dispatch] = useReducer(reducer, initialState);
- reducer:简单来说 reducer是一个函数
(state, action) => newState:接收当前应用的state和触发的动作action,计算并返回最新的state - initialState:自定义状态初始值。
- state:自定义状态值的引用
- dispatch:用来触发reducer函数。
补充说明:
- 可以将reducer理解为一个事件处理函数,它定义了一套处理机制,dispatch根据这套机制进行调度。
reducer本质是一个纯函数,没有任何UI和副作用。这意味着相同的输入(state、action),reducer函数无论执行多少遍始终会返回相同的输出(newState)
2. initialValue是我们自定义变量的默认值,该值可以是简单类型(number、string),也可以是复杂类型(object、array)。
即使该值是简单类型,也建议单独定义出来而不是直接将值写在useReducer函数中,因为单独定义可以让我们更加清晰读懂数据结构。
- dispatch function 通常以下列格式派发一个对象:
dispatch({ type: "ACTION_TYPE", payload: optionalArguments });
其中 type 是动作的描述,payload 是你要传递给 reducer 的参数。
7.3 代码说明
我们通常会在组件外部定义reducer和initialValue,作为useReducer的两个参数传入。复杂的reducer也可以在文件外部定义,并在组件文件中导入。
注意:
- reducer处理的state对象必须是
immutable,这意味着永远不要直接修改参数中的state对象,reducer函数应该每次都返回一个新的state object - 既然reducer要求每次都返回一个新的对象,我们可以使用ES6中的解构赋值方式去创建一个新对象,并复写我们需要改变的state属性
reducer是一个利用action提供的信息,将state从oldState转换到newState的一个纯函数。
import React, { useState, useReducer } from "react";
import Modal from "./Modal";
import { data } from "../../../data";
// 在外部单独定义初始状态
const defaultState = {
people: [],
isModalOpen: false,
modalContent: "",
};
// 定义reducer事件处理函数,它规定了一套机制,当dispatch触发不同操作时,会根据reducer的规定return不同值
const reducer = (state, action) => {
if (action.type === "ADD_ITEM") {
const newPeople = [...state.people, action.payload];
return {
...state,
people: newPeople,
isModalOpen: true,
modalContent: "item added",
};
} else if (action.type === "NO_VALUE") {
return {
...state,
isModalOpen: true,
modalContent: "please enter value",
};
}
};
在组件中使用useReducer钩子,它将返回两个变量,state是initialValue的引用,也就是我们定义的状态合集。dispatch相当于一个调度函数,在其中传入你想执行的操作,dispatch将会根据reducer中该名称返回对应newState。
// Index组件
const Index = () => {
const [name, setName] = useState("");
const [state, dispatch] = useReducer(reducer, defaultState);
const handleSubmit = (e) => {
e.preventDefault();
if (name) {
const newItem = { id: new Date().getTime().toString(), name };
dispatch({ type: "ADD_ITEM", payload: newItem });
setName("");
} else {
dispatch({ type: "NO_VALUE" });
}
};
return (
<>
{state.isModalOpen ? <Modal modalContent={state.modalContent} /> : ""}
<form className="form" onSubmit={handleSubmit}>
<div>
<input
type="text"
value={name}
onChange={(e) => {
setName(e.target.value);
}}
></input>
</div>
<button type="submit">submit</button>
</form>
{state.people.map((person) => {
return (
<div key={person.id}>
<h4>{person.name}</h4>
</div>
);
})}
</>
);
};
export default Index;
7.3 useReducer的应用
在React 16.8版本以前,通常需要使用第三方Redux来管理React的公共数据,但自从 React Hook 概念出现以后,可以使用 useContext + useReducer 轻松实现 Redux 相似功能。