useState
useState 用于存储和修改组件的状态,它接收一个参数作为状态的初始值,返回最新的状态值和修改状态值的方法。当状态发生变化时,会触发组件的更新。下面来看下具体的使用:
js
import { useState } from "react";
export default function StateHook() {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
function minus() {
setCount(count - 1);
}
return (
<div>
<p>count最新值为:{count}</p>
<button onClick={add}>加一</button>
<button onClick={minus}>减一</button>
</div>
);
}
在App.js 中引用:
js
import "./App.css";
import StateHook from "./components/StateHook";
function App() {
return (
<div className="App">
<StateHook />
</div>
);
}
export default App;
演示效果:

可以看到count 变化时, 页面的显示也随之更新。
注意点: 当状态值引用类型时,只修改引用里面的某个属性,而没有修改引用值,不会触发更新,比如将上述例子中的改造下,将count 放到一个对象的属性上。修改时只改count 属性,然后将原引用赋值传给set方法,你会发现组件没有触发重新渲染。
js
import { useState } from "react";
export default function StateHook() {
const [data, setData] = useState({ count: 0 });
function add() {
data.count = data.count + 1;
setData(data);
console.log("加法", data);
}
function minus() {
data.count = data.count - 1;
setData(data);
console.log("减法", data);
}
return (
<div>
<p>count最新值为:{data.count}</p>
<button onClick={add}>加一</button>
<button onClick={minus}>减一</button>
</div>
);
}

可以看到,虽然在add 方法中和minus 打印的值变化了,但是页面上显示的并没有变化,这怎么解决呢?其实很简单,调用set方法时给新的引用值即可。将上述代码进行改造:
js
import { useState } from "react";
export default function StateHook() {
const [data, setData] = useState({ count: 0 });
function add() {
setData({
count: data.count + 1,
});
console.log("加法", data);
}
function minus() {
setData({
count: data.count - 1,
});
console.log("减法", data);
}
return (
<div>
<p>count最新值为:{data.count}</p>
<button onClick={add}>加一</button>
<button onClick={minus}>减一</button>
</div>
);
}

可以看到,对象的也能正常更新了,但是可能有的同学会想,如果这个对象有很多属性,我只想改其中一个改怎么办。其实很简单,使用es6的扩展语法即可,比如我们现在给data 增加一个title 属性,修改的时候还是只修改count。
js
import { useState } from "react";
export default function StateHook() {
const [data, setData] = useState({ count: 0, title: "这是一个标题" });
function add() {
setData({
...data,
count: data.count + 1,
});
console.log("加法", data);
}
function minus() {
setData({
...data,
count: data.count - 1,
});
console.log("减法", data);
}
return (
<div>
<h2>{data.title}</h2>
<p>count最新值为:{data.count}</p>
<button onClick={add}>加一</button>
<button onClick={minus}>减一</button>
</div>
);
}
上述代码开始可以正常运行。

useReducer
useReducer 是一种集中式的状态管理,它和redux 一样,是flux思想的一种实现,状态的标准化管理。当一个状态有多种修改方式时,使用useReducer 来管理会更加方便。在上面的useState 例子中,如果除了加减法之外,还有更多的修改状态方法,比如乘法,除法,那么用useState来管理看上去比较乱,因为分散在各个方法中。当然你也可以将它封装成一个方法然后点击传入不同的参数来区别执行哪个逻辑。但是这样的话,不同开发者会写出不同的写法,没有一个标准。使用useReducer 就可以使这种方式变得标准化。下面来看下useReducer使用:
js
import { useReducer } from "react";
export default function StateHook() {
const [data, dispatch] = useReducer(reducer, {
count: 0,
title: "Reducer 状态管理",
});
function reducer(params, actions) {
console.log(params, actions);
switch (actions.type) {
case "add":
return {
...params,
count: params.count + 1,
};
case "minus":
return {
...params,
count: params.count - 1,
};
case "mult":
return {
...params,
count: params.count * 2,
};
case "div":
return {
...params,
count: params.count / 2,
};
default:
return params;
}
}
return (
<div>
<h2>{data.title}</h2>
<p>count最新值为:{data.count}</p>
<button onClick={() => dispatch({ type: "add" })}>加一</button>
<button onClick={() => dispatch({ type: "minus" })}>减一</button>
<button onClick={() => dispatch({ type: "mult" })}>乘以2</button>
<button onClick={() => dispatch({ type: "div" })}>除以2</button>
</div>
);
}

可以看到useRducer 管理的状态,和useState 管理的一样,当状态更新时,会触发组件的更新。和useState不同的是,useRducer 把状态管理集中化了,标准化了,比较适合在状态有多种形式修改时的情况。useRducer 和useState 一样如果状态是一个对象,修改时需要赋值新的引用,否则不会触发组件的重新渲染。
useContext
useContext 用于跨组件层级的状态管理。比如前面的例子,如果button是在Grandson 组件中,Grandson 在Child组件中,而StateHook 引用的是Child组件,这个时候要将StateHook 中的方法传入到Grandson组件中就会非常麻烦,需要经过Child组件,Child组件根本不需要这些操作方法,就出现了多余的props,非常不友好,当我们需要修改这些方法时,Child也不得不修改,增加了维护成本。这个时候该怎么办呢,这个时候就该useContext 出场了。来看下具体的使用方式:
js
import { useReducer, createContext, useContext } from "react";
const DataContext = createContext({});
export default function StateHook() {
const [data, dispatch] = useReducer(reducer, {
count: 0,
title: "Reducer Context 状态管理",
});
function reducer(params, actions) {
console.log(params, actions);
switch (actions.type) {
case "add":
return {
...params,
count: params.count + 1,
};
case "minus":
return {
...params,
count: params.count - 1,
};
case "mult":
return {
...params,
count: params.count * 2,
};
case "div":
return {
...params,
count: params.count / 2,
};
default:
return params;
}
}
return (
<div>
<h2>{data.title}</h2>
<p>count最新值为:{data.count}</p>
<DataContext.Provider value={{ data, dispatch }}>
<Child />
</DataContext.Provider>
</div>
);
}
function Child() {
return (
<div>
<h3>Child</h3>
<Grandson />
</div>
);
}
function Grandson() {
const { data, dispatch } = useContext(DataContext);
return (
<div>
<button onClick={() => dispatch({ type: "add" })}>加一</button>
<button onClick={() => dispatch({ type: "minus" })}>减一</button>
<button onClick={() => dispatch({ type: "mult" })}>乘以2</button>
<button onClick={() => dispatch({ type: "div" })}>除以2</button>
</div>
);
}
演示效果:

可以看到这样我们就轻松实现了跨层级组件的状态管理。需要注意的是useContext需要配合CreateContext一起使用。
useEffect
useEffect 主要用来执行一些副作用,比如请求数据,操作dom,和Vue中的watch 功能相似。useEffect 接收一个函数作为第一个参数,第二个参数是依赖项,当然第二个参数可以不写。
先来看下只有一个参数的情况:
js
import { useEffect, useState } from "react";
export default function EffectComponent() {
const [page, setPage] = useState(1);
const [list, setList] = useState([]);
const prevPage = () => {
if (page <= 1) return;
setPage(page - 1);
};
const nextPage = () => {
setPage(page + 1);
};
useEffect(() => {
(async function () {
const result = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`
);
const data = await result.json();
setList(data);
console.log(data, "data");
})();
});
return (
<div>
<div>
<button onClick={prevPage}>上一页</button>
<button onClick={nextPage}>下一页</button>
</div>
<ul>
{list.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
这段代码看上去好像正常,但是当你打开控制台的时候就发觉不正常了,你会发现一直在发送网络请求,如下图

这是什么原因造成的呢,因为没有第二个参数时,useEffect 在所有状态变更时都会触发组件的重新执行,在这个例子里面,useEffect 理请求完数据后,进行了setList(data) 导致状态的更新,状态的更新造成了组件的重新执行,而组件重新执行,又会重新执行useEffect, useEffect执行又会调用setList(data),这样就形成了无限循环,也就造成了无限请求。那么怎么处理这个问题呢?可以利用第二个参数来处理,第二个参数是一个数组,每一项代表一个依赖,有了依赖项之后,只有依赖项变化时才会再次触发useEffect。来看下如何利用第二个参数解决上面这个问题。
js
import { useEffect, useState } from "react";
export default function EffectComponent() {
const [page, setPage] = useState(1);
const [list, setList] = useState([]);
const prevPage = () => {
if (page <= 1) return;
setPage(page - 1);
};
const nextPage = () => {
setPage(page + 1);
};
useEffect(() => {
(async function () {
const result = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`
);
const data = await result.json();
setList(data);
console.log(data, "data");
})();
}, [page]);
return (
<div>
<div>
<button onClick={prevPage}>上一页</button>
<button onClick={nextPage}>下一页</button>
</div>
<ul>
{list.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
看下运行效果:

可以看到现在,加了依赖项之后,就没有出现无限循环请求了,因为我们填的依赖项并不包括list的变化。但是你还是在控制台看见了两次请求,这个不用管,是React 在开发环境为了方便调试故意这样做的。
我们再来点击一下上一页和下一页看是否正常:
可以看到上一页和下一页功能能正常使用。说明useEffect确实监听到了limit的变化,重新执行了,并且重新渲染。
现在第二个参数我们填了一个依赖,要是我们只填一个空数组会是什么情况呢?来看下:
js
import { useEffect, useState } from "react";
export default function EffectComponent() {
const [page, setPage] = useState(1);
const [list, setList] = useState([]);
const prevPage = () => {
if (page <= 1) return;
setPage(page - 1);
};
const nextPage = () => {
setPage(page + 1);
};
useEffect(() => {
(async function () {
const result = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`
);
const data = await result.json();
setList(data);
console.log(data, "data");
})();
}, []);
return (
<div>
<div>
<button onClick={prevPage}>上一页</button>
<button onClick={nextPage}>下一页</button>
</div>
<ul>
{list.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
演示效果:

可以看到初始化的时候正常,那么点击上一页和下一页是否能正常呢?

可以看到点击上一页下一页没有反应了,为什么呢?因为useEffect 第二个参数是一个空数组时,它只会在组件初始化时执行一次,后面的更新不再执行。
总结下useEffect的三个特点:
- 只有一个参数时,在组件初始化和所有的状态发生更新时都会触useEffect 的重新执行,所以需要注意无限循环的产生。
- 第二个参数不为空数组时,useEffect 会在组件初始化和状态发生变化时执行
- 第二个参数为空数组时,只在初始化时执行。
useRef
useRef 是React 提供的一个获取dom的工具,在React 中很少直接操作dom,但是有些时候我们不得不操作dom,比如输入框获取焦点,音视频的播放暂停,音量调整,等这些可能直接使用操作dom的方式会更方便,于是React 提供了一个我们可以操作dom的hook useRef,下面来看下useRef的使用。
js
import { useEffect, useRef } from "react";
export default function RefHook() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
});
return <input ref={inputRef} />;
}

可以看到输入框初始化时就自动获取到了焦点。useRef 需要在标签上添加ref属性来配合一起使用,那么如何直接将ref 放到一个组件上会怎么样呢?来看下面的代码:
js
import { useEffect, useRef } from "react";
export default function RefHook() {
const inputRef = useRef();
useEffect(() => {
console.log(inputRef);
inputRef.current.focus();
});
return <Input />;
}
function Input() {
return (
<label>
用户名: <input placeholder="请输入用户名" />
</label>
);
}

可以看到直接报错了,并且打印出来的useRef返回出来的 current属性为undefined,说明当ref 为组件上的属性时,useRef获取不到dom。那么如果我们想要获取的dom就在子组件中,该怎么办呢?这需要用到forwardRef 进行转发。来看下forwardRef 的使用:
js
import { forwardRef, useEffect, useRef } from "react";
export default function RefHook() {
const inputRef = useRef();
useEffect(() => {
console.log(inputRef);
inputRef.current.focus();
});
return <Input ref={inputRef} />;
}
const Input = forwardRef((props, ref) => {
return (
<label>
用户名: <input placeholder="请输入用户名" ref={ref} />
</label>
);
});
运行效果:

可以看到运行正常了。
useMemo
useMemo 用于缓存一个复杂的状态,和Vue中的计算属性类似。现在我们来看一个不使用useMemo 的一个例子会有什么问题,然后在看下使用useMemo带来的好处。
js
import { useState } from "react";
export default function ComponentNoUseMemo() {
const [count, setCount] = useState(0);
const [title] = useState("这是一个标题");
const [desc] = useState("这是一段描述");
const add = () => {
setCount(count + 1);
};
const childData = {
title,
desc,
};
return (
<div style={{ padding: "20px" }}>
<p>
{count} <button onClick={add}>count + 1</button>
</p>
<div>
<Child childData={childData} />
</div>
</div>
);
}
function Child(props) {
console.log("Child 执行了");
return (
<div>
<h1>{props.childData.title}</h1>
<p>{props.childData.desc}</p>
</div>
);
}
运行效果:
在这个例子中,可以看到我们点击count + 1的时候Child也重新执行了,这其实非常不合理,因为count 和Child 组件根本没有任何关系。这就造成了性能的浪费,这该怎么处理呢?这个时候就可以使用useMemo 来解决这个问题。来看下具体使用方式:
js
import { useState, useMemo } from "react";
export default function ComponentUseMemo() {
const [count, setCount] = useState(0);
const [title] = useState("这是一个标题");
const [desc] = useState("这是一段描述");
const add = () => {
setCount(count + 1);
};
const chilcData = useMemo(() => {
return {
title,
desc,
};
}, [title, desc]);
return (
<div style={{ padding: "20px" }}>
<p>
{count} <button onClick={add}>count + 1</button>
</p>
<div>
<Child childData={chilcData} />
</div>
</div>
);
}
function Child(props) {
console.log("Child执行了");
return (
<div>
<h1>{props.childData.title}</h1>
<p>{props.childData.desc}</p>
</div>
);
}
运行效果:

可以看到我们使用了useMemo ,Child还是执行了,这是为什么呢?因为count 的变化导致了ComponentUseMemo的重新执行,ComponentUseMemo的重新导致了Child的重新初始化,所以Child 还是重新执行了,那么到底该怎么办呢,有没有什么办法可以将Child 这个组件缓存起来呢?是有的,React提供了一个方法,memo,我们只需要用memo将Child 包裹一下即可,来看下具体使用方式:
js
import { useState, useMemo, memo } from "react";
export default function ComponentUseMemo() {
const [count, setCount] = useState(0);
const [title] = useState("这是一个标题");
const [desc] = useState("这是一段描述");
const add = () => {
setCount(count + 1);
};
const chilcData = useMemo(() => {
return {
title,
desc,
};
}, [title, desc]);
return (
<div style={{ padding: "20px" }}>
<p>
{count} <button onClick={add}>count + 1</button>
</p>
<div>
<Child childData={chilcData} />
</div>
</div>
);
}
const Child = memo((props) => {
console.log("Child执行了");
return (
<div>
<h1>{props.childData.title}</h1>
<p>{props.childData.desc}</p>
</div>
);
});
运行效果:

这个时候我们就可以看到Child 不再执行了,达到了我们想要的效果。
useCallback
useCallback 是用来缓存函数的,什么意思呢!其实不太好解释,直接来看代码,先不使用useCallback
js
import { useState, useMemo, memo } from "react";
export default function NoCallbackComponent() {
const [count, setCount] = useState(0);
const [title, setTitle] = useState("这是一个标题");
const [desc] = useState("这是一段描述");
const add = () => {
setCount(count + 1);
};
function log() {
console.log("log");
}
const chilcData = useMemo(() => {
return {
title,
desc,
};
}, [title, desc]);
return (
<div style={{ padding: "20px" }}>
<p>
{count} <button onClick={add}>count + 1</button>
</p>
<div>
<Child childData={chilcData} log={log} />
</div>
</div>
);
}
const Child = memo((props) => {
console.log("Child执行了");
return (
<div>
<h1>{props.childData.title}</h1>
<p>{props.childData.desc}</p>
<div>
<button onClick={props.log}>log</button>
</div>
</div>
);
});
运行效果:

可以看到,Child又开始执行了,为什么呢?因为我们想Child 传递了一个函数log, 点击count+1导致了NoCallbackComponent 的重新执行,NoCallbackComponent 重新执行导致log 函数被重新初始化,所以就导致了Child 又开始重新执行。那该怎么办呢?这个时候就要使用useCallback了。来看拿下具体的使用:
js
import { useState, useMemo, memo, useCallback } from "react";
export default function NoCallbackComponent() {
const [count, setCount] = useState(0);
const [title, setTitle] = useState("这是一个标题");
const [desc] = useState("这是一段描述");
const add = () => {
setCount(count + 1);
};
const log = useCallback(() => {
console.log("log");
}, []);
const chilcData = useMemo(() => {
return {
title,
desc,
};
}, [title, desc]);
return (
<div style={{ padding: "20px" }}>
<p>
{count} <button onClick={add}>count + 1</button>
</p>
<div>
<Child childData={chilcData} log={log} />
</div>
</div>
);
}
const Child = memo((props) => {
console.log("Child执行了");
return (
<div>
<h1>{props.childData.title}</h1>
<p>{props.childData.desc}</p>
<div>
<button onClick={props.log}>log</button>
</div>
</div>
);
});
运行效果:

可以看到,Child不会执行了,达到了我们想要的效果。
总结
本篇分享了useState,useReducer,useContext,useEffect,useRef,useMemo,useCallback 7个React中最常用的hooks,用代码示例演示了不使用这些函数会怎么样,用这些函数会带来什么好处。看到这里说明你真的很优秀。赶快用起来吧。
最后本篇已收录到React 知识储备 专栏,欢迎关注后续更新