Open WangShuXian6 opened 3 years ago
https://github.com/jrlarsen/react-hooks-in-action React使得构建用户界面组件变得很容易,这些组件是可组合的、可重用的,并能对数据的变化和用户的互动做出反应。 一个社交媒体网站的页面包括按钮、帖子、评论、图片和视频,以及其他许多界面组件。 当用户向下滚动页面、打开帖子、添加评论或转换到其他视图时,React帮助更新界面。 一些页面上的组件可能有重复的子组件,即具有相同结构但内容不同的页面元素。 而这些子组件也可能是由组件组成的!这里有图片缩略图、重复的按钮、可点击的文本和大量的图标。 从整体上看,这个页面有数百个这样的元素。但是,通过将这样丰富的界面分解成可重用的组件,开发团队可以更容易地专注于特定的功能领域,并将这些组件应用于多个页面。 使得定义和重用组件变得容易,并将它们组成复杂但可理解和可用的界面是React的核心目的之一
React使应用程序的用户界面与它的数据保持同步。 你的应用程序在任何时候持有的数据被称为应用程序的状态,可能包括,例如,当前的帖子,关于登录用户的细节,评论是显示还是隐藏,或者文本输入字段的内容。 如果新的数据通过网络到达,或者用户通过按钮或文本输入更新了一个值,React就会计算出需要对显示进行哪些改变,并有效地更新它。
React智能地安排更新的顺序和时间,以优化你的应用程序的感知性能并改善用户体验。 React通过重新渲染用户界面来响应组件状态的变化。 但更新状态和重新渲染并不是一次性的任务。 访客在使用你的应用程序时可能会引起大量的状态变化,而React需要反复向你的组件询问代表这些最新状态值的最新用户界面。 你的组件的工作是将它们的状态和props(传递给它们的属性)转换成它们的用户界面描述。 然后React接受这些用户界面描述,并在必要时对浏览器的文档对象模型(DOM)进行排版更新。
当一个组件的状态值发生变化时,React会重新渲染用户界面。
步骤 | 会发生什么? | 讨论 |
---|---|---|
1 | React调用该组件。 | 为了生成页面的UI,React遍历了树状的组件,调用每一个组件。React会把每个组件在JSX中设置为属性的任何props。 |
2 | 该组件指定了一个 | 事件处理程序可以监听用户的点击、定时器的启动。事件处理程序。 或资源加载,例如。该处理程序将改变以后运行时的状态。React会把处理程序挂到当它在第4步更新DOM时,将DOM加入。 |
3 | 该组件返回其用户界面。 | 该组件使用当前的状态值来生成其用户界面并将其返回,完成其工作。 |
4 | React更新DOM。 | React比较了该组件的UI描述返回当前的应用程序用户界面的描述。它可以有效地巧妙地对DOM进行任何必要的修改,并设置在必要时增加或更新事件处理程序。 |
5 | 事件处理程序启动。 | 一个事件发生了,处理程序运行。该处理程序改变了的state。 |
6 | React调用该组件。 | React知道状态值已经改变,所以必须重新计算。迟来的用户界面。 |
7 | 该组件指定了一个 | 这是一个新版本的处理程序,可以使用新的事件处理程序。 更新的状态值。 |
8 | 该组件返回其用户界面。 | 该组件使用当前的状态值来生成其用户界面并将其返回,完成其工作。 |
9 | React更新DOM。 | React比较了该组件的UI描述这与之前对应用程序用户界面的描述相同。它有效地巧妙地对DOM进行任何必要的修改,并设置在必要时增加或更新事件处理程序。 |
一个组件是否应该管理自己的状态, 一些组件是否应该共享状态, 一些状态是否应该全局共享
React为所有这三种情况提供了机制,而且发布的软件包, 例如Redux、MobX、React Query和Apollo Client,都提供了通过组件外的数据存储来管理状态的方法
在过去,组件是否管理它自己的一些状态决定了组件创建方法; React提供了两种主要的方法:函数组件和类组件
为了定义一个组件,React让你使用两种JavaScript结构:一个函数或一个类。
function MyComponent(props) {
// Maybe work with the props in some way.
// Return the UI incorporating prop values.
}
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
// 在这里设置状态。
// 类组件在构造函数中设置了它们的状态。
};
}
componentDidMount() {
// 执行一个副作用,如加载数据。
// 类的组成部分可以包括在其生命周期的各个阶段的方法。
}
render() {
// 使用props值和状态返回UI。
// 类组件有一个渲染方法,用来返回其用户界面。
}
}
function MyComponent(props) {
// Use local state. // 使用本地状态。
// 使用hooks来管理状态。
const [value, setValue] = useState(initialValue);
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
// Perform side effect. //执行副作用
// 使用hooks来管理副作用
});
// 直接从函数中返回UI
return (
<p>
{value} and {state.message}
</p>
);
}
React团队建议在新项目中使用函数作为组件
组件类型 | 描述 |
---|---|
无状态函数组件 | 一个被传递属性并返回UI的JavaScript函数 |
函数组件 | 一个被传递属性的JavaScript函数,使用hooks来管理状态和执行副作用,以及返回UI |
类组件 | 一个JavaScript类,包括一个返回用户界面的渲染方法。它也可以在其构造函数中设置状态,在其生命周期方法中管理状态和执行副作用。 |
函数组件只是返回其用户界面描述的JavaScript函数。 在编写组件时,开发人员通常使用JSX来指定用户界面。 UI可能取决于传递给函数的属性。有了无状态的函数组件,故事就结束了;它们把属性变成了用户界面。
更为普遍的是,函数组件现在可以包含状态并与副作用一起工作。
类组件是使用JavaScript类语法构建的,从React.Component或React.PureComponent基类扩展而来。 它们有一个构造函数,在那里可以初始化状态,以及React在组件生命周期中调用的方法; 例如,当DOM被更新为最新的组件UI或当传递给组件的属性发生变化时。 他们也有一个渲染方法,返回组件的用户界面描述。 类组件是创建有状态的组件的方法,它可以引起副作用。 带有hooks的函数组件提供一种比类更好的方式来创建有状态的组件和管理副作用
React组件通常将状态转化为用户界面。 当组件代码在这个主要焦点之外执行动作时--也许是从网络上获取数据,如博客文章或股票价格,设置一个在线服务的订阅,或直接与DOM交互以聚焦表单字段或测量元素尺寸--我们将这些动作描述为组件的副作用。
我们希望我们的应用及其组件的行为是可预测的,所以应该确保任何必要的副作用是自由的和可见的。 React提供了useEffect Hooks来帮助我们在我们的功能组件中设置和管理副作用。
React 16包括对核心功能的重写,这为新库功能和方法的稳定推出铺平了道路。
◾ 有状态的功能组件(useState、useReducer)。 ◾ 上下文API(useContext)。 ◾ 更清洁的副作用管理(useEffect)。 ◾ 简单但强大的代码重用模式(自定义Hooks)。 ◾ 代码拆分(lazy) ◾ 更快的初始加载和智能渲染(Concurrent Mode 并发模式-实验)。 ◾ 对加载状态有更好的反馈(Suspense、useTransition)。 ◾ 强大的调试、检查和分析功能(开发工具和分析器)。 ◾ 有针对性的错误处理(错误界限)。
以use开头的词-useState, useRedcer, useContext, useEffect, and useTransition-是React Hooks的例子。 它们是你可以从React功能组件中调用的函数,并且可以钩住React的关键功能:状态、生命周期和上下文。 React Hooks让你把状态添加到功能组件中,干净地封装副作用,并在你的项目中重用代码。 通过使用Hooks,你不再需要类,以一种优雅的方式减少和整合你的代码。
并发模式和Suspense提供了一种手段,可以更慎重地考虑何时加载代码、数据和资源,并以一种协调的方式处理加载状态和回退content,如旋转器。 其目的是在应用程序加载和状态变化时改善用户体验,并改善开发人员的体验,使其更容易钩住这些新行为。 React可以暂停昂贵但不紧急的组件的渲染,并切换到紧急任务,如对用户交互的反应,以保持你的应用程序的响应,并为用户的生产力平滑感知路径。
https://reactjs.org 上的React文档是一个很好的资源,它提供了清晰的、结构良好的关于该库的理念、API和推荐使用的解释,以及团队的博客文章和实时代码示例的链接、关于新功能的会议讲座和其他React相关资源。 特别是,看看关于React 17(https://reactjs.org/blog/2020/10/20/react-v17.html)的博文。React的下一个主要版本于2020年10月发布,但不包含面向开发者的新功能。 相反,它包括使React应用程序更容易逐步升级的变化,以及并发模式及其API的进一步实验性开发。
React的核心优势之一是它如何将应用和组件的状态与用户界面同步。 当状态发生变化时,基于用户的交互或来自系统或网络的数据更新,React智能而有效地计算出在浏览器中的DOM或在其他环境中的UI应该做哪些变化。
状态可以是一个组件的本地状态,也可以是树中更高一级的组件,并通过属性在兄弟姐妹之间共享,或者是全局的,并通过React的Context机制或高阶组件(将一个组件作为参数并返回一个新的组件的函数,该组件包裹了传入的组件,但有额外的功能)来访问。 为了让一个组件拥有状态,过去你需要使用一个由React.Component扩展的JavaScript类的组件。 现在,通过React Hooks,你可以将状态添加到功能组件中。
与类相比,带有Hooks的函数组件鼓励更干净、更精简的代码,易于测试、维护和重复使用。 函数组件是一个JavaScript 函数,返回其用户界面的描述。 该用户界面取决于传入的属性和由该组件管理或访问的状态。
◾ 它加载自己的问题数据--包括初始数据和用户选择新问题集时的新问题。 ◾ 它订阅了一个用户服务--该服务提供当前在线的其他测验用户的最新信息,因此用户可以加入一个团队或挑战对手。
在JavaScript中,函数可以包含其他函数,所以组件可以包含事件处理程序,对用户与用户界面的交互作出反应, 例如,显示、隐藏或提交答案,或移动到下一个问题。
在该组件中,你可以 很容易地封装副作用,比如获取问题数据或订阅用户服务。 你也可以为这些副作用包括清理代码,以取消任何未完成的数据获取和取消对用户服务的订阅。
使用Hooks,这些功能甚至可以被提取到组件外的自己的函数中,以备重复使用或共享。
◾ 减去代码 ◾ 更好的代码组织,相关的代码与任何清理的代码保存在一起 ◾ 将特征提取为可重复使用和共享的外部函数 ◾ 更容易测试的组件 ◾ 不需要在类的构造函数中调用super()。 ◾ 不需要用this和绑定的处理程序工作 ◾ 更简单的生命周期模型 ◾ 处理程序、副作用函数和返回的用户界面范围内的本地状态
这个列表中的所有项目都有利于编写更容易理解的代码,因此更容易工作和维护
在类组件中,状态被设置在构造函数中,事件处理程序被绑定到this,而副作用代码被分割到多个生命周期方法中(componentDidMount, componentWillUnmount, componentWillUpdate,等等)。 在一个生命周期方法中,与不同效果和功能有关的代码并排在一起是很常见的。
带有Hooks的函数组件不再需要所有的生命周期方法,因为效果可以被封装到Hooks中。 这种变化产生了更整洁、更有组织的代码
一个代码分散在生命周期方法中的类组件,和一个具有相同功能但代码较少、组织较好的函数组件。
图中的Quiz函数组件所示。代码的组织更加合理,两个副作用被分开,它们的代码被整合在每个效果的一个地方。 改进后的组织结构使我们更容易找到某个特定效果的代码,看到一个组件是如何工作的,并在将来维护它。 把一个功能或效果的代码放在一个地方,使得它更容易被提取到它自己的一个外部函数中,这就是我们接下来要讨论的
带Hooks的函数组件鼓励你把相关的副作用逻辑放在一个地方。 如果副作用是许多组件都需要的功能,你可以进一步组织,将代码提取到自己的外部函数中;你可以创建所谓的自定义Hooks。
下图显示如何将Quiz功能组件的问题加载和用户服务订阅任务转移到他们自己的自定义Hooks中。 任何仅用于这些任务的状态都可以被移到相应的Hooks中。 获取问题数据和订阅用户服务的代码可以被提取到自定义Hooks中。随之而来的状态也可以由Hooks管理。
你可以在许多组件中使用它,与你的团队分享它,或者发布它供其他人使用
你可以将代码提取到自定义Hooks中,以便重复使用和共享。 Quiz组件同时调用useUsers和useFetchHooks。 聊天组件调用useUsersHooks。
新的超轻量级功能组件使用 useUsers 自定义Hooks和useFetch自定义Hooks来执行用户服务子描述和问题获取的任务,以前它是自己执行的。 但现在第二个组件,聊天,也使用了useUsers自定义Hooks。 Hooks使这种功能共享在React中变得更加容易; 自定义Hooks可以被导入并在你的应用程序组合中需要的地方使用。
每个自定义Hooks都可以维护它自己的状态,不管它需要什么来履行它的职责。 因为Hooks只是函数,如果组件需要访问Hooks的任何状态,Hooks可以在其返回值中包含这些状态。 例如,一个获取指定ID的用户信息的自定义Hooks可以将获取的用户数据存储在本地,但将其返回给任何调用该Hooks的组件。 每个Hooks调用都封装了自己的状态,就像其他函数一样。
要想了解程序员将各种常见任务轻松抽象为自定义Hooks的情况,请看一下useHooks网站:https://usehooks.com
◾ useRouter-封装React Router提供的新Hooks。 ◾ useAuth-使任何组件能够获得当前的认证状态,并在其发生变化时重新渲染。 ◾ useEventListener-抽象了向组件添加和删除事件监听器的过程。 ◾ useMedia-让你在组件逻辑中轻松使用媒体查询。
在推出自己的Hooks之前,值得在useHooks等网站或npm等软件包库中研究是否存在适合你使用情况的Hooks。 如果你已经使用库或框架来处理数据获取或状态管理等常见情况,请检查最新版本,看看它们是否引入了Hooks,以使其工作更容易。
跨组件共享功能并不新鲜,它已经是React开发的一个重要部分了。 Hooks提供了一种更简洁的共享代码和钩住功能的方式,而不是老式的高阶组件和渲染props的方法,后者往往会导致高度嵌套的代码("包装地狱")和错误的代码层次结构。
◾ 用于页面导航的React Router ◾ Redux作为一个应用程序的数据存储 ◾ 用于动画的React Spring
React Router提供了一些组件来帮助开发者管理他们应用中页面之间的导航。 它的自定义Hooks使得访问导航中涉及的常见对象变得容易:useHistory、useLocation、useParams和useRouteMatch。 例如,useParams可以访问页面的URL中匹配的任何参数。
URL: /quiz/:title/:qnum
代码: tsx const {title, qnum} = useParams()
。
对于一些应用程序,单独的状态存储可能是合适的。 Redux是一个流行的创建这种存储的库,它经常通过React Redux库与React结合。从7.1版开始,React Redux提供了一些使与存储的交互更容易的方法:useSelector、useDispatch和useStore。 例如,useDispatch可以让你调度一个动作来更新商店中的状态。 假设你有一个为测验建立问题集的应用程序,你想添加一个问题。
const dispatch = useDispatch()。 dispatch({type: "add question", payload: /* question data */})。
新的自定义Hooks消除了一些与连接React应用程序和Redux store有关的模板代码。 React也有一个内置的Hooks,useReducer,它可能提供一个更简单的模型,用于调度更新状态的动作,并在某些情况下消除对Redux的感知需求。
React Spring是一个基于Spring的动画库,目前提供了四个Hooks来访问其功能:useSpring、useSprings、useTrail、useTransition和useChain。 例如,要在两个值之间制作动画,你可以选择useSpring。
const props = useSpring({opacity: 1, from: {opacity: 0}})。
React Hooks使库作者更容易为开发者提供更简单的API,而不是用潜在的深度嵌套的虚假组件层次结构来扰乱他们的代码。 同样,其他一些新的React功能,如并发模式和暂停(Suspense),使库作者和应用程序开发人员能够在他们的代码中更好地管理同步进程,并提供更顺畅、更灵敏的用户体验。
应用程序可能需要加载大量的代码,获取大量的数据,并试图处理这些数据以提供用户所需的信息
为16和17版本重写React的很大一部分动机是建立架构,以应对用户界面的多种需求,因为它在用户继续与应用程序互动时加载和操作数据。 并发模式是这个新架构的核心部分,而Suspense组件自然适合新模式
假设你有一个应用程序,在一个长长的列表中显示产品,并有一个文本框,用户在其中键入以过滤该列表。 你的应用程序在用户输入时更新列表。每一次点击触发代码重新过滤列表,要求React将更新的列表组件绘制到屏幕上。 昂贵的过滤过程、重新计算和更新用户界面占用了处理时间,降低了文本框的响应速度。 用户的体验是一个滞后的、缓慢的文本框,在用户输入时不显示文本。
如果没有并发模式,像按键输入这样的互动就会被长期运行的更新所阻断
如果应用程序能够优先考虑文本框的更新,并保持用户体验的顺畅,暂停(Suspense)和重新启动围绕打字的过滤任务,那不是很好吗?向 "并发模式 "问好吧!
通过并发模式,React可以以更精细的方式安排任务,暂停(Suspense)其构建元素的工作,检查差异,并更新DOM的先前状态变化,以确保它响应用户的互动, 例如。在前面的过滤应用程序的例子中,React可以暂停(Suspense)渲染过滤后的列表,以确保用户输入的文本出现在文本框中。
那么,并发模式是如何实现这种魔力的呢?新的React架构将其任务分解成更小的工作单元,为浏览器或操作系统提供定期的点,以通知应用程序,用户正试图与它互动。 然后,React的调度器可以根据每个任务的优先级来决定要做什么工作。 对组件树的某一部分进行编辑和提交修改,可以暂停(Suspense)或取消。
不仅仅是用户交互可以从这种智能调度中受益;对传入数据的响应、懒加载的组件或媒体,或其他异步进程也可以享受更顺畅的用户界面升级。 React可以继续显示一个完全交互式的现有用户界面(而不是一个加载器),同时它在内存中渲染更新状态的用户界面,当足够多时就切换到新的用户界面。 并发模式启用了几个新的Hooks,useTransition和useDeferredValue,以改善用户体验,使从一个视图到另一个视图或从一个状态到另一个状态的变化更加平滑。 它还与 "暂停(Suspense)"相辅相成,后者既是一个用于渲染回退内容的组件,也是一个用于指定组件正在等待某些东西的机制,比如加载数据。
React应用程序是由分层树中的组件构建的。 为了在屏幕上显示你的应用程序的当前状态(例如使用DOM),React会遍历你的组件并在内存中创建元素树,即对预期UI的描述。 它将最新的树与之前的树进行比较,并智能地决定需要进行哪些DOM更新来实现预期的用户界面。 并发模式让React暂停(Suspense)处理元素树的部分内容,要么是为了处理更优先的任务,要么是因为当前组件还没有准备好被处理。
为与Suspense一起工作而构建的组件现在可以暂停(Suspense),如果它们还没有准备好返回它们的用户界面(记住,组件要么是函数,要么有一个渲染方法,并将属性和状态转换成用户界面)。 它们可能在等待组件代码或资源或数据的加载,只是还没有完全描述它们的用户界面所需的信息。 React可以暂停(Suspense)对一个暂停(Suspense)的组件的处理,继续遍历元素树。 但这在屏幕上看起来如何呢?你的用户界面上会有一个洞吗?
除了指定组件暂停(Suspense)的机制外,React还提供了一个Suspense组件,你可以用它来填补暂停(Suspense)的组件在用户界面上留下的漏洞。 用Suspense组件包裹你的用户界面部分,并使用它们的回退属性,让React知道在一个或多个被包裹的组件暂停(Suspense)时要显示什么内容。
<Suspense fallback={<MySpinner />}> <MyFirstComponent /> <MySecondComponent /> </Suspense>;
Suspense允许开发者有意管理多个组件的加载状态,可以显示单个组件、组件组或整个应用的回退。 它为库作者提供了一种机制,以更新他们的API,使之与Suspense组件协同工作,因此他们的异步功能可以充分利用Suspense提供的加载状态管理。
下图是对React工作的一个非常基本的说明: 它应该使用当前的状态来渲染UI。 如果状态发生变化,React应该重新渲染用户界面。 图中显示了一个友好信息中的名字。当名字的值发生变化时,React会更新UI,在其消息中显示新的名字。 我们通常希望状态和UI是同步的(尽管我们可能会选择在状态转换期间延迟同步--例如在获取最新数据时)
当你改变一个组件的值时,React应该更新UI。
React提供了少量的函数,或者说Hooks,使它能够跟踪你的组件中的值,并保持状态和UI同步。 对于单个值,React给我们提供了useState Hooks 我们将看看如何调用这个Hooks,它返回什么,以及如何使用它来更新状态,触发React更新UI。
组件通常需要不止一个状态来完成它们的工作,所以我们将看到如何多次调用useState来处理多个值。 这不仅仅是一个记录useState API的问题(你可以去看React的官方文档)。
当调用useStateHooks时,它同时返回最新的状态值和一个用于更新该值的函数。 使用updater函数可以让React保持在循环中,并让它做它的syncy业务。
你的有趣但专业的公司拥有众多可供员工预订的资源:会议室、视听设备、技术员时间、桌上足球,甚至还有派对用品。 有一天,老板要求你为公司建立一个应用程序的骨架 网络,让工作人员预订资源。 该应用程序应该有三个页面,分别是Book- ings、Bookables和Users。 (从技术上讲,这是一个单页面的应用程序,页面实际上是组件,但我们将继续称它们为页面,因为从用户的角度来看,他们是在页面之间切换。
npx create-react-app react-hooks-in-action
npm i history react-router-dom@next
npm i react-icons
预订应用程序有三个页面。预订,可预订,和用户
◾ /src/components/App.js--包含所有其他组件的根组件。 ◾ /src/index.js--导入App组件并将其渲染到index.html页面的文件
当我们向应用程序添加功能时,我们使用组件来封装这些功能,并展示使用Hooks提供的技术。 我们把组件放在与它们所在的页面相关的文件夹中。 在组件文件夹中创建三个新的文件夹,命名为Bookables、Bookings和Users。 对于骨架应用程序,创建三个结构相同的页面,就像下面列表中的页面。 叫它们BookablesPage、BookingsPage和UsersPage。
应用程序需要几种类型的数据,包括用户、可预订数和预订数。 我们首先从一个单一的JavaScript对象符号(JSON)文件中导入所有的数据
src/static.json
{ "bookables": [ { "id": 1, "group": "Rooms", "title": "Meeting Room", "notes": "The one with the big table and interactive screen. Seats 12. See Colin if you need the tea and coffee trolley.", "sessions": [ 1, 2, 3 ], "days": [ 1, 2, 3, 4, 5, 0 ] }, { "id": 2, "group": "Rooms", "title": "Lecture Hall", "notes": "For more formal 'sage-on-the-stage' presentations. Seats 100. See Sandra for help with AV setup.", "sessions": [ 1, 3, 4 ], "days": [ 0, 1, 2, 3, 4 ] }, { "id": 3, "group": "Rooms", "title": "Games Room", "notes": "Table tennis, table football, pinball! There's also a selection of board games. Please tidy up!", "sessions": [ 0, 2, 4 ], "days": [ 0, 2, 3, 4, 5, 6 ] }, { "id": 4, "group": "Rooms", "title": "Lounge", "notes": "A relaxing place to hang out. Ideal for bean bag wranglers and sofa surfers. Help yourself to a beer after hours.", "sessions": [ 0, 1, 2, 3, 4 ], "days": [ 0, 1, 2, 3, 4, 5, 6 ] }, { "id": 5, "group": "Kit", "title": "Projector", "notes": "Portable but powerful. Keep it with the case. Be careful, it gets quite hot after a while!", "sessions": [ 1, 2, 3, 4 ], "days": [ 0, 2, 3, 4, 5 ] }, { "id": 6, "group": "Kit", "title": "Wireless mics", "notes": "Really handy but don't forget to switch them off when you pop out of the room.", "sessions": [ 1, 3, 4 ], "days": [ 0, 2, 3, 4, 5, 6 ] } ], "users": [ { "id": 1, "name": "Mark", "img": "user1.png", "title": "Envisioning Sculptor", "notes": "With the company for 15 years, Mark has consistently sculpted innovative and compelling narratives for enforwarding the mutual ethos of all stakeholders." }, { "id": 2, "name": "Simon", "img": "user2.png", "title": "Outreach Samurai", "notes": "Simon wrangles social networks, elegantly employing bleeding-katana psycho-tools to leverage what he likes to call 'News Technology'." }, { "id": 3, "name": "Clarisse", "img": "user3.png", "title": "Quantum Explorator", "notes": "Surfing a higher plane of understanding, Clarisse value-adds the latest 'beyond fullstack' platforms, libraries and universes, collapsing realities to find the one truth." }, { "id": 4, "name": "Sanjiv", "img": "user4.png", "title": "Devil's Advocate Advocate", "notes": "Sanjiv lives your life to better understand your power-tantrums and architect empathic growth journeys that break the shell and distribute the yoke company-wide." } ], "bookings": [], "sessions": [ "Breakfast", "Morning", "Lunch", "Afternoon", "Evening" ], "days": [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ] }
src/index.js
import ReactDOM from 'react-dom'; import App from './components/App.js';
ReactDOM.render(
>`src/App.css`
```css
:root {
--page-bg: #eee7be;
--primary: #173f5f;
--button-bg: #20639b;
--error: #ed553b;
--secondary: #3caea3;
--dark-text: #444;
--light-text: #fff;
}
body {
background: var(--page-bg);
margin: 0;
font-size: 16px;
font-family: sans-serif;
text-align: center;
}
header {
margin: 0;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--primary);
}
header ul {
list-style: none;
padding: 0;
margin: 0;
}
header li {
display: inline-block;
margin: 0 1rem 0 0;
padding: 0;
}
header a.btn {
width: 9rem;
}
.btn {
background: var(--button-bg);
color: var(--light-text);
border: none;
border-radius: 1.5rem;
padding: 0.5rem 1.5rem;
margin-left: 0.5rem;
font-size: 1rem;
transition: all 0.4s ease;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-sizing: border-box;
}
.btn:first-child {
margin-left: 0;
}
a.btn {
text-decoration: none;
background-color: var(--primary);
}
.btn-delete {
background: var(--error);
color: white;
}
.btn > svg:first-child {
margin-right: 0.4rem;
}
.btn > svg:last-child {
margin-left: 0.4rem;
}
.btn:hover {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.btn-header {
background: var(--primary);
border: 1px solid var(--button-bg);
}
.btn-header:hover {
border-color: var(--light-text);
}
main {
margin: 40px auto;
display: grid;
}
.bookings-page {
grid-template-columns: 1fr 5fr;
grid-column-gap: 20px;
margin: 40px 20px;
}
.bookables-page,
.users-page {
max-width: 70em;
grid-template-columns: 1fr 3fr;
grid-column-gap: 40px;
}
.items-list-nav {
margin: 1rem auto;
padding: 0;
list-style: none;
width: 100%;
}
.items-list-nav > li {
margin: 0 0 1rem;
padding: 0;
}
.items-list-nav .btn {
width: 100%;
background: var(--light-text);
color: var(--dark-text);
border: 1px solid rgba(0, 0, 0, 0.3);
transition: all 0.4s ease;
}
.items-list-nav a {
display: block;
padding: 1em 2em;
text-decoration: none;
}
.items-list-nav .btn:hover {
cursor: pointer;
border: 1px solid var(--primary);
}
.items-list-nav .selected .btn {
background: var(--primary);
color: var(--light-text);
}
.item {
background: var(--secondary);
color: var(--light-text);
padding: 2rem;
text-align: left;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--primary);
border-bottom: 8px solid white;
margin: -2rem -2rem 1rem;
padding: 0 1rem;
}
.item-header .controls {
font-weight: normal;
}
.item h3 {
border-bottom: 1px solid white;
margin: 1em 0 0.5em;
padding-bottom: 3px;
}
.bookable-availability {
display: flex;
padding: 0 1em;
}
.bookable-availability ul {
list-style: none;
margin: 0;
padding: 0;
width: 40%;
}
.bookable-availability li {
margin: 0.4em 0;
}
.item-details .bookable-availability ul {
list-style: square;
}
src/components/App.js
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import "../App.css";
import {FaCalendarAlt, FaDoorOpen, FaUsers} from "react-icons/fa";
import BookablesPage from "./Bookables/BookablesPage"; import BookingsPage from "./Bookings/BookingsPage"; import UsersPage from "./Users/UsersPage"; import UserPicker from "./Users/UserPicker";
export default function App () { return (
); }
>`src/components/Bookables/BookablesPage.js`
```js
import BookablesList from "./BookablesList";
export default function BookablesPage () {
return (
<main className="bookables-page">
<BookablesList/>
</main>
);
}
src/components/Bookables/BookablesList.js
import {useState, Fragment} from 'react'; import {bookables, sessions, days} from "../../static.json"; import {FaArrowRight} from "react-icons/fa";
export default function BookablesList () { const [group, setGroup] = useState("Kit"); const bookablesInGroup = bookables.filter(b => b.group === group); const [bookableIndex, setBookableIndex] = useState(0); const groups = [...new Set(bookables.map(b => b.group))];
const bookable = bookablesInGroup[bookableIndex];
const [hasDetails, setHasDetails] = useState(false);
function nextBookable () { setBookableIndex(i => (i + 1) % bookablesInGroup.length); }
return (
{bookable.notes}
{hasDetails && (); }
>`src/components/Bookings/BookingsPage.js`
```js
export default function BookingsPage () {
return (
<main className="bookings-page">
<p>Bookings!</p>
</main>
);
}
src/components/Users/UserPicker.js
import {users} from "../../static.json";
export default function UserPicker () { return (
); }
>`src/components/Users/UsersList.js`
```js
import {useState, Fragment} from 'react';
import {users} from "../../static.json";
export default function UsersList () {
const [userIndex, setUserIndex] = useState(0);
const user = users[userIndex];
return (
<Fragment>
<ul className="users items-list-nav">
{users.map((u, i) => (
<li
key={u.id}
className={i === userIndex ? "selected" : null}
>
<button
className="btn"
onClick={() => setUserIndex(i)}
>
{u.name}
</button>
</li>
))}
</ul>
{user && (
<div className="item user">
<div className="item-header">
<h2>{user.name}</h2>
</div>
<div className="user-details">
<h3>{user.title}</h3>
<p>{user.notes}</p>
</div>
</div>
)}
</Fragment>
);
}
src/components/Users/UsersPage.js
import UsersList from "./UsersList";
export default function UsersPage () { return (
); }
## 用useState来记录、使用和设置数值
>React应用程序会照顾到某种状态:在用户界面上显示的值,或者帮助管理显示的内容。
>例如,状态可能包括论坛上的帖子,这些帖子的评论,以及评论是否被显示。
>当用户与应用程序互动时,他们会改变其状态。
>他们可以加载更多的帖子,切换评论是否可见,或者添加他们自己的评论。
>React的作用是确保状态和UI同步。
>当状态改变时,React需要运行使用该状态的组件。这些组件通过使用最新的状态值来返回它们的用户界面。
>React将返回的UI与现有的UI进行比较,并根据需要有效地更新DOM。
>有些状态是在整个应用程序中共享的,有些是由几个组件共享的,有些是由一个组件自己本地管理的。
>如果组件只是函数,它们如何在不同的渲染中坚持自己的状态?它们的变量在执行完毕后不会丢失吗?React又是如何知道这些变量何时发生变化的?如果React试图忠实地匹配状态和用户界面,它肯定需要知道状态的变化,对吗?
>最简单的方法是useState Hooks,它可以在调用你的组件时保持状态,并在你改变组件的状态时保持React的循环。
>useState Hooks是一个可以让React帮助管理状态值的函数。
>当你调用useState Hooks时,它同时返回最新的状态值和一个用于更新该值的函数。使用updater函数可以让React保持在循环中,并让它做它的syncy业务。
>useState函数返回一个有两个元素的数组:一个值和一个更新器函数。
![image](https://user-images.githubusercontent.com/30850497/134171409-0ee9ad68-c033-451d-931f-f3b152cd6056.png)
>你可以把返回的数组分配给一个变量,然后按索引单独访问这两个元素,像这样。
```tsx
const selectedRoomArray = useState();
const selectedRoom = selectedRoomArray[0];
const setSelectedRoom = selectedRoomArray[1];
useState函数返回一个数组。
第一个元素是价值。 第二个元素是用于更新数值的函数。
但更常见的是使用数组析构,并在一个步骤中把返回的元素分配给变量。
const [selectedRoom, setSelectedRoom] = useState();
数组重构让我们把数组中的元素分配给我们选择的变量。 selectedRoom和setSelectedRoom的名字是任意的,也是我们的选择,尽管第二个元素的变量名,即更新函数,通常是以set开头。 下面的例子也可以这样做。
const [myRoom, updateMyRoom] = useState();
如果你想为该变量设置一个初始值,请将初始值作为参数传给useState函数。 当React第一次运行你的组件时,useState会像往常一样返回双元素数组,但会把初始值分配给数组的第一个元素
有了useState,React就开始监听了。它正在履行它的承诺,使状态与用户界面保持同步
有时一个组件可能需要做一些工作来计算一个状态的初始值。 也许这个组件从传统的存储系统中得到了一串纠缠在一起的数据,并需要从破碎的结中提取有用的信息。 解开这串数据可能需要一段时间,而你只想做一次工作。
function untangle(aFrayedKnot) {
// perform expensive untangling manoeuvers
return nugget;
}
function ShinyComponent({ tangledWeb }) {
const [shiny, setShiny] = useState(untangle(tangledWeb));
// use shiny value and allow new shiny values to be set
}
每当ShinyComponent运行时,也许是为了响应设置另一块状态,昂贵的untangle函数也会运行。 但是useState只在第一次调用时使用其初始值参数。在第一次调用之后,它不会使用untangle返回的值。 一次又一次地运行昂贵的untangle函数是一种时间的浪费。 幸运的是,useStateHooks接受一个函数作为它的参数,一个lazy的初始状态
可以向useState传递一个函数作为初始值。React将使用该函数的返回值作为初始值。 React只在组件第一次被渲染时执行该函数。它使用该函数的返回值作为初始状态。
function ShinyString({ tangledWeb }) {
const [shiny, setShiny] = useState(() => untangle(tangledWeb));
// use shiny value and allow new shiny values to be set
}
如果你需要进行昂贵的工作来生成一块状态的初始值,请使用lazy的初始状态。
用户在BookablesList组件中循环浏览可预订的内容
setBookableIndex(i => (i + 1) % bookablesInGroup.length);
通过使用Hooks将我们的状态值管理交给React,我们不只是要求它更新值和触发重读; 我们还允许它有效地安排任何更新发生的时间。 React可以智能地批量更新,忽略多余的更新
传递给更新器函数一个使用旧状态值并返回新状态值的函数
通过传递一个函数,我们确保任何基于旧值的新值都有最新的信息可供使用。
步骤 | 会发生什么? | 讨论 |
---|---|---|
1 | React调用该组件。 | 为了生成页面的UI,React遍历了树状的组件,调用每一个组件。React会把每个组件在JSX中设置为属性的任何props。 |
2 | 该组件第一次调用useState | 该组件将初始值传递给useState函数。React将该useState调用的当前值从该组件。 |
3 | React返回当前的值和一个更新器函数变量供以后使用 | 组件代码将值和更新器函数分配给了第二个变量名称通常以作为一个数组。 与set(例如,value和setValue)。 |
4 | 该组件设置了一个事件处理程序 | 例如,该事件处理程序可以监听用户的点击。事件处理程序将在以后运行时改变状态。React会钩住当它在步骤6中更新DOM时,将处理程序上传到DOM。 |
5 | 该组件返回其用户界面。 | 该组件使用当前的状态值来生成其用户界面并将其返回,完成其工作。 |
6 | React更新DOM。 | React用任何需要的变化来更新DOM。 |
7 | 该事件处理程序调用更新器的功能 | 一个事件发生了,处理程序运行。该处理程序使用更新器函数来改变状态值。 |
8 | React更新状态值 | React用更新程序函数传递的值替换状态值。 |
9 | React调用该组件。 | React知道状态值已经改变,所以必须重新计算用户界面。 |
10 | 该组件第二次调用state | 这一次,React将忽略初始值参数。 |
11 | React返回当前状态 | React已更新了状态值。该组件需要最新的值。 |
12 | 该组件设置了一个事件处理程序 | 这是一个新版本的处理程序,可以使用新的更新的状态值。 |
13 | 该组件返回其用户界面。 | 该组件使用当前的状态值来生成其用户界面并将其返回,完成其工作。 |
14 | React更新DOM。 | React将新返回的UI与旧的UI进行比较,并有效地在任何需要改变的情况下,都可以方便地更新DOM。 |
术语 | 描述 |
---|---|
组件 | 一个接受props并返回其用户界面描述的函数。 |
初始值 | 组件将这个值传递给useState。当组件第一次运行时,React将状态值设置为这个初始值。 |
更新器功能 | 组件调用这个函数来更新状态值。 |
事件处理程序 | 一个响应某种事件而运行的函数--例如,用户点击一个可预订的东西。事件处理程序经常调用更新器函数来改变状态。 |
UI | 对构成用户界面的元素的描述。状态值通常包括在用户界面的某个地方。 |
◾ 当你想让React管理一个组件的值时,调用useState Hooks。 它返回一个有两个元素的数组:状态值和一个更新器函数。 如果需要,你可以传入一个初始值。
const [value, setValue] = useState(initialValue)。
◾ 如果你需要进行昂贵的计算来生成初始状态,可以在一个函数中把它传递给useState。 React只有在第一次调用组件的时候才会运行这个函数来获得这个lazy的初始状态。
const [value, setValue] = useState(() => { return initialState; });
◾ 使用useState返回的updater函数来设置一个新的值。 新的值将取代旧的值。如果值发生了变化,React将安排一次重新渲染。
setValue(newValue)。
◾ 如果你的状态值是一个对象,当你的更新函数只更新一个子集的属性时,请确保从以前的状态中复制未改变的属性。
setValue({ ...state, property: newValue });
◾ 为了确保你在调用updater函数并根据旧值设置新值时使用的是最新的状态值,请向updater函数传递一个函数作为其参数。React会把最新的状态值分配给函数参数。
setValue(value => { return newValue; });
setValue(state => { return { ...state, property: newValue }; });
>◾ 如果你有多件状态,你可以多次调用useState。React使用调用的顺序来一致地将值和更新器函数分配给正确的变量。
```tsx
const [index, setIndex] = useState(0); // call 1
const [name, setName] = useState("Jamal"); // call 2
const [isPresenting, setIsPresenting] = useState(false); // call 3
◾ 专注于状态和Events将如何更新状态。React将完成其同步状态和用户界面的工作。
function Counter () {
const [count, setCount] = useState(0); ❶
return (
<p>{count} ❷
<button onClick={() => setCount(c => c + 1)}> + </button> ❸
</p>
);
}
当组件开始变得更加复杂,多个事件引起多个状态变化时,跟踪这些变化并确保所有相关的状态值被一起更新变得越来越困难。
当状态值以这种方式相关时,要么相互影响,要么经常一起改变,这有助于将状态更新逻辑移到一个地方,而不是将执行改变的代码分散到事件处理函数中,无论是内联还是单独定义。 React给我们提供了useReducer Hooks来帮助我们管理这种状态更新逻辑的搭配
我们不希望笨重的、不可预知的界面阻碍用户继续完成任务。 如果用户界面不断地把他们的注意力从他们想要的焦点上拉开,或者让他们在没有反馈的情况下等待,或者把他们送进死胡同,他们的思维过程就会被打断,他们的工作就会变得更加困难,他们的一天就会被毁掉。
当你有多块相互关联的状态时,使用一个还原器(reducer)可以使你更容易进行和理解状态变化。
◾ 还原器(reducer)可以帮助你以集中的、定义明确的方式管理状态变化,并有明确的行动来作用于状态。 ◾ 还原器(reducer)使用动作从先前的状态生成一个新的状态,使其更容易指定更复杂的更新,可能涉及多个相互关联的状态。 ◾ React提供了useReducer Hooks,让你的组件指定初始状态,访问当前状态,并调度行动来更新状态和触发重新渲染。 ◾ Dispatching 明确定义的动作使得跟踪状态变化和理解你的组件如何与状态互动以响应不同的事件更加容易。
还原器(reducer)是一个接受状态值和动作值的函数。 它根据传入的两个值生成一个新的状态值。然后它返回新的状态值
一个还原器(reducer)接收一个状态和一个动作并返回一个新的状态。
状态和动作可以是简单的原始值,如数字或字符串,或更复杂的对象。 有了还原器(reducer),你可以把所有更新状态的方法都放在一个地方,这使得管理状态变化更加容易,特别是当一个动作影响到多个状态时。
useStateHooks让我们要求React为我们的组件管理单个值。 通过useReducerHooks,我们可以给React更多的帮助来管理值,给它传递一个reducer和组件的初始状态。 当事件发生在我们的应用程序中时,我们不是给React设置新的值,而是派发一个动作,React在调用组件的最新UI之前,使用reducer中的相应代码来生成一个新的状态。 当调用useReducerHooks时,我们把reducer和初始状态传给它。 Hooks返回当前状态和一个用于分配动作的函数,是一个数组中的两个元素。
用一个reducer和一个初始状态调用useReducer。 它返回当前状态和一个调度函数。使用dispatch函数向reducer分配动作。
对于useReducer,我们使用数组重构,将返回的数组中的两个元素分配给两个变量,其名称由我们选择。 第一个元素,即当前状态,我们把它赋给一个变量,称为state , 第二个元素,即调度函数,我们把它赋给一个变量,称为dispatch。
const [state, dispatch] = useReducer(reducer, initialState);
React在第一次调用组件时,只注意传递给useReducer的参数(在我们的例子中,是reducer和initialState)。 在随后的调用中,它忽略了这些参数,但仍然返回当前状态和reducer的调度函数。
src/components/Bookables/reducer.js
export default function reducer (state, action) { switch (action.type) {
case "SET_GROUP":
return {
...state,
group: action.payload,
bookableIndex: 0
};
case "SET_BOOKABLE":
return {
...state,
bookableIndex: action.payload
};
case "TOGGLE_HAS_DETAILS":
return {
...state,
hasDetails: !state.hasDetails
};
case "NEXT_BOOKABLE":
const count = state.bookables.filter(
b => b.group === state.group
).length;
return {
...state,
bookableIndex: (state.bookableIndex + 1) % count
};
default:
return state;
} }
>`src/components/Bookables/BookablesList.js`
```jsx
import {useReducer, Fragment} from 'react';
import {bookables, sessions, days} from "../../static.json";
import {FaArrowRight} from "react-icons/fa";
import reducer from "./reducer";
const initialState = {
group: "Rooms",
bookableIndex: 0,
hasDetails: true,
bookables
};
export default function BookablesList () {
const [state, dispatch] = useReducer(reducer, initialState);
const {group, bookableIndex, bookables, hasDetails} = state;
const bookablesInGroup = bookables.filter(b => b.group === group);
const bookable = bookablesInGroup[bookableIndex];
const groups = [...new Set(bookables.map(b => b.group))];
function changeGroup (e) {
dispatch({
type: "SET_GROUP",
payload: e.target.value
});
}
function changeBookable (selectedIndex) {
dispatch({
type: "SET_BOOKABLE",
payload: selectedIndex
});
}
function nextBookable () {
dispatch({type: "NEXT_BOOKABLE"});
}
function toggleDetails () {
dispatch({type: "TOGGLE_HAS_DETAILS"});
}
return (
<Fragment>
<div>
<select
value={group}
onChange={changeGroup}
>
{groups.map(g => <option value={g} key={g}>{g}</option>)}
</select>
<ul className="bookables items-list-nav">
{bookablesInGroup.map((b, i) => (
<li
key={b.id}
className={i === bookableIndex ? "selected" : null}
>
<button
className="btn"
onClick={() => changeBookable(i)}
>
{b.title}
</button>
</li>
))}
</ul>
<p>
<button
className="btn"
onClick={nextBookable}
autoFocus
>
<FaArrowRight/>
<span>Next</span>
</button>
</p>
</div>
{bookable && (
<div className="bookable-details">
<div className="item">
<div className="item-header">
<h2>
{bookable.title}
</h2>
<span className="controls">
<label>
<input
type="checkbox"
checked={hasDetails}
onChange={toggleDetails}
/>
Show Details
</label>
</span>
</div>
<p>{bookable.notes}</p>
{hasDetails && (
<div className="item-details">
<h3>Availability</h3>
<div className="bookable-availability">
<ul>
{bookable.days
.sort()
.map(d => <li key={d}>{days[d]}</li>)
}
</ul>
<ul>
{bookable.sessions
.map(s => <li key={s}>{sessions[s]}</li>)
}
</ul>
</div>
</div>
)}
</div>
</div>
)}
</Fragment>
);
}
useReducer返回当前状态和调度函数,我们使用数组析构将它们分配给变量state和dispatch。 列表使用了一个中间状态变量,然后将状态对象解构为单个变量--group、bookableIndex、bookables和hasDetails-- 但你可以直接在数组解构中进行对象解构。
const [{ group, bookableIndex, bookables, hasDetails }, dispatch] = useReducer(
reducer,
initialState
);
如果你的状态很复杂,并且/或者初始状态的设置很昂贵,或者是由你想重用或导入的函数生成的,你可以使用 useReducerHooks的第三个参数
我们可以通过向Hooks传递一个函数来为useState生成初始状态。 类似地,对于useReducer,除了传递一个初始化参数作为第二个参数外,我们还可以传递一个初始化函数作为第三个参数
useReducer的初始化函数使用初始化参数来生成reducer的初始状态
在第一次调用时,状态是初始化函数的返回值。在随后的调用中,它是调用时的状态。
const [state, dispatch] = useReducer(reducer, initArgument, initFunction);
用户将从预订的网格日历中挑选日期和时段 预订页面将包括一个可预订列表、一个预订网格和一个星期选取器
一个可能的界面,用于选择要在预订网格中显示的星期。它包括以下内容。 ◾ 所选星期的开始和结束日期 ◾ 移动到下一个和上一个星期的按钮 ◾ 一个显示包含今天日期的一周的按钮 WeekPicker组件显示了所选星期的开始和结束日期,并有按钮可以在各星期之间进行导航。
预订网格将一次显示一个星期,从周日到周六。 在任何一个特定的日期,显示包含该日期的一周。 让我们创建代表一个星期的对象,在这个星期里有一个特定的日期,以及这个星期的开始和结束的日期。
例如,以2020年4月1日星期三为例。 本周的开始是2020年3月29日星期日,而本周的结束是2020年4月4日星期六。
几个实用的函数:一个是由旧的日期创建一个新的日期,偏移若干天,第二个是生成周的对象
src/utils/date-wrangler.js
export function addDays (date, daysToAdd) { const clone = new Date(date.getTime()); clone.setDate(clone.getDate() + daysToAdd); // 将日期移到指定的天数上。 return clone; }
export function getWeek (forDate, daysOffset = 0) { const date = addDays(forDate, daysOffset); // 立即转移日期 const day = date.getDay(); // 获取新日期的索引数,例如,星期二=2。
return { date, start: addDays(date, -day), // 例如,如果是星期二,就往后移2天 end: addDays(date, 6 - day) // 例如,如果是星期二,就往前移4天。 }; }
>getWeek函数使用JavaScript的Date对象的getDay元来获得指定日期的星期指数:
>周日是0,周一是1, ....,星期六是6.
>为了得到一周的开始,该函数减去与日指数相同的天数:
>对于星期天,它减去0天;对于星期一,它减去1天; . . . ;对于星期六,它减去6天。
>一周的结束时间比一周的开始时间晚6天,所以为了得到一周的结束时间,该函数执行与一周开始时间相同的减法,但也要加上6。
>我们可以使用getWeek函数来为一个给定的日期生成一个星期对象。
```js
const today = new Date();
const week = getWeek(today);
// 获取包含今天日期的那一周的周对
如果我们想要一个相对于第一个参数中的日期的星期对象,我们也可以指定一个偏移的天数作为第二个参数。
const today = new Date(); const week = getWeek(today, 7);
// 获取包含从今天起一周内的日期的一周对象。
>getWeek函数可以让我们在预订应用程序中从一个星期浏览到另一个星期时生成星期对象。
>让我们在reducer中用它来做这件事。
### 构建reducer来管理组件的日期
>一个reducer帮助我们集中管理WeekPicker组件的状态管理逻辑。
>在一个地方,我们可以看到所有可能的动作以及它们如何更新状态。
>◾ 在当前日期的基础上增加七天,移到下一周。
>◾ 从当前日期减去七天,就可以移动到前一周。
>◾ 通过将当前日期设置为今天的日期,移动到今天。
>◾ 通过将当前日期设置为动作的有效载荷,移动到一个指定的日期。
>对于每个动作,reducer都会返回一个周对象,如上一节所述。
>尽管我们真的只需要跟踪一个日期,但我们还是需要在某些时候生成星期对象,而将星期对象的生成与reducer一起抽象出来对我来说是明智的。
>你可以在下面的列表中看到可能的状态变化是如何转化为reducer的。
>我们把weekReducer.js文件放在Bookings文件夹中。
>`src/components/Bookings/weekReducer.js`
```js
import {getWeek} from "../../utils/date-wrangler";
export default function reducer (state, action) {
switch (action.type) {
case "NEXT_WEEK":
return getWeek(state.date, 7); // 返回一个星期的对象,提前7天。
case "PREV_WEEK":
return getWeek(state.date, -7); // 返回一个星期前的对象为7天。
case "TODAY":
return getWeek(new Date());
case "SET_DATE":
return getWeek(new Date(action.payload)); // 返回一个指定日期的周对象。
default:
throw new Error(`Unknown action type: ${action.type}`)
}
}
reducer导入getWeek函数,为每个状态变化生成星期对象
WeekPicker组件让用户从一个星期浏览到另一个星期,以预订公司的资源。 我们在上一节中设置了reducer,现在是使用它的时候了。 reducer需要一个初始状态,即一个星期对象。 下面的列表显示了我们如何使用getWeek函数,从传递给WeekPicker的日期中生成初始周对象作为props。
src/components/Bookings/WeekPicker.js
import {useReducer} from "react"; import reducer from "./weekReducer"; import {getWeek} from "../../utils/date-wrangler"; import {FaChevronLeft, FaCalendarDay, FaChevronRight} from "react-icons/fa";
export default function WeekPicker ({date}) { const [week, dispatch] = useReducer(reducer, date, getWeek);
return (
{week.start.toDateString()} - {week.end.toDateString()}
); }
>我们对useReducer的调用将指定的日期传递给getWeek函数。
>getWeek函数返回一个被设置为初始状态的星期对象。
>我们将useReducer返回的状态分配给一个叫做week的变量。
```js
const [week, dispatch] = useReducer(reducer, date, getWeek);
除了让我们重复使用getWeek函数来生成状态(在reducer和WeekPicker组件中), 初始化函数(useReducer的第三个参数)还允许我们只运行一次昂贵的状态生成函数,在对useReducer的初始调用中。
src/components/Bookings/BookingsPage.js
import WeekPicker from "./WeekPicker";
export default function BookingsPage () { return (
Bookings!
); }
### 一些useReducer的概念
>
React将我们的数据转换为用户界面。 每个组件都扮演着自己的角色,为整个用户界面做出自己的贡献。 React构建元素树,将其与已经渲染的内容进行比较,并将任何必要的变化提交给DOM。 当状态发生变化时,React会再次经历这个过程来更新用户界面。 React真的很擅长有效地决定什么应该更新并安排任何变化。
然而,有时我们需要我们的组件到达这个数据流过程之外,直接与其他API交互。 一个以某种方式影响到外部世界的行为被称为副作用。
常见的副作用包括以下几种。 ◾ 设置页面标题势在必行 ◾ 与定时器一起工作,如setInterval或setTimeout ◾ 测量DOM中元素的宽度、高度或位置 ◾ 将信息记录到控制台或其他服务 ◾ 设置或获取本地存储中的值 ◾ 获取数据或订阅和取消订阅服务
无论我们的组件想要实现什么,如果它们简单地忽略React并试图盲目地执行它们的任务,那是很危险的。 更好的做法是寻求React的帮助,有效地安排这些副作用,考虑它们应该在什么时候和多长时间运行一次,甚至在React完成渲染每个组件并将变化提交到屏幕上的工作时。
React提供了useEffectHooks,这样我们就可以更好地控制副作用,并把它们整合到我们组件的生命周期中
如何以不会失控的方式设置副作用。 我们将探讨这四种情况。 ◾ 每次渲染后运行的副作用 ◾ 只在组件安装时运行一个效果 ◾ 通过返回一个函数来清理副作用 ◾ 通过指定依赖关系来控制一个效果的运行时间
假设你想在浏览器中为页面的标题添加一个随机的问候语。 点击你的友好组件的Say Hi按钮就会产生一个新的问候语并更新标题。 图4中显示了三个这样的问候语。
文档的标题并不是文档主体的一部分,也没有被React渲染。 但标题可以通过窗口的文档属性访问。 你可以像这样设置标题。
document.title = "Bonjour";
以这种方式接触到浏览器的API被认为是一种副作用。 我们可以通过将代码包裹在 useEffect Hooks 中来明确这一点。
useEffect(() => { document.title = "Bonjour"; });
import React, { useState, useEffect } from "react"; // 导入 useEffect Hooks。
export default function SayHello() { const greetings = ["Hello", "Ciao", "Hola", "こんにちは"];
const [index, setIndex] = useState(0);
useEffect(() => { // 给useEffectHooks传递一个函数,即副作用 document.title = greetings[index]; // 从副作用里面更新浏览器的标题 });
function updateGreeting() { setIndex(Math.floor(Math.random() * greetings.length)); }
return ;
>该组件使用一个随机生成的索引,从一个数组中挑选一个问候语。
>每当updateGreeting函数调用setIndex时,React就会重新渲染这个组件(除非索引值没有变化)。
>React在每次渲染后,一旦浏览器重新绘制了页面,就会在useEffectHooks中运行effect函数,根据需要更新页面标题。
>注意,副作用函数可以访问组件中的变量,因为它在同一个范围内。
>它使用了问候语和索引变量的值。
>图显示了你如何将一个副作用函数作为第一个参数传递给 useEffect Hooks。
>将一个副作用函数传递给 useEffect Hooks
![image](https://user-images.githubusercontent.com/30850497/134540344-2828cdf2-73e1-48d8-87ae-fe73c3c3b2c3.png)
>当你以这种方式调用useEffectHooks时,没有第二个参数,React会在每次渲染后运行这个副作用。
>但如果你想只在组件加载时才运行一个副作用呢?
### 只有当一个组件挂载mounts时,才产生副作用。
>假设你想使用浏览器窗口的宽度和高度,也许是为了做一个有趣的动画副作用。
>为了测试读取尺寸,你创建了一个显示当前宽度和高度的小组件,就像图所示。
>显示一个窗口调整后的宽度和高度
![image](https://user-images.githubusercontent.com/30850497/134540916-a88b7902-9750-45f7-b721-f1b0598836e6.png)
>下面的列表显示了该组件的代码。
>它要读取窗口对象的innerWidth和innerHeight属性,所以我们再一次使用 useEffect Hooks。
```tsx
import React, { useState, useEffect } from "react";
export default function WindowSize () {
const [size, setSize] = useState(getSize());
function getSize () { // 定义一个返回窗口维度的函数。
return {
width: window.innerWidth, // 从窗口对象中读取尺寸
height: window.innerHeight
};
}
useEffect(() => {
function handleResize () {
setSize(getSize()); // 更新状态,触发一次重新渲染。
}
window.addEventListener('resize', handleResize); // 为调整大小事件注册一个事件监听器
}, []); // 将一个空数组作为依赖项参数
return <p>Width: {size.width}, Height: {size.height}</p>
}
在useEffect中,该组件为调整大小事件注册了一个事件监听器。
在useEffect中,该组件为调整大小事件注册了一个事件监听器。 每当用户调整浏览器的大小时,处理程序handleResize就会通过调用setSize来更新状态的新尺寸。
通过调用updater函数,该组件启动了一次重新渲染。 我们不希望每次React调用该组件时都要重新注册事件监听器。 那么,我们如何防止每次渲染后运行副作用呢? 诀窍在于作为useEffect的第二个参数传递的空数组 传递一个空的依赖关系数组会导致副作用函数在组件挂载时只运行一次
第二个参数是针对依赖性列表的。 React通过检查列表中的值在组件上次调用该副作用后是否发生了变化来决定是否运行该副作用。 通过将列表设置为空数组,列表将永远不会改变,导致副作用只运行一次,即在组件第一次挂载时。
但等一下;警钟应该响起。 我们注册了一个事件监听器......我们不应该让这个监听器听着,像僵尸一样在墓室里晃荡,直到永远。 我们需要进行一些清理工作,取消对监听器的注册。让我们来处理这些僵尸
当我们设置诸如订阅、数据请求、计时器和事件监听器等长期运行的副作用时,我们必须小心,不要让事情变得一团糟
useEffect Hooks包含了一个简单的机制来清理我们的副作用。 只要从副作用中返回一个函数。当需要清理的时候,React会运行返回的函数。
下面的列表更新了我们的窗口测量应用程序,以便在不再需要时删除调整大小的监听器。
import React, { useState, useEffect } from "react";
export default function WindowSize() { const [size, setSize] = useState(getSize());
function getSize() { return { width: window.innerWidth, height: window.innerHeight, }; }
useEffect(() => { function handleResize() { setSize(getSize()); }
window.addEventListener("resize", handleResize);
// 从清理函数中引用handleResize函数 return () => window.removeEventListener("resize", handleResize); // 从副作用中返回一个清理函数。 }, []);
return (
Width: {size.width}, Height: {size.height}
); }
>因为代码将一个空数组作为第二个参数传给了useEffect,所以该副作用只运行一次。
>当副作用运行时,它注册了一个事件监听器。
>React保留了副作用返回的函数,并在清理的时候调用它。
>返回的函数删除了事件监听器。我们的内存不会泄漏。
>因为清理函数被定义在副作用中,它可以访问副作用范围内的变量。
>清理函数可以删除handleResize函数,因为handleResize也被定义在同一个副作用中
>从副作用中返回一个函数。React将运行该函数来清理副作用。
![image](https://user-images.githubusercontent.com/30850497/134543915-bd2d0f32-34fb-47e3-a84f-7f57b9ef654a.png)
>在React Hooks方法中,组件和Hooks只是函数,它很好地利用了JavaScript的固有特性,而不是过多地依赖一层在概念上脱离了底层语言的特异的API。
>然而,这确实意味着你需要很好地掌握范围和闭包,以最好地理解把你的变量和函数放在哪里。
>React在卸载组件的时候会运行清理函数。但这并不是它唯一运行的时间。
>每当组件重新渲染时,如果副作用再次运行,React会在运行副作用函数之前调用清理函数。
>如果多个副作用需要再次运行,React会调用这些副作用的所有清理函数。
>一旦清理完成,React会根据需要重新运行副作用函数。
>我们已经看到了两个极端:只运行一次副作用和每次渲染后运行一次副作用。
>如果我们想对副作用的运行时间有更多的控制呢?还有一种情况需要讨论。让我们来填充这个依赖性数组。
### 通过指定依赖关系来控制副作用的运行时间
>当调用useEffect时,你可以指定一个依赖列表并返回一个清理函数。
![image](https://user-images.githubusercontent.com/30850497/134544879-84a31ec9-439c-4e3e-8eb3-826b6d41e9ce.png)
>每次React调用一个组件时,它都会记录调用useEffect的depen- dency数组中的值。
>如果数组中的值在上次调用后发生了变化,React就会运行这个副作用。
>如果值没有变化,React就跳过这个副作用。
>这当它所依赖的值没有变化时,停止该副作用的运行,因此其任务的结果也将没有变化。
### useEffect hooks的各种用例
调用模式 | 代码模式 | 执行模式
---|---|---
没有第二个参数 | ```tsx useEffect(() => {//执行副作用 })``` | 在每次渲染后运行。
空数组作为第二个参数 | useEffect(() => {//执行副作用}, []) | 只在组件挂载时运行一次。
第二个参数为非空依赖数组 | useEffect(() => {//执行副作用 //使用dep1和dep2 }, [dep1, dep2]) | 每当依赖数组中的一个值发生变化时,就会运行。
返回一个函数 | useEffect(() => {//执行副作用 return () => {/* 清理 */} }, [dep1, dep2]) | React会在组件卸载后和重新运行副作用前运行清理函数。
### 调用useLayout Effect,在browser重绘前运行一个副作用。
>大多数情况下,我们通过调用useEffect来使副作用与状态同步。
>React会在组件渲染和浏览器重新绘制屏幕后运行这些副作用。
>偶尔,我们可能想在React更新DOM后但在浏览器重绘前对状态做进一步的修改。
>例如,我们可能想使用DOM元素的尺寸来以某种方式设置状态。
>在useEffect中进行修改,将向用户展示一个将立即被更新的中间状态。
>我们可以通过调用useLayout Effect Hooks来避免这种状态变化的闪现,而不是useEffect。
>这个Hooks的API与useEffect相同,但在React更新DOM后、浏览器重绘前同步运行。
>如果副作用对状态做了进一步的更新,中间的状态就不会被画到屏幕上。
>一般来说,你不需要useLayoutEffect,但如果你遇到问题(也许是一个元素在不同状态之间闪烁),你可以尝试从useEffect切换到可疑的副作用。
## 请求数据
◾ 创建新的db.json文件
◾ 使用json-server包设置一个JSON服务器
◾ 构建一个组件,从我们的服务器获取数据,显示一个用户列表
◾ 在一个副作用中使用async和await时的注意事项
### 重建新的db.json文件
>把预订、用户和可预订的数据复制到项目根目录下的一个新的db.json文件。
>不要复制static.json中的days和sessions数组;我们将其视为配置信息并继续导入
>db.json
```JSON
{
"booking": [
/*空 */
],
"users": [
/*用户对象*/
],
"bookables": [
/*可预订对象 */
]
}
static.json
{ "days": [ /*日子的名称*/ ], "sessions": [ /*会议名称*/ ] }
在后面的章节中,我们将开始通过发送POST和PUT请求来更新数据库文件。
每当 src 文件夹中的文件发生变化时,create-react-app 开发服务器就会重新启动。
将db.json文件放在src之外,可以避免在我们测试添加新的可预订项目和进行预订时不必要的重启。
到目前为止,我们一直从一个JSON文件(static.json)为BookablesList、UsersList和UserPicker组件导入数据。
import { bookables } from "../../static.json"; import { users } from "../../static.json";
npm install json-server -D
在我们项目的根目录下,用这个命令启动服务器。
json-server --watch db.json --port 3001
你应该能够在
localhost:3001
上查询我们的数据库。通过URL端点使我们的db.json文件的JSON数据在HTTP上可用。
运行json-server时的输出。db.json文件中的属性已经变成了可获取资源的端点。
例如,要获得用户列表,请导航到
localhost:3001/users
要获得ID为1的用户,请导航到localhost:3001/users/1
。两个请求的结果:第一,数组中的用户对象列表,第二,ID为1的用户对象。
从useEffect Hook获取数据
为了从useEffectHooks中引入数据获取,我们更新了UserPicker组件,从JSON数据库中获取用户。
从数据库获取的用户列表
React在渲染后调用副作用函数,所以数据在第一次渲染时是不可用的; 我们设置了一个用户列表作为初始值,并返回另一个UI,一个新的Spinner组件,作为加载状态。
获取用户列表并将其显示在下拉列表中
src/components/Users/UserPicker.js
import { useEffect, useState } from "react"; import Spinner from "../UI/Spinner";
export default function UserPicker() { const [users, setUsers] = useState(null);
useEffect(() => { // 从一个副作用函数内部获取数据 fetch("http://localhost:3001/users") // 通过使用浏览器的fetch API向数据库发出请求。转换返回的JSON字符串 .then((resp) => resp.json()) .then((data) => setUsers(data)); }, []); // 包括一个空的依赖性数组,以便在组件第一次挂载时加载一次数据。
if (users === null) {
// 在用户加载时返回替代的用户界面。
return
return (
); }
>UserPicker代码使用浏览器的fetch API从数据库中获取users列表,
>通过使用resp.json方法将响应解析为JSON,并调用setUse rs将结果更新到本地状态。
>该组件最初渲染一个Spinner占位符(来自 repo中新的/src/components/UI文件夹),
>然后用用户列表替换它。
>如果你想给获取调用增加延迟,以便更好地看到任何加载状态,
>可以用一个延迟标志启动JSON服务器。
>这个Fragment将响应延迟了3000毫秒,即3秒。
```bash
json-server --watch db.json --port 3001 --delay 3000
以上副作用只在组件挂载时运行一次。 我们不期望用户列表发生变化,所以不需要管理列表的重新加载。
下面的列表显示了以这种方式从一个副作用中获取数据的步骤顺序。
1 React调用该组件。 2 useState调用将users变量设置为空。 3 useEffect调用将数据获取的副作用函数与React注册。 4 用户变量是空的,所以该组件返回加载UI。 5 React运行副作用,向服务器请求数据。 6 数据到达后,副作用调用setUsers更新函数,触发重新渲染。 7 React调用该组件。 8 useState调用将用户变量设置为返回的用户列表。 9 useEffect的空依赖数组[]是不变的,所以Hooks调用不会重新注册副作用。 10 用户数组有四个元素(它不是空的),所以该组件返回下拉用户界面。
这种获取数据的方法,即组件在启动数据请求之前进行渲染,被称为渲染时获取。
其他方法有时可以为你的用户提供更顺畅的体验,我们将在第二部分看看其中一些方法。 但是,取决于数据源的复杂性和稳定性以及你的应用程序的需求, 在调用useEffect Hooks时简单地获取数据可能是完全合适的,而且相当吸引人。
JavaScript还提供了异步函数和await关键字来处理异步响应,但在将它们与useEffectHooks结合起来时有一些注意事项。 作为将我们的数据获取转换为异步等待的初步尝试,我们可以这样尝试。
useEffect(async () => {
const resp = await fetch("http://localhost:3001/users");
const data = await resp.json();
setUsers(data);
}, []);
但这种方法会引起React在控制台显示一个警告
◾ 副作用回调是同步的,以防止竞赛条件。把异步函数放在里面。
async函数默认会返回一个promise。 将副作用函数设置为异步会引起麻烦,因为React正在寻找副作用的返回值是一个清理函数。 要解决这些问题,记得把异步函数放在副作用函数里面,而不是让副作用函数本身成为异步。
useEffect(() => {
// 定义一个异步函数
async function getUsers() {
const resp = await fetch(url); //等待异步的结果
const data = await resp.json();
setUsers(data);
}
getUsers(); //调用异步函数。
}, []);
现在我们已经建立了JSON服务器,尝试了一个使用useEffect Hooks的fetch-on-render数据获取方法的例子,并花时间考虑了async- await语法, 我们准备更新预订应用程序,为Book- ablesList组件获取数据。
在上一节中,我们看到了一个组件是如何在其初始渲染后,通过在调用useEffect Hook中包含获取代码来加载数据的。
更复杂的应用程序由许多组件和对数据的多次查询组成,可以使用多个端点。 你可能会尝试通过将状态及其相关的数据获取动作转移到一个单独的数据存储中,然后将组件连接到该存储中,来平滑这种复杂性。
但对于你的应用程序来说,将数据获取置于消耗数据的组件中,可能是一种更直接、更容易理解的方法。
现在,我们将保持简单,让BookablesList组件加载自己的数据。 我们将通过四个步骤来开发它的数据获取能力。
◾ 检查数据加载过程 ◾ 更新reducer以管理加载和错误状态 ◾ 创建一个辅助函数来加载数据 ◾ 加载可预订物品
以上的UserPicker组件使用fetch API从JSON数据库服务器上加载用户列表。 对于BookablesList组件,我们考虑了加载和错误状态,以及Bookables本身。 我们到底想让更新的组件做什么?
在该组件第一次渲染后,它将发起一个请求,以获取它所需要的数据。 在任何数据加载之前,我们没有可供显示的账本或组, 所以该组件将显示一个加载指示器
BookablesList组件在加载数据时显示一个加载指示灯
如果加载数据出现问题--可能是因为网络、服务器、授权或文件丢失问题-- 该组件将显示类似图中的错误信息 如果加载数据时出现问题,BookablesList组件会显示错误信息
如果一切顺利,数据到达,它将显示在用户界面中。 从Rooms组中选择可预订的会议室,并显示其详细信息。 下图显示了预期的结果。 用户将能够与应用程序进行互动,选择组别和可预订项目,用 "下一步 "按钮循环浏览可预订项目,用 "显示细节 "复选框切换可预订的细节。
BookablesList组件显示了数据加载后的可预订列表 之前已经创建了一个reducer来帮助管理BookablesList组件的状态。 我们应该如何更新该reducer以应对新的功能?
现在必须考虑驱动这样一个接口所需的组件状态。 为了启用加载指示器和错误提示,我们又给状态添加了两个属性:isLoading 和 error。 我们还将bookables设置为一个空数组。
完整的初始状态现在看起来像这样。
{
"group": "Rooms",
"bookableIndex": 0,
"hasDetails": true,
"bookables": [],
"isLoading": true,
"error": false
}
该组件将在第一次渲染后开始加载数据,因此从一开始我们就将isLoading设置为为真。 我们的初始用户界面将是加载指示器。
为了响应数据获取事件而改变状态,我们在reducer中添加了三种新的动作类型。 ◾ FETCH_BOOKABLES_REQUEST-组件发起请求。 ◾ FETCH_BOOKABLES_SUCCESS-从服务器成功获取书本数据。 ◾ FETCH_BOOKABLES_ERROR-请求数据时发生错误。
在reducer中管理加载和错误状态
src/components/Bookables/reducer.js
export default function reducer(state, action) { switch (action.type) { case "SET_GROUP": return { ...state, group: action.payload, bookableIndex: 0, };
case "SET_BOOKABLE":
return {
...state,
bookableIndex: action.payload,
};
case "TOGGLE_HAS_DETAILS":
return {
...state,
hasDetails: !state.hasDetails,
};
case "NEXT_BOOKABLE":
const count = state.bookables.filter(
(b) => b.group === state.group
).length;
return {
...state,
bookableIndex: (state.bookableIndex + 1) % count,
};
case "FETCH_BOOKABLES_REQUEST":
return {
...state,
isLoading: true,
error: false,
bookables: [], // 在请求新的数据时,清除可预订的数据
};
case "FETCH_BOOKABLES_SUCCESS":
return {
...state,
isLoading: false,
bookables: action.payload, // 通过payload将加载的可读文件传递给reducer。
};
case "FETCH_BOOKABLES_ERROR":
return {
...state,
isLoading: false,
error: action.payload, // 通过payload将错误传递给reducer。
};
default:
return state;
} }
>`FETCH_BOOKABLES_REQUEST`
>当组件发出对可预订数据的请求时,我们想在用户界面中显示加载指标。
>除了将isLoading设置为"true"外,我们还要确保当前没有可预订数据,并清除任何错误信息。
>`FETCH_BOOKABLES_SUCCESS`
>呜呼!书籍已经到达,并且在动作的有效载荷中。
>我们想取消它们,所以把isLoading设为false,并把payload分配给bookables状态属性。
>`FETCH_BOOKABLES_ERROR`
>Boo!出错了,错误信息就在动作的有效载荷中。
>我们想显示错误信息,所以将isLoading设置为false,并将有效载荷分配给错误状态属性。
>每个动作都有很多相互关联的状态变化;
>有一个reducer来分组和集中这些变化,真的很有帮助。
### 创建一个辅助函数来加载数据
>当UserPicker组件获取其数据时,它并不担心加载状态或错误信息;
>它只是直接从useEffectHooks中调用fetch。现在我们要做的是在数据加载时给用户一些反馈,创建一些专门的数据获取函数可能会更好。
>我们希望我们的数据代码能够执行三个关键任务。
>◾ 发送请求
>◾ 检查响应是否有错误
>◾ 将响应转换为JavaScript对象
>一个获取数据的函数
```tsx
export default function getData(url) {
//接受一个URL参数。
//将URL传递给浏览器的获取功能。
return fetch(url).then((resp) => {
if (!resp.ok) {
//我们检查响应的状态,如果不正常(HTTP状态码不在200到299的范围内)就抛出一个错误。
throw Error("There was a problem fetching data.");
}
//将响应的JSON字符串转换成一个JavaScript对象。
return resp.json();
});
}
状态代码在200到299范围之外的响应是有效的,而且fetch不会自动抛出任何错误。 我们做自己的检查,必要时抛出一个错误。 我们在这里不捕捉任何错误;调用代码应该根据需要设置异常捕获
将响应转换为一个javascript对象 如果响应通过,我们通过调用响应的json方法将服务器返回的JSON字符串转换为JavaScript对象。 json方法返回一个promise,该promise可解析为我们的数据对象,我们从该函数中返回该promise。
return resp.json()
getData函数对来自fetch的响应进行了一些预处理,有点像一个中间件。 使用getData的组件不需要自己做这些预处理的检查和修改。 让我们看看BookablesList组件如何使用我们的数据获取函数来加载可供显示的书籍。
这段代码导入了我们新的getData函数,并在一个useEffectHooks中使用它, 该Hooks在组件第一次挂载时运行一次。 它还包括isLoading和错误状态值,以及一些相关的用户界面,用于显示数据正在加载或有错误信息。
React Hooks