原文连接javascript
在2018年10月的React Conf上引入了React Hooks,做为在React函数组件中使用state和反作用的一种方式。尽管功能组件之前被称为函数无状态组件(FSC),但它们最终可以与React Hooks一块儿使用状态。所以,许多人如今将它们称为函数组件。java
在本演练中,我想解释钩子函数背后的动机,React会发生什么变化,本教程只是对React Hooks的介绍。在本教程的最后,您将找到更多的教程来深刻了解React Hooks。 ##为何要使用React Hooks? React Hooks是由React团队发明的,旨在在函数组件中引入状态管理和反作用。这是他们的一种方法,它无需使用生命周期方法将React函数组件重构为React类组件,而是以使用反作用或state的方式,使得React函数组件变得更加轻松,React Hooks让咱们可以仅使用函数组件来编写React应用程序。react
没必要要的组件重构:之前,仅React类组件用于本地状态管理和生命周期方法。后者对于在React类组件中引入诸如侦听器或数据获取之类的反作用相当重要。npm
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button
onClick={() =>
this.setState({ count: this.state.count + 1 })
}
>
Click me
</button>
</div>
);
}
}
export default Counter;
复制代码
仅当您不须要状态或生命周期方法时,才可使用React无状态组件。并且因为React函数组件更轻巧(和美观),人们已经使用了不少函数组件。这样作的缺点是每次须要状态或生命周期方法时都会将组件从React函数组件重构为React类组件(反之亦然)。编程
import React, { useState } from 'react';
// how to use the state hook in a React function component
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Counter;
复制代码
使用Hooks时,不须要进行此重构。反作用和状态最终能够在React函数组件中得到。这就是将函数无状态组件重命名为函数组件的合理缘由。api
反作用逻辑:在React类组件中,反作用大可能是在生命周期方法中引入的(例如componentDidMount,componentDidUpdate,componentWillUnmount)。反作用多是在React中获取数据或与Browser API交互。一般,这些反作用来自设置和清理阶段。例如,若是您想删除您的监听器,可能会遇到React性能问题。数组
// side-effects in a React class component
class MyComponent extends Component {
// setup phase
componentDidMount() {
// add listener for feature 1
// add listener for feature 2
}
// clean up phase
componentWillUnmount() {
// remove listener for feature 1
// remove listener for feature 2
}
...
}
// side-effects in React function component with React Hooks
function MyComponent() {
useEffect(() => {
// add listener for feature 1 (setup)
// return function to remove listener for feature 1 (clean up)
});
useEffect(() => {
// add listener for feature 2 (setup)
// return function to remove listener for feature 2 (clean up)
});
...
}
复制代码
如今,若是您要在React类组件的生命周期方法中引入多个以上反作用,那么全部反作用将按生命周期方法分组,而不是按反作用自己来分组。这就是React Hooks经过useEffect钩子函数将反作用封装在其中,而每一个hook在处理和清理阶段都有其本身的反作用。您将在本教程的后面部分中看到如何经过在React Hook中添加和删除监听器来实现此功能。浏览器
React的抽象地狱:React中的高阶组件和render props组件体现了抽象以及可复用性。还有React的上下文Context及其Provider和Consumer组件,它们引入了另外一个层级的抽象。React中全部这些高级模式都使用了所谓的包装组件。如下组件的实现对于建立更大的React应用程序的开发人员来讲不该该陌生。bash
import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
function App({ history, state, dispatch }) {
return (
<ThemeContext.Consumer>
{theme =>
<Content theme={theme}>
...
</Content>
}
</ThemeContext.Consumer>
);
}
export default compose(
withRouter,
withReducer(reducer, initialState)
)(App);
复制代码
Sophie Alpert在React中将其称为“包装地狱”。您不只会在实现中看到它,并且还会在浏览器中检查组件时看到它。因为Render Prop组件(包括React上下文中的Consumer组件)和High-Order组件,包装的组件有数十个。它变成了一个不可读的组件树,由于全部抽象的逻辑都被其余React组件所掩盖。实际可见的组件很难在浏览器的DOM中找到。那么,若是不须要这些附加组件,而是仅将逻辑做为反作用封装在函数中,该怎么办呢?那么您能够删除全部这些包装组件并展平组件树的结构:react-router
function App() {
const theme = useTheme();
const history = useRouter();
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Content theme={theme}>
...
</Content>
);
}
export default App;
复制代码
这就是React Hooks提出的建议。全部反作用都直接存在于组件中,而没有引入其余组件做为业务逻辑的容器。容器消失了,逻辑仅存在于函数的React Hooks中。
JavaScript类混乱: JavaScript很好地融合了两个概念:面向对象编程(OOP)和函数式编程。React向开发人员很好地展现了这两个概念。一方面,React(和Redux)向人们介绍了由函数组成的函数式编程(FP),还有其余函数的通用编程概念(例如,高阶函数,JavaScript内置方法(如map,reduce,filter))以及其余术语,例如做为不变性(immutability )和反作用( side-effects)。React自己并无真正引入这些东西,由于它们是语言或编程范例自己的功能,可是它们在React中大量使用,使得每一个React开发人员都潜移默化地成为更好的JavaScript开发人员。
另外一方面,React使用JavaScript类做为定义React组件的一种方法。类仅是声明,而组件其实是类的实例化。它建立一个类实例,而该类实例的this对象用于与类方法进行交互(例如setState,forceUpdate,其余自定义类方法)。可是,对于没有面向对象编程思想(OOP)的React初学者,课程的学习曲线更为陡峭。这就是为何类绑定,this对象和类继承会形成混淆。
如今,许多人都认为React不该该取消JavaScript类,这是人们不理解React Hooks。可是,引入Hooks API的假设之一是,对于React初学者来讲,当他们一开始就编写没有JavaScript类的React组件时,学习曲线就更加平滑。
每次引入新功能时,人们都会对此加以关注。创新派会所以感到兴奋,而部分人则对变革感到恐惧。我听人们最关心React Hooks的问题是:
让我来回答以上问题:
一切都变了:React Hooks未来会改变咱们编写React应用程序的方式。可是,如今仍是什么都没有改变。您仍然可使用局部状态和生命周期方法编写类组件,还有其余例如高阶组件或render props组件。React团队确保React保持向后兼容。与React 16.7相同。
React变得像Angular同样臃肿:React一直被视为具备轻量API的库。没错,未来也是如此。可是,为了适应几年前基于组件构建应用程序的作法,而且不被其余库所取代,React引入了一些更改,以支持较旧的API。若是React有全新的东西,应该只有函数组件和React Hooks。可是React是在几年前发布的,须要进行调整以跟上现状或改变现状。也许几年后将会弃用React类组件和生命周期方法,以支持React函数组件和React Hooks,可是目前,React团队将React类组件保留在其工具库中。毕竟,React团队利用hooks做为一项发明但愿能够长期使用。显然,React Hooks为React添加了另外一个API,可是有利于未来简化React的API。我喜欢这种过渡,而不是推出一个与以前React大相径庭的React2。
这没用,用Class组件也能够正常工做:假设您从零开始学习React,而后直接给您介绍React Hooks。也许建立React应用程序不会从React类组件开始,而是从React函数组件开始。您须要学习的全部组件都是React Hooks。它们管理状态和反作用,所以您只须要了解state和effect hooks。对于React初学者来讲,无需使用JavaScript类(继承,this,绑定,super,...)带来的全部其余开销,这些都是React类组件以前的作法。经过React Hooks学习React将会更加简单。想象一下React Hooks是一种如何编写React组件的新方法-这是一种新思惟。我参加过不少次React研讨会,而且我是一个多疑的人,可是当我用React Hooks编写了一些简单的场景,我就确信这是编写和学习React的最简单方法。
最后,以这种方式进行思考:基于组件的解决方案(例如Angular,Vue和React)在每一个发行版中都在推进Web开发的边界。它们创建在二十多年前发明的技术之上,而且不断进行调整,使得Web开发在2018年变得不费吹灰之力。他们疯狂地对其进行了优化,以知足当代的需求。咱们正在使用组件而不是HTML模板来构建Web应用程序。我能够想将来象咱们将坐在一块儿,为浏览器发明基于组件的标准。Angular,Vue和React只是这一运动的先行者。
你已经在代码中看到了一个典型的计数器示例的useState Hook。它用于管理功能组件中的本地状态。让咱们在更复杂的场景中使用该钩子函数,在该示例中,咱们将管理列表项:
import React, { useState } from 'react';
const INITIAL_LIST = [
{
id: '0',
title: 'React with RxJS for State Management Tutorial',
url:
'https://www.robinwieruch.de/react-rxjs-state-management-tutorial/',
},
{
id: '1',
title: 'React with Apollo and GraphQL Tutorial',
url: 'https://www.robinwieruch.de/react-graphql-apollo-tutorial',
},
];
function App() {
const [list, setList] = useState(INITIAL_LIST);
return (
<ul>
{list.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
复制代码
useState钩子函数接收一个初始状态做为参数,并经过使用数组解构来返回两个可命名的变量。第一个变量是state的值,而第二个变量是则是更新状态的函数。
此示例的目标是从列表中删除一个项。为了实现它,呈现列表中的每一个项都有一个带有点击事件的按钮。能够在函数组件中内联一个onRemoveItem
函数,它稍后将使用list和setList。不须要将这些变量传递给函数,由于它们已在组件的外部范围中可用。
function App() {
const [list, setList] = useState(INITIAL_LIST);
function onRemoveItem(id) {
// remove item from "list"
// set the new list in state with "setList"
}
return (
<ul>
{list.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
<button type="button" onClick={() => onRemoveItem(item.id)}>
Remove
</button>
</li>
))}
</ul>
);
}
复制代码
咱们须要以某种方式知道应该从列表中删除的列表项。使用高阶函数,咱们能够将该项的id传递给处理函数。不然,咱们将没法识别应从列表中删除的项目。
function App() {
const [list, setList] = useState(INITIAL_LIST);
function onRemoveItem(id) {
const newList = list.filter(item => item.id !== id); //删除目标item
setList(newList); // 从新设置列表
}
return (
<ul>
{list.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
<button type="button" onClick={() => onRemoveItem(item.id)}>
Remove
</button>
</li>
))}
</ul>
);
}
复制代码
你能够根据传递给函数的id从列表中删除列表项。而后,用filter函数过滤列表,并使用setList函数设置列表的新的state。
useState钩子函数为你提供了管理功能组件中的状态所需的一切:初始状态,最新状态和状态更新函数。其余一切都是JavaScript。此外,你没必要像之前在类组件中那样为状态对象的浅层合并而烦恼。相反,你可使用useState来封装一个域(例如,列表),可是若是你须要另外一个状态(例如,计数器),则只需使用另外一个useState来封装该域。
让咱们转到下一个名为useEffect的钩子函数。如上所述,功能组件应该可以经过钩子管理状态和反作用。经过useState挂钩展现了管理状态。如今出现了useEffect钩子,用于产生反作用,这些反作用一般用于与Browser / DOM API或外部API(例如数据提取)进行交互。让咱们看看如何经过实现一个简单的秒表将useEffect钩子函数与浏览器API交互:
import React, { useState } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
return (
<div>
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
export default App;
复制代码
目前尚未秒表。可是至少有一个条件渲染显示“开始”或“中止”按钮。按钮的状态由useState挂钩管理。
让咱们用useEffect介绍咱们的反作用,该反作用记录了一个间隔。它每秒发出一个console.log,在控制台打印出来。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
setInterval(() => console.log('tick'), 1000); // 每秒执行一次
});
return (
<div>
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
export default App;
复制代码
为了在组件卸载时清空定时器,能够在useEffect中返回一个函数,以进行任何清理操做。例如,当该组件再也不存在时,不该有任何内存泄漏。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval); // 这样在组件卸载时,就会清空定时器
});
...
}
export default App;
复制代码
如今,你要在挂载组件时设置反作用,并在卸下组件时清理反作用。若是你记录useEffect钩子中的函数被调用的次数,则会看到每次组件状态改变时,它都会设置一个新的interval(例如,单击“开始” /“中止”按钮)。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
console.log('effect runs');
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval);
});
...
}
export default App;
复制代码
为了仅在组件的挂载和卸载时执行反作用,您能够向其传递一个空数组做为第二个参数。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval);
}, []); // 这样useEffect只会在挂载和卸载阶段执行
...
}
export default App;
复制代码
可是,因为每次卸载时清空了定时器,所以咱们也须要在更新周期中setInterval。可是咱们能够告诉效果useEffect仅在isOn变量更改时才运行。仅当数组中的变量之一更改时,useEffect才会在更新周期内运行。若是将数组保留为空,则效果将仅在挂载和卸载时运行,由于没有变量可用于再次运行反作用。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(interval);
}, [isOn]); // useEffect会在isOn变化时来执行
...
}
export default App;
复制代码
不管isOn是true仍是false,setInterval都会运行。但咱们只但愿在秒表激活的时候才运行:
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(() => console.log('tick'), 1000);
}
return () => clearInterval(interval);
}, [isOn]);
...
}
export default App;
复制代码
如今在功能组件中引入另外一种状态,以跟踪秒表的计时器。它用于更新计时器,但仅在秒表被激活时使用。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer + 1), // 秒表被激活时,计时器就+1
1000,
);
}
return () => clearInterval(interval);
}, [isOn]);
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
export default App;
复制代码
代码中仍然存在一个错误。当setInterval运行时,它将每秒增长1,从而更新计时器。可是,它始终依赖计时器的失效状态。仅当inOn改变时,状态才时正常的。为了在间隔运行时始终接收计时器的最新状态,能够对始终具备最新状态的状态更新功能使用功能。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer => timer + 1),
1000,
);
}
return () => clearInterval(interval);
}, [isOn]);
...
}
export default App;
复制代码
另外一种选择是在计时器更改时也运行效果。而后效果将接收最新的计时器状态。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer + 1),
1000,
);
}
return () => clearInterval(interval);
}, [isOn, timer]);
...
}
export default App;
复制代码
这是使用浏览器API的秒表的实现。若是要继续,您也能够经过提供“重置”按钮来扩展现例。
import React, { useState, useEffect } from 'react';
function App() {
const [isOn, setIsOn] = useState(false);
const [timer, setTimer] = useState(0);
useEffect(() => {
let interval;
if (isOn) {
interval = setInterval(
() => setTimer(timer => timer + 1),
1000,
);
}
return () => clearInterval(interval);
}, [isOn]);
const onReset = () => {
setIsOn(false);
setTimer(0);
};
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
<button type="button" disabled={timer === 0} onClick={onReset}>
Reset
</button>
</div>
);
}
export default App;
复制代码
useEffect钩子函数用于React函数组件中的反作用,该函数用于与浏览器/ DOM API或其余第三方API进行交互(例如,数据获取)。
最后,当您了解了在功能组件中引入状态和反作用的两个最流行的钩子函数以后,我要向您展现的最后一件事:自定义钩子。没错,您能够实现本身的自定义React Hook,能够在您的应用程序或其余应用程序中复用。让咱们看看它们如何与示例应用程序一块儿使用,该应用程序可以检测您的设备是在线仍是离线。
import React, { useState } from 'react';
function App() {
const [isOffline, setIsOffline] = useState(false);
if (isOffline) {
return <div>Sorry, you are offline ...</div>;
}
return <div>You are online!</div>;
}
export default App;
复制代码
再次,引入useEffect钩子以产生反作用。在这种状况下,该效果会添加和删除监听器,用于检查设备是在线仍是离线。两个侦听器仅在安装时设置一次,并在卸载时清除一次(空数组做为第二个参数)。每当调用其中一个侦听器时,它都会为isOffline布尔值设置状态。
import React, { useState, useEffect } from 'react';
function App() {
const [isOffline, setIsOffline] = useState(false);
function onOffline() {
setIsOffline(true);
}
function onOnline() {
setIsOffline(false);
}
useEffect(() => {
window.addEventListener('offline', onOffline);
window.addEventListener('online', onOnline);
return () => {
window.removeEventListener('offline', onOffline);
window.removeEventListener('online', onOnline);
};
}, []);
if (isOffline) {
return <div>Sorry, you are offline ...</div>;
}
return <div>You are online!</div>;
}
export default App;
复制代码
如今一切都很好地封装在一个useEffect中,也能够在其余地方复用。这就是为何咱们能够将这个功能提取为其自定义钩子函数,该钩子遵循与其余钩子相同的命名约定。
import React, { useState, useEffect } from 'react';
function useOffline() {
const [isOffline, setIsOffline] = useState(false);
function onOffline() {
setIsOffline(true);
}
function onOnline() {
setIsOffline(false);
}
useEffect(() => {
window.addEventListener('offline', onOffline);
window.addEventListener('online', onOnline);
return () => {
window.removeEventListener('offline', onOffline);
window.removeEventListener('online', onOnline);
};
}, []);
return isOffline;
}
function App() {
const isOffline = useOffline();
if (isOffline) {
return <div>Sorry, you are offline ...</div>;
}
return <div>You are online!</div>;
}
export default App;
复制代码
将定制钩子提取为函数并非惟一的事情。您还必须根据isOffline从自定义钩子函数返回状态,以便在您的应用程序中使用它来向离线用户显示消息。这是用于检测您在线仍是离线的自定义钩子。您能够在React的文档中阅读有关自定义钩子的更多信息。
React Hooks的可复用性很是高,由于有可能发展出一个能够从npm安装到任何React应用程序的自定义React Hooks生态系统。