通过构建一个绘图应用来学习 React Hooks
引言
先决条件
useState- 第一部分
useState- 第二部分
useEffect
useState&useEffect挑战
useState&useEffect解决方案
useEffect清理
useRef挑战
useRef解决方案
useCallback挑战useMemo
useCallback解决方案
定制挂钩
使用 Hooks 构建绘图应用程序
结尾
据业内人士透露,React Hooks 目前非常热门。本文将跟随 Christian Jensen 的14 部分教程,了解 React 这项新特性的基础知识。一起来看看吧!
引言
Hooks 是 React 库中的新特性,它允许我们在组件之间共享逻辑,使它们可重用。
在本课程中,我们将构建一个类似于微软画图的绘图应用程序,它将允许我们为项目命名、更换颜色、获取一批新颜色,当然还可以进行绘图。
Scrimba 允许你随时暂停屏幕录制并修改代码。这是一种边做边学的绝佳方式!
先决条件
本课程假定您已具备ES6、JSX、State 和 Props的一些基础知识,但不用担心,我们已经为您准备好了——点击上面的链接查看我们的 Scrimba 文章。
如果您是 React 新手,请务必查看我们的Scrimba React 课程。
useState- 第一部分
首先,我们为应用程序提供了一种使用 useState 来管理状态的方法。
在我们的<Playground.js />组件中,我们声明了一个名为 `count` 的组件<Playground />,并创建了用于递增和递减它的按钮。然后,我们给 `useState` 传递一个参数 (0),并使用状态重构从我们的函数中获取state`count` 和 `count` setState(更新状态的函数)useState。现在,它们分别被重命名为 `count`count和 ` setCountcount`。最后,我们在浏览器中渲染计数。
最后,我们渲染按钮,这些按钮使用内联函数更新计数,点击时会触发该函数。
为了确保计数准确,我们向setState函数传递一个函数而不是一个值。该函数以当前状态作为参数,然后更新当前状态:
import React, { useState } from "react";
import randomColor from "randomcolor";
export default function Playground() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button onClick={() => setCount((currentCount) => currentCount - 1)}>
-
</button>
<button onClick={() => setCount((currentCount) => currentCount + 1)}>
+
</button>
</div>
);
}
如果您担心内联函数的性能,请查看这篇博客。
useState- 第二部分
现在我们向组件中添加名称输入框,<Name.js />以便用户可以为他们的项目命名。
要使用<Name.js />Hook useState,我们需要使用命名导入语句导入 Hook,然后设置状态。我们的状态将以 `state` 开头name,我们将使用 `setName` 更新它。然后,我们调用 `useState` 并传入一个空字符串作为默认状态值。
现在我们需要一个具有四个属性的输入元素。这四个属性分别是:
valuename这将是始终处于上述状态的状态。onChange它将使用内联方式,通过将值传递给 setStatesetState来更新。nameonClick它使用 setSelectionRange,该函数接受起始索引 0 和字符串长度的结束索引来选择整个名称,从而方便最终用户更改名称。placeholder我们将其设置为“无标题”。
import React, { useState } from "react";
export default function Name() {
const [name, setName] = useState("");
return (
<label className="header-name">
<input
value={name}
onChange={(e) => setName(e.target.value)}
onClick={(e) => e.target.setSelectionRange(0, e.target.value.length)}
placeholder="Untitled"
/>
</label>
);
}
现在我们可以为项目命名,只需单击一下即可选择名称以重置项目名称:
useEffect
目前,我们的 Playground.js 组件只是简单地渲染一个计数器,可以对其进行加减操作。现在我们将对其进行更新,使其每次计数改变时,某个元素的颜色也会随之改变。
我们使用 useState Hook 来设置初始颜色,并将其设置为 ,null然后使用函数来更新它(setColor)。现在,我们设置useEffect来更新这个颜色。useEffect的第一个参数是 setColor,我们想将其设置为randomColor。
由于我们只希望当计数发生变化时才count触发效果useEffect,因此我们将其设置为第二个参数。如果计数值没有变化,则 Hook 将不会运行效果,颜色将保持不变。
import React, { useState, useEffect } from "react";
import randomColor from "randomcolor";
export default function Playground() {
const [count, setCount] = useState(0);
const [color, setColor] = useState(null);
useEffect(() => {
setColor(randomColor());
}, [count]);
return (
<div style={{ borderTop: `10px solid ${color}` }}>
{count}
<button onClick={() => setCount((currentCount) => currentCount - 1)}>
-
</button>
<button onClick={() => setCount((currentCount) => currentCount + 1)}>
+
</button>
</div>
);
}
现在,每当我们增加或减少计数时,颜色都会发生变化。
useState&useEffect挑战
现在是时候检验我们目前所掌握的技能了。在本视频教程中,我们添加了一个可以随机获取颜色的函数:
const getColors = () => {
const baseColor = randomColor().slice(1);
fetch(`https://www.thecolorapi.com/scheme?hex=${baseColor}&mode=monochrome`)
.then((res) => res.json())
.then((res) => {
setColors(res.colors.map((color) => color.hex.value));
setActiveColor(res.colors[0].hex.value);
});
};
我们的任务是编写函数setColors,该函数将给我们一个十六进制颜色数组,以及setActiveColor函数,该函数将告诉我们当前激活的颜色是什么。
如果一切设置正确,用户界面将更新为五种颜色,点击即可展开。本次测试只需要 useState 和 useEffect 这两个函数。
useState&useEffect解决方案
在本视频教程中,Christian 将指导我们如何为组件添加功能<ColorPicker />。教程结束时,我们已经为组件添加了一些颜色:
useEffect清理
现在我们添加一个名为“窗口宽度和高度”的组件<WindowSize.js />,当用户调整窗口大小时,该组件会在屏幕底部显示窗口的宽度和高度。半秒钟后,该组件会消失。
当我们设置定时器或事件监听器时,组件卸载后也需要进行清理。这需要两个状态——<WindowSize />组件的窗口大小和可见性:
export default function WindowSize() {
const [[windowWidth, windowHeight], setWindowSize] = useState([
window.innerWidth,
window.innerHeight,
]);
const [visible, setVisible] = useState(false);
}
现在我们来设置效果,它会添加事件监听器:
useEffect(() => {
const handleResize = () => {};
window.addEventListener("resize", handleResize);
});
接下来,我们设置清理阶段。此阶段会返回一个函数,并传入一个空数组,以告知函数 useEffect 仅在首次挂载时运行。然后,清理过程将运行并移除事件监听器:
useEffect(() => {
const handleResize = () => {};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
现在我们设置窗口大小、可见性和计时器,以便调整大小的窗口出现,然后在 500 毫秒后消失:
const [visible, setVisible] = useState(false);
useEffect(() => {
const handleResize = () => {
setWindowSize([window.innerWidth, window.innerHeight]);
setVisible(true);
setTimeout(() => setVisible(false), 500);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
但是,我们不希望每次用户调整窗口大小时都添加一个新的计时器,因此我们还需要使用以下方法清理计时器clearTimeout(timeoutId):
timeoutId = setTimeout(() => setVisible(false), 500);
为了获取函数上次运行时的超时时间,我们使用clearTimeout闭包,这意味着我们在函数外部声明变量。这样,内部函数仍然可以访问该变量。每次函数运行时,之前的超时时间都会被清除,并设置一个新的超时时间。timeoutIdtimeoutIdhandleResize
最后,我们将调整大小的功能渲染到浏览器中。最终代码可以在屏幕录像中看到。
现在,每当用户调整窗口大小时,窗口大小都会设置为当前窗口大小,可见性会设置为 true,并且会启动一个计时器,在 500 毫秒后将可见性设置为 false。
useRef挑战
如果在 React 中需要访问实际的 DOM 元素,则可能需要使用 Refs。React 有一个useRef专门用于 Refs 的 Hook。
要使用 Ref,需要将其添加到元素中:
<input
ref={inputRef}
type="range"
onChange={(e) => setCount(e.target.value)}
value={count}
/>
这个输入框是一个滑块,它会更新count所选颜色。由于该值也与计数相关,因此如果通过我们之前添加的按钮更改计数,滑块也会随之调整。
我们现在已经声明了参考号 (Ref),但我们还需要通过调用以下函数来设置它useRef:
const inputRef = useRef();
为了每次通过按钮更改计数时都能聚焦输入框,我们只需在按钮被点击时运行的效果中添加必要的逻辑:
useEffect(() => {
setColor(randomColor())
inputRef.current.focus()
},
目前,画布的高度设置为窗口本身的高度,这使得用户可以在画布内滚动,如果导出图像,则可能会导致空白区域。
我们现在面临的挑战是确保绘图应用程序的画布大小不超过窗口高度减去页眉高度。为此,我们需要使用 useRef 获取页眉高度,并将其从窗口高度中减去。
useRef解决方案
在本视频教程中,Christian 将指导我们如何使用useRef.
此后,用户将无法再滚动页面,Scrimba 浏览器和普通浏览器之间仅有几像素的偏移。图像底部现在没有任何空白区域。
useCallback挑战useMemo
在本视频教程中,我们将了解_记忆化_的概念。记忆化是指纯函数返回之前已处理过的计算结果,而不是重新运行整个计算过程:
function Calculate(num) {
// first call, num === 3... ok I will calculate that
return fetchComplicatedAlgorithmToAdd47(3); // returns 50 after a while
// second call, num === 5... ok I guess I have to calculate that too
return fetchComplicatedAlgorithmToAdd47(5); // returns 52 after a while
// third call, num === 3... WAIT, I've seen this before! I know this one!
return 50; // immediately
}
React 提供了两个 Hooks,允许我们使用 memoization:useCallback和useMemo。
useCallback
我们首先从 Playground.js 中的一个非常简单的组件开始,该组件会渲染函数已渲染的次数:
function Calculate(num) {
const renderCount = useRef(1);
return <div>{renderCount.current++}</div>;
}
现在假设组件应该只在计数改变时渲染,而不是在颜色改变时渲染。为了实现这一点,我们可以使用 ` useCallback.`。我们将 `.` 的结果赋值useCallback给一个名为 `_` 的变量calculate:
const calculate = useCallback(<Calculate />, [count]);
现在我们将渲染新的calculate变量而不是<Calculate />组件。现在,组件仅在计数发生变化时渲染,而不会在点击“更改颜色”按钮时渲染。
我们还需要渲染<Calculate />组件,而不是之前使用的变量,并创建一个回调函数。我们使用useCallback并将其赋值给一个名为 `count` 的变量cb。这count是唯一的依赖项,这意味着如果计数发生变化,我们将获得一个新的函数实例:
const cb = useCallback((num) => console.log(num), [count]);
现在,我们将一个数字(设置为计数)传递给Calculate组件和回调函数,并将该数字记录到控制台。每当Calculate组件重新渲染时(即点击加号和减号按钮时),当前计数都会记录到控制台。
然而,使用这种方法,当我们点击“更改颜色”按钮时,计数也会被记录到控制台。这是因为我们对console.log函数使用了记忆化,但对组件本身没有使用,这意味着它没有检查回调函数是否与之前的回调函数相同。
React.memo
为了解决这个问题,我们向Calculate组件添加了 React.memo。现在,它会检查输入内容是否相同,如果相同则不会渲染:
const Calculate = React.memo(({ cb, num }) => {
cb(num);
const renderCount = useRef(1);
return <div>{renderCount.current++}</div>;
});
“更改颜色”按钮现在不再将计数记录到控制台。
useMemo
为了了解它的功能,我们在现有调用旁边useMemo添加一个调用:useCallbackuseMemo
useCallback(() => console.log("useCallback"));
useMemo(() => console.log("useMemo"));
这说明useMemo每次函数渲染时都会用到它。这是因为useCallback`is` 返回函数本身,而useMemo`is` 返回函数的结果:
useCallback(() => console.log("useCallback")); // return the function
useMemo(() => console.log("useMemo")); // return the result of the function
useMemo可以用于一些需要缓存的耗时函数。UseCallback另一方面,更适合在不想不必要地渲染组件时,将回调函数传递给组件。
屏幕录像最后提出了一个新的挑战。我们的绘图应用目前只提供几种颜色。我们的挑战是为新添加的刷新按钮添加一些功能,以便用户可以点击该按钮获取一些新的颜色。这应该在 `setup()` 函数中实现,该函数目前接收一个回调函数,并在点击刷新按钮时调用该回调函数。我们的挑战是使用 `setup ()` 或 ` RefreshButton.jssetup()` 函数传入回调函数。useCallbackuseMemo
作为一项额外挑战,我们还被要求使用React.memomemoize<Name />组件,该组件目前每次我们更改颜色时都会不必要地重新渲染。
useCallback解决方案
现在,克里斯蒂安将带领我们了解之前遇到的挑战的解决方案,请观看这段精彩的屏幕录像。
在屏幕录像的最后,点击刷新按钮后,它会显示全新的炫酷色彩:
定制挂钩
在这里,我们将通过将组件重构为 Hook 来学习自定义 Hook <WindowSize />。这对于提高组件的复用性非常有益。
目前,<WindowSize />它处理两组不同的状态:窗口大小和可见性。由于在未来的使用中可能不再需要可见性<WindowSize />,我们将它的逻辑移到我们的<Paint />组件中,我们也将在该组件中使用我们的useWindowSizeHook。
以下几行已从代码中删除WindowSize.js:
let timeoutId;
///
setVisible(true);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => setVisible(false), 500);
此外,现在需要从<Paint.js />以下位置返回以下几行<WindowSize />:
<div className={`window-size ${visible ? "" : "hidden"}`}>
{windowWidth} x {windowHeight}
</div>
窗口的宽度和高度将从以下函数返回<WindowSize />:
return [windowWidth, windowHeight];
为了使这些windowWidth变量windowHeight可用,我们向代码中添加以下代码<Paint.js />:
const [windowWidth, windowHeight] = useWindowSize();
为了实现可见性逻辑,以便我们可以根据需要显示和隐藏窗口大小,我们将回调传递给我们的useWindowSizeHook,并使用 Ref 使其timeoutID在渲染之间可用:
let timeoutId = useRef();
const [windowWidth, windowHeight] = useWindowSize(() => {
setVisible(true);
clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(() => setVisible(false), 500);
});
现在我们可以根据需要从以下位置调用它<WindowSize />:
export default function useWindowSize(cb) {
const [[windowWidth, windowHeight], setWindowSize] = useState([
window.innerWidth,
window.innerHeight,
]);
useEffect(() => {
const handleResize = () => {
cb();
setWindowSize([window.innerWidth, window.innerHeight]);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return [windowWidth, windowHeight];
}
我们现在拥有与以前相同的功能,但<WindowSize />逻辑位于一个可重用的 Hook 中。
课程最后还有一个挑战——将<Canvas />组件转换为使用 Hooks 而不是生命周期方法的函数。
使用 Hooks 构建绘图应用程序
本视频教程将指导您如何<Canvas />使用 Hooks 将组件转换为函数式组件。它还会演示如何重构应用程序,使其更加简洁易读。使用 Hooks 的一大优势在于,所有相关逻辑都集中在一起,这与之前组件中相关逻辑项彼此分离的情况截然不同。
屏幕录像结束时,我们的绘图应用程序终于完成了,我们准备开始创作我们的杰作:
结尾
我们已经完成了 React Hooks 课程。我们学习了以下内容:
useState它管理状态useEffect但这会产生副作用,useRef它会获取对 DOM 元素的引用,并在渲染过程中保留这些值。useCallback这样就创建了一些不需要在每次渲染时都创建的函数。useMemo它会将昂贵的计算过程记忆化React.Memo它可以绕过 React 组件并对其进行缓存。custom Hooks这使得我们能够创建自己的可重用逻辑。
使用这些钩子时,需要牢记两条规则:
- 只能在 React 组件的顶层调用 Hooks,即不能在 if 代码块或类似代码块中调用。
- 只能在 React 函数中调用 Hooks,不能在自定义函数中调用。
恭喜你完成了教程并掌握了本项目中使用的所有技能。为了进一步学习,不妨看看 Scrimba 的免费六小时React 入门课程,它旨在让你成为 React 高手!
祝您编程愉快!
文章来源:https://dev.to/scrimba/learn-react-hooks-by-building-a-paint-app-4p9c















