发布于 2026-01-06 7 阅读
0

白板:React Hooks Hooks UseState useEffect 构建 什么?计划是什么?白板 handleMouseDown handleMouseUp handleMouseMove 控件 颜色 橡皮擦 DEV's Worldwide Show and Tell Challenge Presented by Mux: Pitch Your Projects!

白板:React Hooks

钩子

使用状态

使用效果

建造什么?

计划是什么?

木板

鼠标按下

鼠标抬起

处理鼠标移动

控制

颜色

橡皮

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

我们之前在《文本录制器:React 状态、事件处理和条件渲染》一文中讨论过状态以及如何设置和处理状态的变化。当时使用的是类组件,但当然,我们不必使用类组件也能获得所有优势,对吧?

让我们来看看如何对函数组件执行相同的操作!

钩子

Hooks 允许我们在函数组件中使用状态和生命周期方法。它们并非一直存在,而是最近在 React 16.8 中引入的。

它们是 JavaScript 函数,但不能在循环、条件语句或嵌套函数中调用。它们必须始终在 React 函数的顶层调用。

我们将讨论两个主要的钩子:

  • 使用状态
  • 使用效果

使用状态

要在类组件中设置状态,我们可以this.state在构造函数或this.setState()其他任何地方使用它。我们的代码可能如下所示:

this.setState({
        dummyState: "dum dum"
});

要使用 Hooks 重写上述代码,我们需要用到 useState 函数。它接受一个参数,用于设置状态的初始值,并返回一个数组,数组的第一个元素是当前状态的值,第二个元素是一个函数,该函数稍后将用于设置状态的值。

const [dummyState, setDummyState]= useState("dum dum");

当然,我们可以随意命名它们,但惯例如上所述。此外,通常使用数组解构方法来轻松访问返回值。

要稍后更新状态值,我们只需使用更新后的值调用返回的函数即可。

setDummyState("dum dum dum");

使用效果

我们之前在《Woof Vs. Meow:数据获取和 React 组件生命周期》一文中学习了 componentDidMount、componentDidUpdate 和 componentWillUnmount 。我们的 useEffect Hook 可以等效地实现所有这些方法的功能。是不是很酷?

useEffect 接受一个函数作为参数,还可以接受一个可选的数组。让我们把下面的代码翻译成 Hooks 代码,以便更好地理解!

两个都

componentDidMount(){
    functionThatFetchesSomeData();
}


componentDidUpdate(){
    functionThatFetchesSomeData();
}

可以通过 useEffect Hook 实现相同的效果。

useEffect(()=>{
    functionThatFetchesSomeData();
});

如前所述,useEffect Hook 的作用类似于 componentDidUpdate。它会在每次更新发生时重新运行。有时我们希望控制 useEffect 的运行时机,这就是第二个数组参数的作用所在。通过将特定的状态传递给这个数组,我们可以指示 Hook 将当前值与其先前的值进行比较,只有当它们不同时,我们的代码才会执行​​。

useEffect(()=>{
    functionThatFetchesSomeData();
},[userId]);

我们可以设置多个 useEffect Hooks,每个 Hooks 都可以有自己的过滤器和代码。

如果我们只想在组件挂载时获取数据,并且不想在更新时重新运行代码,我们可以欺骗 Hook,并向其提供空数组作为第二个参数,这样它就永远不会检测到数组中的任何更改,并且只会运行一次。

我们最后要讨论的方法是 componentWillUnmount,它通常用作清理方法。为了让 Hook 知道要清理什么,我们只需返回一个包含清理指令的函数即可。

useEffect(()=>{
    functionThatOpensAnImaginaryConnection();

    return ()=>{
        functionThatClosesAnImaginaryConnection();
    }

});

这足以让我们开始动手做点什么了!我已经彻底上瘾了!

建造什么?

你知道有时候在解释某个概念的时候,你会很想用一张歪歪扭扭的手绘图表来佐证自己的理论吗?或者在解决问题的时候,你需要记下一些笔记才能更好地理解它?

今天,我们要自己动手制作一块白板,在上面画出我们想要的各种奇形怪状的图案和涂鸦!

在这里做一些小实验
白板上写着钩子

计划是什么?

我们想要一大片空白区域用于绘画,所以我们的第一个组件就叫它“画板”吧!此外,我们还需要一些控件来改变颜色和擦除内容,因此我们的应用程序将再增加三个组件:一个用于控件,一个用于颜色,还有一个用于橡皮擦。

出发!

木板

现在,我们应该能够闭着眼睛安装 create-react-app 并创建文件夹结构,所以我就不赘述了。

在 Board 组件中,我们首先需要一个 canvas 元素。通常,为了给 canvas 添加 2D 上下文并使其可绘制,我们会使用 id 来选择它,但在 React 中,id 和 class 都不支持选择。因此,我们将使用 refs 来实现这一点。

我们之前讨论过如何在类组件中处理引用,函数组件中的处理方式也大同小异。让我们来看看它们具体是什么样的!

import React from "react";
import "./Board.css";

function Board() {
  const canvasRef = React.useRef(null);
  return (
    <div className="board">
      <canvas ref={canvasRef} />
    </div>
  );
}

export default Board;

让我们把看板添加到我们的应用程序中,以便像往常一样查看更改!

import React from "react";
import "./App.css";
import Board from "./components/Board/Board";

function App() {
  return (
    <div className="app">
      <Board />
    </div>
  );
}

export default App;

现在我们要开始使用 Hooks 了。让我们导入 useState,然后首先添加上下文!

import React,{useState} from "react";
import "./Board.css";

function Board() {
  const canvasRef = React.useRef(null);
  const [ctx, setCtx] = useState({});
  return (
    <div className="board">
      <canvas ref={canvasRef} />
    </div>
  );
}

export default Board;

首先,我们需要为画布设置上下文。在类组件中,我们会使用 componentDidMount,但正如我们之前约定的,在我们的例子中,它将被 useEffect Hook 所取代。所以,让我们导入 useEffect Hook 并设置上下文吧!

import React, { useState, useEffect } from "react";
import "./Board.css";

function Board() {
  const canvasRef = React.useRef(null);
  const [ctx, setCtx] = useState({});
  useEffect(() => {
    let canv = canvasRef.current;

    let canvCtx = canv.getContext("2d");
    canvCtx.lineJoin = "round";
    canvCtx.lineCap = "round";
    canvCtx.lineWidth = 5;
    setCtx(canvCtx);
  }, [ctx]);

  return (
    <div className="board">
      <canvas ref={canvasRef} />
    </div>
  );
}

export default Board;

我给上下文添加了一些基本设置,ctx并将 useEffect 的第二个参数添加进去,以便仅在更改时触发它ctx,避免进入设置其值的无限循环。

太好了!现在我们需要处理一下我们将要使用的事件。

我们需要处理三件大事:

  • onMouseDown 事件触发时,点击鼠标开始绘制。
  • onMouseMove 事件会在绘制过程中移动鼠标时触发。
  • onMouseUp 事件会在鼠标离开时停止绘制。

让我们把这些事件添加到画布元素中。

<canvas
  ref={canvasRef}
  onMouseDown={handleMouseDown}
  onMouseUp={handleMouseUp}
  onMouseMove={handleMouseMove}
/>

鼠标按下

对于此事件,我们需要一个标志来跟踪绘图过程是否已开始,并为其赋予一个初始状态。false

const [drawing, setDrawing] = useState(false);

在我们的函数中,我们只需将其设置为 true 即可。

function handleMouseDown() {
  setDrawing(true);
}

鼠标抬起

在这个函数中,我们将执行与 handleMouseDown 函数中完全相反的操作。

function handleMouseUp() {
  setDrawing(false);
}

处理鼠标移动

这是我们处理绘图的主要函数。我们需要移动到上次检测到的鼠标位置,并从该点绘制一条线到当前鼠标位置。

首先,我们要记录前一个位置,起始值为 (0,0)。

const [position, setPosition] = useState({ x: 0, y: 0 });

我们还需要记录画布的偏移量。在本例中,画布位于窗口的左上角,但之后我们可能需要添加其他元素或一些 CSS 代码来改变它的位置。

const [canvasOffset, setCanvasOffset] = useState({ x: 0, y: 0 });

为了保证鼠标位置能够带来预期的结果,我们在设置上下文时会记录画布的左侧和顶部偏移量。

useEffect(() => {
    let canv = canvasRef.current;

    let canvCtx = canv.getContext("2d");
    canvCtx.lineJoin = "round";
    canvCtx.lineCap = "round";
    canvCtx.lineWidth = 5;
    setCtx(canvCtx);

    let offset = canv.getBoundingClientRect();
    setCanvasOffset({ x: parseInt(offset.left), y: parseInt(offset.top) });
  }, [ctx]);

之后,我们可以通过从鼠标位置减去该偏移量来轻松检测位置。现在,我们有了先前的位置和当前的位置。在开始绘制路径之前,我们只需要检查绘制标志以确保绘制过程正在进行,完成后,我们将设置下一个笔画的位置。

function handleMouseMove(e) {
    let mousex = e.clientX - canvasOffset.x;
    let mousey = e.clientY - canvasOffset.y;
    if (drawing) {
      ctx.strokeStyle = "#000000";
      ctx.beginPath();
      ctx.moveTo(position.x, position.y);
      ctx.lineTo(mousex, mousey);
      ctx.stroke();
    }
    setPosition({ x: mousex, y: mousey });
  }

此外,我们需要在鼠标点击后设置位置,以便在下一次笔画绘制时移动到该位置,因此我们需要修改 handleMouseDown 函数。

function handleMouseDown(e) {
  setDrawing(true);
  setPosition({
      x: parseInt(e.clientX - canvasOffset.x),
      y: parseInt(e.clientY - canvasOffset.y),
    });
}

太棒了!现在,让我们给 App.css 添加一些 CSS 代码。

* {
  box-sizing: border-box;
}
html,
body,
#root {
  width: 100%;
  height: 100%;
}
.app {
  height: 100vh;
  width: 100vw;
  display: flex;
  flex-direction: column;
}

还有我们的 Board.css 文件。

.board {
  background-color: white;
  cursor: crosshair;
  margin: 0 auto;
  position: relative;
  width: 100%;
  overflow: hidden;
  flex: auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

这一切都很棒,我可以在白板上画画,但还有一个问题一直困扰着我。我不常使用 Canvas,所以花了不少时间琢磨为什么线条看起来像素化,也让我意识到自己有多么热爱后端开发。我发现这是因为我用 CSS 设置了 Canvas 的高度,不知怎么的,这导致了问题。我应该直接将窗口的内部宽度和内部高度动态地赋值给 Canvas 的宽度和高度属性,或者赋值给父元素的偏移宽度和偏移高度。

为了实现这一点,让我们为 canvas 父元素添加一个新的引用,以便能够访问其偏移宽度和高度!

const parentRef = React.useRef(null);

我们还应该将其添加到父元素中。

return (
    <div className="board" ref={parentRef}>
      <canvas
        ref={canvasRef}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
        onMouseMove={handleMouseMove}
      />
    </div>
  );

我们可以在设置上下文之前直接指定宽度和高度。

useEffect(() => {
  let canv = canvasRef.current;
  canv.width = parentRef.current.offsetWidth;
  canv.height = parentRef.current.offsetHeight;

  let canvCtx = canv.getContext("2d");
  canvCtx.lineJoin = "round";
  canvCtx.lineCap = "round";
  canvCtx.lineWidth = 5;
  setCtx(canvCtx);

  let offset = canv.getBoundingClientRect();
  setCanvasOffset({ x: parseInt(offset.left), y: parseInt(offset.top) });
}, [ctx]);

太棒了!现在我们可以在白板上自由绘画了!

控制

是时候让我们的白板更进一步,添加控件组件了。它只需要几个按钮,所以我把它设计成置于画布之上。

在 Controls 组件中,我们只需添加一个简单的结构来容纳我们的按钮。

import React from "react";
import "./Controls.css";

function Controls() {
  return <div className="controls"></div>;
}

export default Controls;

然后在 Controls.css 文件中添加一些 CSS 代码,将其定位到画布上。

.controls {
  position: absolute;
  top: 0;
  display: flex;
  justify-content: center;
  width: auto;
}

颜色

接下来我们来看颜色组件!我们需要一个颜色选择器。我选择了react-color包,可以通过运行以下命令安装:

npm install react-color --save

顺便一提,我还想给控件添加图标,所以我们可以通过运行以下命令来安装react-fontawesome包:

npm i --save @fortawesome/fontawesome-svg-core  @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome

我们先导入 Font Awesome 并添加颜色图标!

import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPalette } from "@fortawesome/free-solid-svg-icons";

function Color() {
  return (
    <div className="color">
      <FontAwesomeIcon
                title="choose color"
        className="fa-icon"
        icon={faPalette}
      />
    </div>
  );
}

export default Color;

现在,我们需要添加颜色选择器。我喜欢 ChromePicker 的外观,所以我会导入它。

我只希望在点击调色板图标后才弹出选择器,因此我需要添加一个标志来检测它是否被点击,一些自定义 CSS 并处理点击事件。

import React, { useState } from "react";
import { ChromePicker } from "react-color";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPalette } from "@fortawesome/free-solid-svg-icons";

function Color(props) {
  const popover = {
    position: "absolute",
    zIndex: "2",
  };
  const cover = {
    position: "fixed",
    top: "0px",
    right: "0px",
    bottom: "0px",
    left: "0px",
  };
  const [displayed, setDisplayed] = useState(false);

  function handleClick() {
    setDisplayed(true);
  }

  function handleClose() {
    setDisplayed(false);
  }

  return (
    <div className="color">
      <FontAwesomeIcon
        onClick={handleClick}
        title="choose color"
        className="fa-icon"
        icon={faPalette}
      />
      {displayed ? (
        <div style={popover}>
          <div style={cover} onClick={handleClose} />
          <ChromePicker />
        </div>
      ) : null}
    </div>
  );
}

export default Color;

很好!现在让我们把颜色组件添加到控件组件中。

import React from "react";
import "./Controls.css";
import Color from "../Color/Color";

function Controls() {
  return <div className="controls">
      <Color />
  </div>;
}

export default Controls;

我们将控制组件与电路板组件进行交互,以查看我们取得了多大的进展。

return (
    <div className="board" ref={parentRef}>
    <Controls />
      <canvas
        ref={canvasRef}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
        onMouseMove={handleMouseMove}
      />
    </div>
  );

好了,现在我们需要在 Board 组件中添加另一个功能。我们还没有处理从颜色选择器中选择的颜色如何在画板上体现出来。

让我们使用 Hooks 来跟踪颜色值,并将其默认值设置为黑色。

const [color, setColor] = useState("#000000");

现在让我们修改 handleMouseMove 函数,将 strokeStyle 设置为颜色状态!

function handleMouseMove(e) {
    let mousex = e.clientX - canvasOffset.x;
    let mousey = e.clientY - canvasOffset.y;
    if (drawing) {
      ctx.strokeStyle = color;
      ctx.beginPath();
      ctx.moveTo(position.x, position.y);
      ctx.lineTo(mousex, mousey);
      ctx.stroke();
    }
    setPosition({ x: mousex, y: mousey });
  }

还有一件事,我们希望颜色选择器更改时颜色状态也能更新,所以我们将添加另一个函数来处理这种情况,并将其作为 prop 发送到我们的 Controls 组件,然后再从那里将其作为 prop 发送到 Color 组件。

function handleColor(color) {
  setColor(color);
}

return (
  <div className="board" ref={parentRef}>
    <Controls handleColor={handleColor} />
    <canvas
      ref={canvasRef}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseMove={handleMouseMove}
    />
  </div>
);

在 Controls 组件中,让我们将该属性传递给 Color 组件!

function Controls(props) {
  return <div className="controls">
      <Color handleColor={props.handleColor} />
  </div>;
}

现在,让我们回到 Color 组件,并添加一个状态来跟踪颜色变化!

const [color, setColor] = useState("#000000");

之后,我们可以使用属性来处理颜色选择器的变化。我们需要的是传递给 handleChange 函数的参数中包含的颜色十六进制值。

function handleChange(pickerColor) {
    setColor(pickerColor.hex);
    props.handleColor(pickerColor.hex);
  }

我们还希望使用选定的颜色更新颜色选择器本身。

<ChromePicker color={color} onChange={handleChange} />

完美!现在,颜色显示正常了!让我们在 Controls.css 文件中添加一些 CSS 样式,让按钮看起来更漂亮。

.controls .fa-icon {
  cursor: pointer;
  font-size: 3rem;
  margin: 0.5rem;
  padding: 0.5rem;
  border-radius: 30%;
  box-shadow: 0 0 6px black;
  z-index: 2;
  color: #071a54;
  background: linear-gradient(
    90deg,
    rgba(174, 238, 237, 1) 0%,
    rgba(181, 23, 23, 1) 100%
  );
}

橡皮

我们的工作快完成了,现在只需要能用橡皮擦了。我打算作弊一下,直接把颜色改成白色。我们当然可以用原ctx.globalCompositeOperation = 'destination-out';方法,但改成白色就能解决问题。

我们的组件看起来会是这样。

import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEraser } from "@fortawesome/free-solid-svg-icons";

function Eraser(props) {
  function handleEraser(e) {
    e.preventDefault();
    props.handleColor("#ffffff");
  }

  return (
    <div className="eraser">
      <FontAwesomeIcon
                title="erase"
        icon={faEraser}
        className="fa-icon"
        onClick={handleEraser}
      />
    </div>
  );
}

export default Eraser;

在我们的 Controls 组件中,我们将传递与传递给 Color 组件相同的 prop,以便在绘制时使其反映在我们的面板上。

import React from "react";
import "./Controls.css";
import Color from "../Color/Color";
import Eraser from "../Eraser/Eraser";

function Controls(props) {
  return (
    <div className="controls">
      <Color handleColor={props.handleColor} />
      <Eraser handleColor={props.handleColor} />
    </div>
  );
}

export default Controls;

这就是我们的功能齐全的白板!

代码可以在这里找到

借着这块迷你白板,我将结束我迈向 React 伟大之路的第五小步,期待我们下次再见。

任何反馈或建议都非常欢迎。可以通过这里、推特、其他任何方式联系我!

GitHub 标志 RanaEmad /白板

一个 React 脚本,充当用户自由绘画的白板。

文章来源:https://dev.to/ranaemad/whiteboard-react-hooks-2pc9