2020-03-02
原文: The Guide to Learning React Hooks
作者: Eric Bishard
第一章:关于React Hooks
它发布于2018年10月份的16.7.0-alpha.0测试版本中,当时Facebook已经在生产中使用了一个月,确保了社区不会面临重大的漏洞和问题。由于对于破坏向后兼容的大型重构往往会出现问题,所以React采用了渐进迁移策略( gradual migration and adoption strategy)允许新的API和模式与旧的API和模式共存。
Hooks是对核心库的添加。这意味着它是可选以及向后兼容的。他们在GITHUB之前发表了对评论过程的请求。如果您想使用它们,只需安装最新版本的React。
这种Hooks模式提供了一种对于类组件的替代写法,可以简单的使用状态以及生命周期方法。Hooks使得函数组件也可以使用一些只有类组件才能使用的东西,比如我们可以通过useState, useEffect 以及 useContext访问 React local state, effects 和 context。
其它的Hooks还有useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect 以及 useDebugValue。
所以怎样使用Hooks
最有效的展示方式就是举一个对比的例子,一种使用类组件的方式写,需要访问状态以及生命周期方法;另一种使用函数组件的方式实现同样的功能。
下面我提供了一个与ReactJS文档中的示例类似的工作示例,但是您可以修改和测试它,在我们学习的每个阶段都可以使用StackBlitz演示来亲自修改测试。所以让我们停止谈论,开始学习React Hooks。
使用Hooks的优点
Hooks对于开发者又很对优点,它改变了我们书写组件的方式。可以帮助我们写出更清晰、更简洁的代码——就像我们进行了代码节食,我们减轻了很多体重,看起来更好,感觉更好。它使我们的下颌线突出,使我们的脚趾感到更轻。就像下面这样。
image
好了,不开玩笑了。Hooks确实减少了代码体积,它减少并使我们的代码更加可读、简洁和清晰。为了证明这一点,我们来看一段类组件的版本的代码,它和用Hooks重写过的有什么不同。
可以看出,代码量少了多少。使用Hooks不仅减少了差不多5行代码,而且提升了可读性以及可测性。将现有的代码改成Hooks的方式确实可以减少代码量提高可读性,但我还是需要提醒你要慎重。记住Hooks是向后兼容的,并且可以与旧版本共存,所以不需要立即重写整个代码库。
image
Hooks的五条重要规则
在我们创建自己的Hooks之前,让我们回顾一些我们必须始终遵循的主要规则。
不要从循环、条件或嵌套函数内部调用Hooks
只在最顶层使用 Hook
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook
在自定义 Hook 中调用其他 Hook
如果需要,可以使用ES Lint插件在团队中强制执行这些规则。同样在同一页上也有关于为什么需要这些规则的很好的解释。
Hooks for State and Effects
这个GIF中展示的类组件和函数组件之间的不同,我们会在后面详细解释。
image
使用useState展示类和函数计数组件的不同
下面我们先看一下在React文档中展示的计数组件例子。它是一个很简单的组件,包括一个按钮,只要点击按钮,它就将状态向前推进一步,并更新state.count以进行渲染。
首先,我们先看一下类组件,使用setState更新状态。
import React from 'react';
class Counter extends React.Component {
constructor() {
this.state = { count: 0 };
this.incrementCount = this.incrementCount.bind(this);
}
incrementCount() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
);
}
}
export default Counter;
首先要注意的是,我们需要使用类语法,声明一个constructor,在这里面可以引用this关键词。在构造器中有一个state属性,使用setState()方法更新状态。
下面我们看下函数组件使用Hooks怎么来实现。
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
return (
)
}
export default Counter;
在这个函数组件中,我们引进了一个useState属性,并没有其它的类语法或者构造器。它的赋值设置了默认值,不仅提供count属性,还提供了一个修改该状态的函数setCount。这个setCount是一个函数方法,可以随便命名。
组件方法incrementCount更加易读,可以之间引用我们的state值,而不是引用this.state。
useEffect方法的对比
当更新状态的时候,有时候会发生一些副作用。在我们的计数组件中,我们可能需要更新数据库、修改本地存储或者修改document的title。在React JS文档中,后一个示例用于使事情尽可能简单。所以让我们并更新我们的例子,使用新的钩子useffect产生一个副作用。
让我们将这个副作用添加到我们现有的例子中,然后再看一下使用类和使用钩子的方法。首先看下使用类组件的实现。
import React from 'react';
class Counter extends React.Component {
constructor() {
this.state = { count: 0 };
this.incrementCount = this.incrementCount.bind(this);
}
incrementCount() {
this.setState({ count: this.state.count + 1 });
}
componentDidMount() { document.title = `You clicked ${this.state.count} times`; }
componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }
render() {
return (
);
}
}
export default Counter;
然后,使用Hooks实现同样的方法。
import React, { Component, useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useEffect(() => {
document.title = `You clicked ${count} times`
});
return (
)
}
export default Counter;
现在我们引入了额外的行为,我们开始看到更多的证据表明,如何切换到钩子提供了一种更干净的方式来处理状态和副作用。在类组件中使用两个方法才能实现的作用,在函数组件中,使用一个useEffect方法就可以实现。只需去掉类语法和一个额外的方法就可以使我们的代码更具可读性。完全值得。
根据你的需要可以多次调用useState 和 useEffect
就像使用setState,你也可以多次调用useState。让我们换一个示例,它显示了一个稍微复杂的情况,我们在页面上显示了一个名称,一些允许更改名称的输入,我们希望同时控制名字和姓氏。我们需要创建两个属性,每个属性都有各自的更新和设置方法。只需对每个调用useState来设置默认值。
在下面的GIF中,您可以看到它是什么样子的,以及它在基于类的版本中是什么样子的,我们将在下面进一步探讨。
image
正如您所期望的,我们还为每个名称提供了一个更新函数,以便您可以独立处理对它们的更改。
我们看下基于类的组件。
import React, { Component } from 'react';
export default class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {
firstName: 'Harry',
lastName: 'Poppins'
};
this.handleFirstNameChange = this.handleFirstNameChange.bind(this);
this.handleLastNameChange = this.handleLastNameChange.bind(this);
}
handleFirstNameChange = (e) => this.setState({ firstName: e.target.value });
handleLastNameChange = (e) => this.setState({ lastName: e.target.value });
render() {
return (
);
}
}
使用Hooks:
import React, { Component, useState } from 'react';
export default function Greeting() {
const [firstName, setFirstName] = useState("Bat");
const [lastName, setLastName] = useState("Man");;
const handleFirstNameChange = (e) => setFirstName(e.target.value);
const handleLastNameChange = (e) => setLastName(e.target.value);
return (
);
}
我不会再讨论所有的差异,但我希望您看到一个稍微复杂一点的例子。希望您开始看到使用Hooks的好处。
让我们对这个示例再做一个更改,并使用useffect将我们的名称保存到本地存储,这样在刷新页面时不会丢失状态。
看下基于类的组件。
import React, { Component } from 'react';
export default class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {
firstName: window.localStorage.getItem('classFirstName') || '',
lastName: window.localStorage.getItem('classLastName') || ''
};
this.handleFirstNameChange = this.handleFirstNameChange.bind(this);
this.handleLastNameChange = this.handleLastNameChange.bind(this);
}
handleFirstNameChange = (e) => this.setState({ firstName: e.target.value });
handleLastNameChange = (e) => this.setState({ lastName: e.target.value });
componentDidUpdate() {
window.localStorage.setItem('classFirstName', this.state.firstName),
[this.state.firstName];
window.localStorage.setItem('classLastName', this.state.lastName),
[this.state.lastName];
}
render() {
return (
);
}
}
对比一下Hooks:
import React, { Component, useState, useEffect } from 'react';
export default function Greeting() {
const [firstName, setFirstName] = useState(() =>
window.localStorage.getItem('hooksFirstName') || ''
);
const [lastName, setLastName] = useState(() =>
window.localStorage.getItem('hooksLastName') || ''
);
const handleFirstNameChange = (e) => setFirstName(e.target.value);
const handleLastNameChange = (e) => setLastName(e.target.value);
useEffect(() => {
window.localStorage.setItem('hooksFirstName', firstName), [firstName];
window.localStorage.setItem('hooksLastName', lastName), [lastName];
});
return (
);
}
第三节:Hooks For Context
为了更好的理解Hooks的另一个基础钩子useContext,我们需要对Context API有一个深入的认识,它是 React 16.3发布的一个特性。像学习大多数东西一样,有时我们在前进之前必须完全理解另一个概念。如果你熟悉Context API,那么可以跳过这一节。如果你第一次接触Context API,我们会简要介绍一下并通过demo展示。首先要在你的应用程序中添加一个上下文,否则不能呢使用useContext。
使用上下文环境的一个很好的例子是profile组件,我们想一下这个组件都需要有哪些东西。当我登录到xyz.com,有一些数据需要在所有或者部分子组件中使用。我们假定需要两个子组件:和。一个组件用来展示用户信息和图片,另一个组件展示我的团队。我们有React、Angular和Vue团队的成员,因此我们将使用这些框架名称作为团队名称。
回到代码,我们需要通过组件value属性,将需要的数据传入到组件中。这样,我们允许任何组件和她的子组件调用这些数据。
让我们了解如何通过简单地将props传递给children(在pre-Context API 阶段的一个选项)来实现这个组件。在使用“prop透传”这种方法的时候,就需要一层一层向每个子组件传递数据。这就为每个组件创建了物理输入,允许数据(状态)从外部流向每个组件及其子组件。
image
让我们看下pre-context 阶段的例子。
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const profileData = {
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoka',
fullName: 'Kendō No Arikata',
team: 'KendoReact'
}
const App = () => (
)
const Profile = (props) => (
)
const User = (props) => {
return (
)
}
const Team = (props) => {
return (
)
}
render( , document.getElementById('root'));
如果一个应用有10个组件,每个组件都有自己的组件树,这些组件树又有一个组件树。您愿意手动将props传递给可能需要或不需要数据的组件吗?或者您更愿意从组件树中的任何点使用该数据?Context允许在组件之间共享值,而不必显式地在树的每个级别传递一个prop。我们看下 Context API本身如何应用于基于类的组件:
image
来看下代码
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const ProfileContext = React.createContext();
class ProfileProvider extends React.Component {
state = {
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoka',
fullName: 'Kendō No Arikata',
team: 'KendoReact'
}
render() {
return (
{this.props.children}
)
}
}
const App = () => (
)
const Profile = () => (
{context => }
)
const User = () => (
)
const Team = () => (
{context =>
}
)
render( , document.getElementById('root'));
Context API入门
我们现在需要了解如何使用useState获取数据状态,使用useEffect替换组件生命周期方法,如何使用useContext提高provider。
在生命Context API的例子中我们使用prop传递的方法分享数据。这是个有效的方法,但是在有些情况下会显得很笨重。更好的情况下,可以是使用Context API。
从简单的prop传递示例到更易于维护的Context API示例,我们最终得到了一些代码,虽然这是解决我们问题的更好方法,但却进一步在我们的组件中造成了混乱,使得组件的复用性变差。
我们看下:
image
每个需要获取数据的地方都要使用包裹。这会在JSX中造成额外的混乱。最好可以在函数组件的顶部创建一个const变量,以便在整个JSX中使用Profile上下文。我们希望JSX尽可能简单,尽可能接近我们想要的HTML输出,让开发人员更容易阅读。Hooks改善了这种情况,是一种非常优雅的上下文消费方式。就像我提及的,它允许我们删除那些曾经把JSX弄得一团糟的标签。
image
这样看起来好多了。这是Hooks如何改变我们编写普通的日常组件的一个例子。
现在来看下使用Hooks重写之后的Profile组件。不过多解释代码内容了-如果你还记得,当我们调用useState时,我们有一组值需要去理解。一个是具体的状态值,一个是这个值的更新方法。使用useEffect,某个状态发生更改时允许改方法。使用useContext,我们只是将它指向一个现有上下文,而该属性现在拥有对该上下文的引用。整个使用方法很简单。
import React, { Component, useContext } from 'react';
import { render } from 'react-dom';
import './style.css';
// Look Ma, No Provider
const ProfileContext = React.createContext({
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoka',
fullName: 'Kendō No Arikata',
team: 'KendoReact'
});
const Profile = () => {
const context = useContext(ProfileContext);
return (
)
}
const User = () => {
const context = useContext(ProfileContext);
return (
)
}
const Team = () => {
const context = useContext(ProfileContext);
return (
)
}
const App = () => ;
render( , document.getElementById('root'));
不需要定义Provider
上面的这个demo,我们介绍使用useContext,因为我们只读取上下文中的数据,我们直接将数据传递给createContext()方法。这种方法很有效,不再需要使用< Provider >包裹内容。尽管如此,但这不是我想要的方式-为了能够改变team属性,我确实需要创建一个Provider。但我想说明的是,如果您传入一些数据,而没有像我们希望的那样访问任何函数,那么您可以在没有提供程序的情况下设置它。
但有些情况下我们需要访问并修改上下文中的状态,就需要使用Provider。比如,我希望能够改变我们的用户所属的团队。
为了上面这种情况,我们又需要创建一个Provider,而不仅仅传递默认的状态数据。
使用Provider更新数据
我们回到之前使用Context API 的例子中,使用setState更新数据。我们应该能够通过调用一个函数来更新team属性的值,这个函数将作为键-值对放在我们的状态中。
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const ProfileContext = React.createContext();
class ProfileProvider extends React.Component {
state = {
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoka',
fullName: 'Kendō No Arikata',
team: 'KendoReact',
changeTeam: (team) => this.setState({
team: `Kendo${team}`
})
}
render() {
return (
{this.props.children}
)
}
}
const App = () => (
)
const Profile = () => (
{context => }
)
const User = () => (
)
const Team = () => (
{context =>
}
)
render( , document.getElementById('root'));
让我们确保用户只需按一个带有您要切换到的团队名称的按钮,就可以在团队中发起更改。我们希望这个修改发生在< User>组件中,但按钮显示在Profile视图的底部。
image
通过添加这些按钮,我们将需要每个按钮处理一次单击,并将正确的团队框架类型传递给函数,该函数将接受团队名称的参数:Vue, Angular 或者 React。
image
我们需要一个修改状态的方法。在状态中添加一个新的属性changeTeam,它的值是名为setState的方法。我们将通过context调用这个方法。
image
现在我们可以改变team的值,也可以从上下文中读取。这个模式可以让我们设置和订阅属性值。
image
我提供了另一个对于之前的Context API例子的优化。这个例子仍然没有使用Hooks,后面我们将看下如何使用Hooks实现。
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const ProfileContext = React.createContext();
class ProfileProvider extends React.Component {
state = {
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoka',
fullName: 'Kendō No Arikata',
team: 'KendoReact',
changeTeam: (team) => this.setState({
team: `Kendo${team}`
})
}
render() {
return (
{this.props.children}
)
}
}
const App = () => (
)
const Profile = () => (
{context => }
)
const User = () => (
)
const Team = () => (
{context =>
}
)
render( , document.getElementById('root'));
接下来我们看另一个例子,包括一个类似按钮的设置,以及一个用于修改teams状态的方法。实际上,在这个Hooks版本中,按钮语法和state对象没有区别。最大的优点就是移除了,我们只需在每个功能组件中创建一个const,它将保存对我们上下文的引用:
const context = useContext(ProfileContext);
我们只需要像以前那样调用上下文及其方法。
import React, { Component, useContext } from 'react';
import { render } from 'react-dom';
import './style.css';
const ProfileContext = React.createContext();
class ProfileProvider extends Component {
state = {
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoken',
fullName: 'Kendoken No Michi',
team: 'KendoReact',
toggleTeam: (team) => this.setState({
team: `Kendo${team}`
})
}
render() {
return (
{this.props.children}
)
}
}
let Profile = () => {
const context = useContext(ProfileContext);
return (
)
}
let User = () => {
const context = useContext(ProfileContext);
return (
)
}
let Team = () => {
const context = useContext(ProfileContext);
return (
)
}
class App extends Component {
render() {
return (
);
}
}
render( , document.getElementById('root'));
我会再举一个例子,把我们当前的profile组件和“Change Team”按钮重构成放进它们自己的独立组件中,并将提供Context API的组件转为函数组件-使用useState替代this.state。注意,在最后一个例子中,我们移除了ProfileContext.Consumer标签,以及引入了useContext。现在我们所有的组件都改成函数组件了。
import React, { Component, useContext, useState } from 'react';
import { render } from 'react-dom';
import './style.css';
const ProfileContext = React.createContext();
const ProfileProvider = (props) => {
const userInformation = {
company: 'Progress',
companyImage: 'https://svgshare.com/i/9ir.svg',
url: 'https://www.telerik.com/kendo-react-ui/',
userImage: 'https://i.imgur.com/Y1XRKLf.png',
userName: 'Kendoken',
fullName: 'Kendoken No Michi',
team: 'KendoReact',
toggleTeam: (property, value) => {
setUserInfo(
{...userInfo,[property]: value}
);
}
}
const [userInfo, setUserInfo] = useState(userInformation);
return (
{props.children}
)
}
const Profile = () => {
const context = useContext(ProfileContext);
return (
)
}
const User = () => {
const context = useContext(ProfileContext);
return (
)
}
const Team = () => {
const context = useContext(ProfileContext);
return (
)
}
const ChangeTeam = () => {
const context = useContext(ProfileContext);
return (
<>
>
)
}
const App = () => (
)
render( , document.getElementById('root'));
第四节:Hooks for Reducers
在前面的几节我们了解了几个基础的React Hooks。现在,让我们把我们学到的知识应用到一个更高级的演示中并学会使用useReducer钩子。在这之前需要保证你对useState有一定了解,如果没有接触过,可以回到前面的部分看一下介绍。
Redux是除了setState之外使用reducer处理单向数据的最流行方法之一,React团队鼓励Redux管理state。然而,从16.9 版本发布之后,React现在有了useReducer,它为我们提供了一种强大的方法来使用reducer,而不依赖Redux库作为依赖项来管理UI状态。
Reducers入门
让我们讨论Redux状态reducer和JavaScript方法Array.prototype.reduce之间的区别。
求和函数是数组原型的典型例子。当我们在只包含数字的数组中调用reducer时,我们可以返回一个数值,将数组中的所有值相加。reducer可以输入一个初始值作为可选的第二个参数。让我们简要地看一看一些代码,这些代码演示了JavaScript的Array.prototype中的reduce方法。
const votesByDistrict = [250, 515, 333, 410];
const reducer = (accumulator, currentValue) => {
return accumulator + currentValue;
}
console.log(votesByDistrict.reduce(reducer));
// expected output: 1508
// and below we simply add a value to start from:
console.log(votesByDistrict.reduce(reducer, 777));
// expected output: 2285
sum函数是最简单的例子,但是在这个reduce中,您可以在花括号之间迭代地执行任何工作。可以把它想象成一个食谱。给定相同的输入,总是产生相同的结果。正在讨论的方法可以成为纯函数。这个概念是很重要的,尤其是用来处理状态管理的时候。
让我们再看一个reduce示例,帮助我们更好地理解它们。不必在每次迭代中都累积一个值,还可以在每次迭代中进行比较。就像求和运算一样,我们每次都会存储一个值,但我们不会将这些值相加,而是存储到目前为止最高的值。每次迭代,我们将比较数组中的下一个项与目前为止的最高值,如果它较大,它将替换它,如果不是,继续迭代,而不更新最高值。
const todos = [
{ name: “dishes”, priority: 2 },
{ name: “laundry”, priority: 3 ),
{ name: “homework”, priority: 1 }
];
let reducer = (highest, todo) => {
return highest.priority > todo.priority
? Highest
: todo;
}
todos.recuce(reduce, {})
// output: { name: “laundry”, priority: 3 }
这个例子演示了使用reducer的另一种方法。在每次迭代中可以做你任意想做的。这个概念很简单,我们获取一个项目数组(在本例中是对象数组),执行一个操作并将其处理为一个返回值。
React Hooks Reducer类似于JavaScript数组Reducer,返回一些东西的累积——在我们的例子中,就是React state。基于所有以前和当前的操作以及过去发生的状态修改,我们的Reducer接收一个状态和一个作为参数的操作,状态根据action.type被处理,并且我们在运行与该特定action.type的大小写相匹配的指令后返回新状态。
就像我们在现实生活中烹调一些东西,比如波尔多风格的波尔多酱一样,我们从许多配料开始:黄油、葱、小牛肉、胡椒,当然还有葡萄酒。所有这些原料在平底锅中混合,然后文火炖。如果重复并给出相同的步骤,使用相同的成分、相同的量、相同的炉子和相同的温度,我们每次都应该得到相同的结果。
State 和 Actions概述
我们将构建一个Todo应用程序。首先,我们希望todo列表有一个初始todo项,该项简单地说:“ Get Started”。
image
当我们添加一个新的todo项时,首先要分派一个操作。
此操作由Reducer函数处理。我们的操作类型是ADD_TODO。当reducer函数注意到类型变为ADD_TODO时,它的作用是把旧的状态取出来,把它展开,然后把新的todo项附加到最后,我们就得到了新的状态。
另一个需要处理的操作是COMPLETE_TODO或者换个更好的名字TOGGLE_COMPLETE。因为我们真正想做的是像开关一样打开和关闭那个状态。
在这个例子中,reducer不会向列表中添加任何新项,它会修改现有todo项的一个属性。如果我们从一个todo开始,上面写着“Get Started”,然后添加一个新的todo:“Take a break”,我们的状态现在应该有两项:
{
id: 1,
name: 'Get started',
complete: false
},
{
id: 2,
name: 'Take a break',
complete: false
}
注意,每一项都包括几个属性,其中一个就是id。这是一个拥有唯一值的键,我们使用它来定位一个特定的todo,并在不影响其他todo属性值的情况下更改该todo的一个属性。TOGGLE_COMPLETE用来将complete属性由false修改为true。 完成此操作后,任何更改都将向下传播到使用该状态的任何组件,从而触发它们更新。
因为列表中的completed初始值都是false,如果我们触发TOGGLE_COMPLETED方法更新id为1的项,状态变为下面这样:
{
id: 1,
name: 'Get started',
complete: true // We changed it!
},
{
id: 2,
name: 'Take a break',
complete: false
}
在使用Hooks之前,如果不引入第三方库,很难处理reducer操作。现在,我们可以在任何React应用程序中轻松实现reducer模式,而不必包含其他依赖项。
这使得处理内部状态比以前更加容易。这不会取代Redux的所有用法,但它为React开发人员提供了一种清晰、简洁的Redux风格的方法,可以在不安装任何依赖项的情况下立即管理内部状态。
状态管理
通常在Redux中,决定如何对状态进行分类以及存储在哪里对于初学者是最大的问题之一。这是他们的Redux常见问题解答中的第一个问题,以下是他们所说的:并没有正确的答案。有些用户更喜欢将每一条数据都保存在Redux中,以便随时维护其应用程序的完全可序列化和受控版本。其他人更喜欢在组件的内部状态中保持非关键或UI状态,例如“此下拉列表当前是否打开”。
Hooks在应用程序层非常强大,我们可以跟踪诸如“是下拉式打开”和“是菜单关闭”之类的内容。我们可以以Redux风格的方式妥善管理UI数据,而不必脱离React核心。
在唯一的责任是不断地改变和附加状态的一台机器中,reducer是每种操作的不同部分。它的逻辑将增加一个计数器或管理一个复杂的对象,这些对象的变化将对当前状态产生影响。让我们从功能组件中访问它和设置状态是这个谜题的最后一部分,同时也是新谜题的第一部分。
让我们看看如何管理非常简单的todo类型应用程序。这是一个很好的示范例子。以下是我们的Todo应用程序的规则。
我们将需要定义一些部分来设计一个使用useReducer的简单的真实案例。我们需要通过添加、完成和清除等操作来跟踪状态的修改和更新。使用熟悉的Redux模式,我们通常会将这些进程中的每一个与分配器处理的特定操作类型相关联:
一个允许我们进入任务的表单域
一个当我们提交时处理表单的分配器
一个包含所有内容的实际任务组件
一个处理状态变化的Reducer
让我们从添加和组合所有这些片段开始。我不会从创建一个React项目开始,有很多方式可以实现。我会提供一些关键代码,你可以复制下来随意处理。
在我们的简单示例中,我们将创建Todo组件作为应用程序的实际根级组件,该组件如下所示:
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const Todo = () => {
return (
<>
Todo Goes Here
>
);
}
render( , document.getElementById('root'));
它包括一个表单和未提交的输入字段。也添加了一些样式表和json数据。我们可以使用它来植入一些todo项,以测试我们的列表呈现方式以及数据的形状是否符合HTML。
你可以从这里(https://stackblitz.com/edit/todos-usereducer-hook-start)fork代码,并测试这些方法。
现在我们已经完成了这个项目,我们将通过从React导入useReducer钩子来进行第一个更改。更新代码中的第一行。
import React, { useReducer } from 'react';
现在我们需要向useReducer添加调用,它需要state 和 action 作为输入。我们将其分配给一个数组对象,这个数组对象是一个元组(两个值)-这是在进行解构,因为useReducer()将其作为返回值进行匹配:
在Todo组件的return语句上方添加以下行:
const [todos, dispatch] = useReducer(todoReducer, initialState);
items将是todo项的实际列表,dispatch将是用于更改该项列表的实际reducer。在return语句中,我们为items数组中的每个项创建一组div。
我们的应用程序还有一个问题,因为我们还没有创建一个名为todoReducer的函数。让我们将代码添加到设置initialState赋值的行的正下方。
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO': {
return (action.name.length)
? [...state, {
id: state.length ? Math.max(...state.map(todo => todo.id)) + 1 : 0,
name: action.name,
complete: false
}]
: state;
}
default: {
return state;
};
}
}
一开始这似乎很复杂。它所做的就是建立一个函数来执行状态和动作。通过switch还判断action.type。一开始,我们只有一个操作,但我们也希望设置默认的catch all,此默认值将返回当前状态。
但是如果它捕捉到一个真正的ADD_TODO,我们将返回当前状态,展开,并将有效数据附加到末尾。棘手的部分是分配新的ID。我们在这里所做的是获取todo的现有列表,并返回最大id加1,否则为零。
既然我已经设置了一个初始状态,我们很高兴进入下一步。我们需要确保当我们按enter键时在输入字段中键入时,我们输入的值被发送到一个函数,该函数将进行处理。
因此,首先让我们用类名todo input替换div,如下所示:
这确保当我们点击enter时,我们将表单信息发送给一个名为addTodo()的函数。我们还使用ref属性引用输入,并为该元素提供inputRef的引用值。随着这些更新,我们需要做更多的事情。
1)我们需要创建一个名为inputRef的属性,它调用useRef钩子2)我们需要创建一个名为addTodo()的函数
让我们从创建inputRef属性开始。在todo组件的顶部,添加以下属性:
const inputRef = useRef();
我们将使用ref属性获取对输入的引用,这将允许我们稍后访问其值。他的引用将由todo函数组件中的本地属性支持,但这只是对useRef钩子的调用。调用创建的inputRef属性,使用inputRef.value获取输入的值。
你需要像我们引入useReducer一样导入另一个钩子。
import React, { useReducer, useRef } from 'react';
最后,我们需要创建addTodo()函数,该函数将使用此引用并负责分配ADD_TODO类型的操作。在返回的正上方添加以下函数:
function addTodo(event) {
event.preventDefault();
dispatch({
type: 'ADD_TODO',
name: inputRef.current.value,
complete: false
});
inputRef.current.value = '';
}
在函数内部,为了防止我们点击提交表单时页面刷新。我们调用了preventDefault ()方法。
然后,我们使用inputRef从表单中获取输入值来触发ADD_TODO操作。所有todo项最初的completed都是false。最后,我们将inputRef值设置为空。这将清除输入字段。
最后,在ADD_TODO触发之前,我们还需要进行一次更新。在JSX内部,我们仍然在initialState上进行映射。我们需要从下面的行中更改:
{initialState.map((todo) => (
改为:
{todos.map((todo) => (
现在我们应该有一个工作的useReducer钩子,它利用addTodo函数将操作分派给todoReducer。
添加完成的待办事项
让我们在这个项目中也有一个useffect的工作示例。每次签出待办事项时,我们都将更新document.title以显示列表中已完成待办事项的计数或数量。
在addTodo()函数的正上方,让我们添加逻辑来计算我们有多少已完成的todo。然后,当document.title更改时,我们需要一个useffect方法来更新它:
const completedTodos = todos.filter(todo => todo.complete);
useEffect(() => {
// inputRef.current.focus();
document.title = `You have ${completedTodos.length} items completed!`;
})
要做到这一点,我们还需要引入钩子:
import React, { useReducer, useRef, useEffect } from 'react';
我们还没有完成,我们现在需要添加一个事件,该事件将调用函数,该函数将分派完成的任务。向div添加一个onClick处理程序,类名为todo name。
You clicked {this.state.count} times
You clicked {count} times
You clicked {this.state.count} times
You clicked {count} times
{this.state.firstName} {this.state.lastName}
Hello, {firstName} {lastName}
{this.state.firstName} {this.state.lastName}
Hello, {firstName} {lastName}
{props.data.team}
{context.team}
{context.team}
{context.team}
{context.team}
{context.team}
{context.team}
toggleComplete(todo.id)}>
{todo.name}
接下来,我们需要一个函数来处理这个点击事件。它很简单,只发送一个简单的id和操作类型。将此添加到addTodo()函数的正下方:
function toggleComplete(id) {
dispatch({ type: 'TOGGLE_COMPLETE', id });
}
最后,我们添加下面代码到todoReducer中:
case 'TOGGLE_COMPLETE': {
return state.map((item) =>
item.id === action.id
? { ...item, complete: !item.complete }
: item
)
}
我还设置了一个样式,我们将根据todo的完整值是否为true来添加或删除该样式。在todos.map代码下面,让我们更改如下所示的代码行:
改为:
我们不再需要alt属性,所以我们删除了它。现在,当我们单击todo时,它将分派一个操作,并将该特定todo的completed值设置为true,现在,我们的过滤器将通过useffect方法来获取这个值,该方法反过来更新document.title。我们还将添加类名completed,并且完成的todo将变得不透明,以表示完成的todo。
在这个时候,除了delete功能,以及清除列表中所有todo的按钮之外,我们几乎所有的东西都在起作用。为了完成我们的演示,我们将重复我们已经学到的使最后两个功能工作的内容。
删除一个Todo项
首先,为todos HTML中的close图标添加onClick()事件:
)
}
export default Counter;
因此,我们希望在这里创建一个自定义钩子,将一段文本传递到钩子中,钩子会为我们更新文档标题。首先让我们看看创建此自定义挂钩所需的代码:
const useDocumentTitle = (title) => {
useEffect(() => {
document.title = title;
}, [title])
}
在上面你可以看到我们需要这个钩子作为参数的是一个字符串,我们称之为title。在钩子中,我们调用React Core的基本useffect钩子,并设置标题。useffect的第二个参数将为我们执行该检查,并且仅当标题的本地状态与我们传入的不同时才更新标题。你的意思是,创建自定义钩子和创建函数一样简单?是的,它的核心非常简单,而且该函数可以引用任何其他钩子。该死的…创建自定义钩子比我们想象的要容易!
让我们回顾一下我们的整体功能组件现在的样子。你会看到我把useffect的旧调用注释掉了,上面是我们如何使用自定义钩子来代替它。
import React, { Component, useState, useEffect } from 'react';
const useDocumentTitle = title => {
useEffect(() => {
document.title = title;
}, [title])
}
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useDocumentTitle(`You clicked ${count} times`);
// useEffect(() => {
// document.title = `You clicked ${count} times`
// });
return (
)
}
export default Counter;
让我们进一步清理一下,看看如果这个钩子是由某个npm包提供的,而不是被复制粘贴在文件的顶部,我们可以如何使用它。
import React, { Component, useState } from 'react';
import useDocumentTitle from '@rehooks/document-title';
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useDocumentTitle(`You clicked ${count} times`);
return (
)
}
export default Counter;
这真是太棒了,但我也希望您注意到,我不必在我的功能组件中导入useffect,因为我们从“@rehooks/document title”导入的钩子会处理这个问题。所以如果我不需要useffect,我可以从组件导入中省略它。
我希望这说明了创建自定义React钩子的基本原理,并且您甚至可以通过这样一个简单的例子看到它的威力。
管理KendoReact组件的控制状态
Hooks非常适合处理特定类型的应用程序状态。例如控制状态、本地组件状态和会话状态。在使用KendoReact UI(https://www.telerik.com/kendo-react-ui/)组件时,我想利用Hooks,但我想从简单开始。我们将重构,不再使用类,而是使用函数组件。我们将查找演示使用this.state和this.setState的实例,因为当我们将组件转换为函数时,我们将不再需要使用this关键字,我们将不需要使用构造函数或调用setState。
因此,让我们开始重构KendoReact演示,演示如何使用我们的KendoReact对话框。
如果您看看下面演示的main.jsx(https://stackblitz.com/edit/kendoreact-dialog-class-based?file=app/main.jsx)页面,我们可以确定在使用功能组件和React钩子时会发生变化的几个目标区域。用绿色突出显示的代码和行需要修改,用红色突出显示的行可以完全删除。
image
在第6行有一个类定义,我们需要把它转换成函数组件
第7行有一个构造器,第8行调用了super()方法,第10行有一些绑定。在使用Hooks的函数组件中这些都是不需要的。
在第9行中,我们创建一个状态实例,并给它一个默认值true,这将是对useState钩子的调用。
在第13行,我们需要重命名toggleDialog函数并将其切换到ES6箭头函数样式语法,第14行到第16行只需调用useState()赋值提供的更新方法setVisible,它将引用的值将是可见的,而不是this.state.visible。
在第19行中,我们有一个render()调用,这在函数组件中是不必要的
在第22、23、26和27行我们提到了这一点。而this.state需要引用visible和toggleVisible而不是toggleDialog,稍后我将解释为什么要重命名该函数。
首先要做的就是将组件转为函数组件,移除constructor构造器,删除supr()引用以及toggleDialog()函数绑定。这里可以使用多种语法选项,我更喜欢ES6箭头函数样式:
const multiply = (x, y) => { return x * y };
在我们的组件中,第6行现在看起来如下:
const DialogWrapper = () => {
让我们来设置一个钩子来代替state对象。我们将不创建名为state的对象,而是设置对useState()的调用,并将其返回值解构为一个变量,该变量将保存我们的状态和更新/设置方法来更新该状态。我们的状态名称将是可见的,其更新方法将被称为setVisible。我们将删除整个构造函数并将其替换为这一行:
const [visible, setVisible] = useState(true);
因为我们使用的是useState()基本钩子,所以还需要导入它。我们的React导入现在看起来像:
import React, { useState } from 'react';
接下来,我们需要一个在这个组件中调用setVisible的函数来切换它的值。我们将其命名为toggleVisible,而不是toggleDialog,因为我们在一个功能组件中,所以之前使用的语法将不起作用。相反,我将更新为ES6箭头函数样式。此函数只需将可视状态设置为与当前状态相反的状态。
const DialogWrapper = () => {;
const [visible, setVisible] = useState(true);
const toggleVisible = () => setVisible(!visible);
现在我们需要去掉render()块及其两个大括号。此外,我们需要删除对this.toggleDialog和this.state.visible的所有引用,并相应地将它们更改为toggleVisible和visible。现在在return()中,我们将进行以下更改:
return (
deleteTodo(todo.id)}>
×
我们将添加操作函数来处理这些操作,它不必是它们自己的函数,我们可以直接从onClick()传递,或者我们可以设置一个类似的switch语句来处理所有分配。我们可以采取任何我们想要的方法。为了演示的目的,我想逐个添加它们。
现在我们创建一个函数来处理dispatch:
function deleteTodo(id) {
dispatch({ type: 'DELETE_TODO', id });
}
现在我们只需在reducer的switch语句中添加一个case来处理reduce。在这里,我们使用数组的.filter()方法从列表中删除一个满足id的todo项并返回状态。
case 'DELETE_TODO': {
return state.filter((x) => x.id !== action.id);
}
清除所有Todos
对于清除todo操作没什么特别的,我们只需要返回一个空数组。下面是实现这一点所需的三段不同的代码。
将onClick()添加到HTML按钮:
onClick={() => clearTodos()}
添加一个方法处理dispatch:
function clearTodos() {
dispatch({ type: 'CLEAR_TODOS' });
}
在我们的reducer方法里添加一个case:
case 'CLEAR_TODOS': {
return [];
}
Reducers总结
我们现在已经使用useReducer构建了Todo应用程序的基础。当处理数据子级别稍微复杂一点的状态时,此模式将非常有用。我们了解了纯函数以及为什么它们是reducer的核心,允许我们返回可预测的状态,现在使用这种模式在核心React库中更容易实现。
第五节:自定义React Hooks
让我们学习如何创建一个定制的React钩子,以及使用钩子时必须记住的所有规则。
Hooks只是功能!任何函数都可以成为Hooks。我觉得ReactJS文档站点上的文档不够简单。这不是敲打他们,我只是觉得,如果我能尝试用更简单的方式来解释,更多的人会受益。
重温 Effect Hook
如果您对基本Hooks有足够的了解,可以直接跳到创建自定义Hooks。不必再回顾所有的基本Hooks,我想我们只需要重新访问其中一个:useffect钩子。我在阅读ReactJS.org文档的Hooks时了解到,有两种方法可以使用useffect。无需清除的 effect和需要清除的effect。我希望在这个阶段使用Hooks的任何人要么知道这些术语,要么花几分钟来读一下官方文档。
在类和Hooks可用之前,effect被放在许多生命周期方法中,比如:componentDidMount 或者 componentDidUpdate。如果在这两种方法中都有重复的代码(执行相同的处理和更新效果),现在我们可以在功能组件中执行这些操作,只需一个钩子就可以完成。
useffect告诉React我们的组件需要在组件呈现之后做一些事情。它在第一次渲染之后和每次更新之后运行。在我之前的文章中,我只讨论了没有清理的副作用,所以我想很快地介绍如何允许功能组件在清理时产生副作用。
下面是一个示例,说明如何在不进行任何清理的情况下运行useffect:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
如果确实需要清理才能运行,可以从useffect返回函数。这是可选的,它允许您在效果之后和任何新效果运行之前运行一些代码。订阅某些内容的情况可能需要取消订阅,作为效果清理过程的一部分。React将在卸载时执行此清理。
useEffect(() => {
console.log("Subscribe to Something);
return function cleanup() {
console.log("Unsubscribe to Something);
};
});
以上效果将在每个渲染上运行一次以上。React在运行下一个渲染的效果之前清除上一个渲染的效果,这应该注意。有关为什么在每次更新时都运行hook的解释,请查看ReactJS文档。不过,请记住,如果此行为导致性能问题,则可以选择退出。
我们还可以通过使用可选参数跳过效果来优化性能。例如,可能我们不想运行subscribe/unsubscribe效果,除非某些id已更改。看看下面的例子,了解如何做到这一点,这是相当简单的!
useEffect(() => {
console.log("Subscribe to Something);
return () => {
console.log("Unsubscribe to Something);
};
}, [props.something.id]); // only if something.id changes
Hooks,特别是useffect,现在允许您根据代码正在做什么而不是它在什么生命周期方法中拆分代码。当我们只有类和生命周期方法时,我们有时不得不混合关注点。现在,使用多个useffect方法,React可以按指定的顺序应用每个效果。这对于在应用程序中组织代码是一个巨大的好处。
创建自定义Hook
我真的很喜欢亚当·拉基斯(Adam Rackis)最近在推特上发表的一篇文章:“Hooks的创作水平远远超过我们所看到的任何东西。”关于Hooks,我想让你了解的是,我们在类中看到的所有伟大的变化,以及我们如何有这么多的组合选项,现在Hooks中都有了这些。这意味着,现在当涉及到React中功能组件的组成时,我们的手是不受约束的。对于React开发人员来说,这是一个巨大的进步。
自定义钩子就是JavaScript函数,其名称以单use作为前缀。自定义钩子是一个普通函数,但我们使用不同的标准。通过在开头添加use这个词,我们知道这个函数遵循Hooks的规则。
有了对Hook的更好理解,让我们把我们知道的作为一段简单的代码,我们的文档标题更新,并创建一个简单的自定义Hook。
似乎我们需要在几个页面上或在应用程序中的许多不同功能组件中执行某些操作。当信息更改时,我们希望用某种类型的字符串更新文档标题。另外,我们不想在每个功能组件中重复这个逻辑。我们将从在同一页面的本地将此代码提取到钩子开始,然后查看如何将同一钩子导入到多个组件并共同定位。很简单吧?
如果这是真的,那么我们的自定义钩子也可以调用React Core基本钩子之一,比如useffect。让我们再检查一次更新文档标题的功能组件。
import React, { Component, useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useEffect(() => {
document.title = `You clicked ${count} times`
});
return (
You clicked {count} times
You clicked {count} times
You clicked {count} times
{visible && }
);
同样,我们刚刚更新了return()中的代码,以不引用this关键字并使用新的函数名toggleVisible。
我们已经成功地将KendoReact演示转换为使用功能组件和基本useState挂钩。让我们使用一个叫做githrisk的很棒的工具来看看我们的整体变化是什么样子的:
image
总结
我希望本指南能帮助您更好地理解Hooks的基础知识,并允许您在这些示例的基础上创建新的和令人惊奇的东西。如果它对你有用,请分享和传播。
我想让你对Hooks的创建有一个很好的理解,我认为这可以通过回顾Sophie Alpert在React Conf 2018上的演讲得到最好的解释。
在过去,一些React开发人员在何时使用和何时不使用类方面遇到了困惑。这个问题可以追溯到几年前,在一个案例中,丹阿布拉莫夫的一篇文章中写道:如何使用React类在晚上睡觉。
尽管我们有时可能在当前的React中使用它们,或者在将来处理遗留代码时遇到它们,但这个问题现在正在处理中,我们已经看到开发人员有很强的见解,并且大多使用功能组件。
当谈到React团队正在做些什么,以便更容易地构建优秀的UI,并改进React中的开发人员体验时,Sophie Alpert提出了一个很好的问题。
为什么React仍然很糟糕?
以下是React Conf 2018大会上著名演讲的答案:
重用逻辑
在React hook之前,我们使用了很多高阶组件和渲染道具来实现这一点,这将需要您在使用这些模式时经常重新构建应用程序,并导致包装地狱(末日金字塔风格的嵌套)。
巨大的部件
由于在不同的生命周期方法中分割出不同的逻辑片段,我们的组件中经常出现混乱。
混淆类
这是我将留给你的许多引语中的第一个引语。
课程对人类来说很难,但不仅仅是人类,课程对机器来说也很难——索菲·阿尔伯特
理解JavaScript中的类可能很棘手,而且在hook之前,还需要使用类组件来访问状态和生命周期方法。简单地定义类组件需要相当多的样板文件。钩子有助于解决这些问题,出于这个原因,我想留给你一些其他值得注意的引用,我们的无畏反应和社区领袖!
Hooks允许您始终使用函数,而不是在函数、类、HOC和渲染道具之间切换——Dan Abramov
如果你想让世界变得更美好,看看React Hooks,然后做出改变。--迈克尔杰克逊
Hooks提供了一种处理React中问题的新方法——Dave Ceddia
有了React钩子,我们拥有了两个世界中最好的:可以使用状态的干净功能组件——David Katz
钩子是React,是React要去的地方,是React V2——迈克尔杰克逊
React钩子从根本上简化了我创建、编写、读取和原型组件的方式——Zach Johnson
这些就是人们所说的关于React Hooks的一些事情。
如果你希望了解更多前端知识,请关注我的公众号“前端记事本”
发表评论 (审核通过后显示评论):