用 React 的状态管理,简简单单实现一个颜色转换器

初探 React 钩子

React 访问 DOM 节点

在 React 中,要想直接访问 DOM 节点需要使用 React 的一项特性------ ref。

ref:是一个对象,存储一个组件整个生命周期内的值

在 React 中,React 为我们提供了 useRef 钩子 来创建 ref。

js 复制代码
import { useRef } from "react";

export default function ChangeColorForm() {
  const hexColor = useRef();
  const rgbColor = useRef();
  const getFullHexColorStr = str => {
    if (isNaN(parseInt(str, 16))) {
      alert("请输入正确的十六进制颜色值");
      return '';
    }
    if (str.length === 3) {
      return str.split('').map(item => item + item).join('');
    } else {
      const lastChar = str.charAt(str.length - 1);
      const strArr = [...Array(5)];
      return str + strArr.map(s => lastChar).join("");
    }
  }
  const toggleColor = e => {
    e.preventDefault();
    const hexColorStr = hexColor.current.value;
    const rgbColorStr = rgbColor.current.value;

    if (hexColorStr) {
      const hexColor = getFullHexColorStr(hexColorStr).toUpperCase();
        if (!hexColor) {
          return;
        }
        
        const strArr = [...Array(6)];
        const rgbColorArr = strArr.reduce((pre, cur, index) => {
          if (index % 2 === 1) {
            pre = [...pre, parseInt(hexColor.substring(index - 1, index + 1), 16)];
          }
          return pre;
        },[]);

        rgbColor.current.value = rgbColorArr.join(',');
    } else {
      if (rgbColorStr) {
        const rgbColorArr = rgbColorStr.split(',');
        const isRightLength = rgbColorArr.length === 3;
        const isRightRGB = rgbColorArr.every(colorNum => !isNaN(Number(colorNum) && Number(colorNum) >= 0 && Number(colorNum) <= 255))
        if (isRightLength && isRightRGB) {
          const hexColorArr = rgbColorArr.map(colorNum => Number(colorNum).toString(16));
          hexColor.current.value = hexColorArr.join('').toUpperCase();
        } else {
          alert("请输入正确的rgb格式颜色值");
        }
      }
    }
  };
  
  return(
    <form onSubmit={toggleColor} style={{padding: '20px'}}>
      <label htmlFor="hexColor">十六进制格式颜色:</label>
      <input name="hexColor" ref={hexColor} type="text" placeholder="请输入十六进制格式颜色"></input>
      <br/>
      <br/>
      <label htmlFor="rgbColor">rgb格式颜色:</label>
      <input name="rgbColor" ref={rgbColor} type="text" placeholder="请输入rgb格式颜色"></input>
      <button>转换</button>
    </form>
  )
}

这里我们手动写了一个关于十六进制颜色值与RGB颜色值互转的简易工具,代码里边我们来看一看 useRef 钩子的用法,我们用 useRef 钩子创建了两个 ref,分别在 JSX 中 input 标签中添加 ref 属性,那么我们就不再需要通过选择器来获取 DOM 元素了,直接在 ref 对象中去取 DOM 元素就可以了,从代码中我们可以看到,这个 DOM 元素存储在 ref 对象中的 current 属性中。

受控组件

在 React 中,受控组件就是以组件内部状态来管理组件内部值得变化的组件。而上一节我们提到了组件的状态有 React 提供的 useState 钩子来进行控制,我们可以理解成受控组件就是由状态来控制属性值变化得组件,由父组件属性传递和事件来驱动状态的变化,使数据产生流动的效果。当然,数据流动的效果也注定使组件不断的重新渲染,这一点,我们也必须要了解。

既然这样,我们来让这个颜色码值转换表单组件改成一个受控组件:

js 复制代码
import { useState } from "react";

export default function ChangeColorForm() {
  const [hexColorStr, setHexColorStr] = useState('FFFFFF');
  const [rgbColorStr, setRgbColorStr] = useState('');
  const getFullHexColorStr = str => {
    if (isNaN(parseInt(str, 16))) {
      alert("请输入正确的十六进制颜色值");
      return '';
    }
    if (str.length === 3) {
      return str.split('').map(item => item + item).join('');
    } else {
      const lastChar = str.charAt(str.length - 1);
      const strArr = [...Array(5)];
      return str + strArr.map(() => lastChar).join("");
    }
  }
  const toggleColor = e => {
    e.preventDefault();
    if (hexColorStr) {
      const hexColor = getFullHexColorStr(hexColorStr).toUpperCase();
        if (!hexColor) {
          return;
        }
        
        const strArr = [...Array(6)];
        const rgbColorArr = strArr.reduce((pre, cur, index) => {
          if (index % 2 === 1) {
            pre = [...pre, parseInt(hexColor.substring(index - 1, index + 1), 16)];
          }
          return pre;
        },[]);

        setRgbColorStr(rgbColorArr.join(','));
    } else {
      if (rgbColorStr) {
        const rgbColorArr = rgbColorStr.split(',');
        const isRightLength = rgbColorArr.length === 3;
        const isRightRGB = rgbColorArr.every(colorNum => !isNaN(Number(colorNum) && Number(colorNum) >= 0 && Number(colorNum) <= 255))
        if (isRightLength && isRightRGB) {
          const hexColorArr = rgbColorArr.map(colorNum => Number(colorNum).toString(16));
          setHexColorStr(hexColorArr.join('').toUpperCase());
        } else {
          alert("请输入正确的rgb格式颜色值");
        }
      }
    }
  };
  
  return(
    <form onSubmit={toggleColor} style={{padding: '20px'}}>
      <label htmlFor="hexColor">十六进制格式颜色:</label>
      <input name="hexColor" type="text" value={hexColorStr} placeholder="请输入十六进制格式颜色" onChange={e => {setHexColorStr(e.target.value)}}></input>
      <br/>
      <br/>
      <label htmlFor="rgbColor">rgb格式颜色:</label>
      <input name="rgbColor" type="text" value={rgbColorStr} placeholder="请输入rgb格式颜色" onChange={e => {setRgbColorStr(e.target.value)}}></input>
      <button>转换</button>
    </form>
  )
}

在组件中,通过 React 的状态来保存两个 input 元素的值,只要触发了 onChange 事件,通过参数 e(event).target 来获取 DOM,获取元素值再通过状态设置函数来将状态值更新,使得 DOM 重新渲染,将值又赋值进输入框,形成一个闭合回路。

如果感觉看代码太麻烦,难以理解,我们来看看示意图:

我们把代码拿出来理一理就能得到一个很简单的逻辑图,是不是一目了然。

上面我们利用状态创建的受控组件中,input 元素只有两个,但是在实际的开发中,对应的 input 元素可能有十多二十个甚至更多,按照这样的方法,那是不是得这样重复操作很多次,之前我们提到过,遇到重复的,结构相似的代码,考虑一下是否可以抽象出来封装一下呢?下面我们来介绍一下自定义的钩子。

自定义钩子

从上面的代码我们可以看到,定义状态,input 元素中赋值 value,onChange 方法,他们的结构是不是基本都是一样的呢?我们先来看看定义状态是不是可以抽象为:

js 复制代码
// initValue 初始值参数
const [value, setValue] = useState(initValue);

对于 input 元素中的属性赋值,我们还能想起属性较多的时候,整体属性的传递方法吗?估计能想起来{...props};那我们能否把 value,跟 onChange 封装在一个对象里边呢?然后用一个函数返回回来。我们新建一个 hooks.js文件,来看看怎么封装。

js 复制代码
// hooks.js
import { useState } from "react";

export const useInput = initValue => {
  const [value, setValue] = useState(initValue);
  return [
    {value, onChange: (e) => setValue(e.target.value)},
    (custVal = initValue) => setValue(custVal)
  ]
}

一眼看完,可能还没明白为啥要这样封装,先暂时不说,我们还是再来把颜色转换器继续优化一下,再来看看为啥这么封装?

js 复制代码
import { useInput } from "./hooks";

export default function ChangeColorForm() {
  const [hexColorProps, setHexColorStr] = useInput('FFFFFF');
  const [rgbColorProps, setRgbColorStr] = useInput('');
  const getFullHexColorStr = str => {
    if (isNaN(parseInt(str, 16))) {
      alert("请输入正确的十六进制颜色值");
      return '';
    }
    if (str.length === 3) {
      return str.split('').map(item => item + item).join('');
    } else {
      const lastChar = str.charAt(str.length - 1);
      const strArr = [...Array(5)];
      return str + strArr.map(() => lastChar).join("");
    }
  }
  const toggleColor = e => {
    e.preventDefault();
    if (hexColorProps.value) {
      const hexColor = getFullHexColorStr(hexColorProps.value).toUpperCase();
        if (!hexColor) {
          return;
        }
        
        const strArr = [...Array(6)];
        const rgbColorArr = strArr.reduce((pre, cur, index) => {
          if (index % 2 === 1) {
            pre = [...pre, parseInt(hexColor.substring(index - 1, index + 1), 16)];
          }
          return pre;
        },[]);

        setRgbColorStr(rgbColorArr.join(','));
    } else {
      if (rgbColorProps.value) {
        const rgbColorArr = rgbColorProps.value.split(',');
        const isRightLength = rgbColorArr.length === 3;
        const isRightRGB = rgbColorArr.every(colorNum => !isNaN(Number(colorNum) && Number(colorNum) >= 0 && Number(colorNum) <= 255))
        if (isRightLength && isRightRGB) {
          const hexColorArr = rgbColorArr.map(colorNum => Number(colorNum).toString(16));
          setHexColorStr(hexColorArr.join('').toUpperCase());
        } else {
          alert("请输入正确的rgb格式颜色值");
        }
      }
    }
  };

  const resetClick = e => {
    e.preventDefault();
    setHexColorStr();
  }
  
  return(
    <form onSubmit={toggleColor} style={{padding: '20px'}}>
      <label htmlFor="hexColor">十六进制格式颜色:</label>
      <input {...hexColorProps} name="hexColor" type="text" placeholder="请输入十六进制格式颜色"></input>
      <button onClick={resetClick}>重置</button>
      <br/>
      <br/>
      <label htmlFor="rgbColor">rgb格式颜色:</label>
      <input {...rgbColorProps} name="rgbColor" type="text" placeholder="请输入rgb格式颜色"></input>
      <button>转换</button>
    </form>
  )
}

我们先来看看这两行代码的对比:

js 复制代码
// useState React 钩子
const [hexColorStr, setHexColorStr] = useState('FFFFFF');
// useInput 自定义钩子
const [hexColorProps, setHexColorStr] = useInput('FFFFFF');

useState 钩子我们已经用得比较熟悉,熟悉了它的用法与结构,当我们需要封装自定义钩子的时候,当函数能返回跟原始钩子保持一致的结构时,当在使用的时候,是不是就会更加的轻松呢?值得注意的是,这里的 hexColorProps 是包含了 value, onChange 的多属性的属性值,在使用的时候我们是需要注意跟 useState 定义的属性值得区别。

在这次的代码中,我故意添加了一个重置按钮,估计用意你们已经猜到,很直白,就是为了讲解一下 hooks.js 中的这个方法:

js 复制代码
(custVal = initValue) => setValue(custVal)

随着 ESNext 的不断更新,前端的代码也是越来越简单,但是只要我们慢慢分析,还是很好理解的,我们来看一下对应 ES5 的代码:

js 复制代码
function (custVal) {
  custVal = custVal ? custVal : initValue;
  return setValue(custVal)
}

在实际的开发中,会遇到比较多的表单的联动与重置,不难看出这个方法,就是为给 input 元素提供外部事件(非input 元素的 onChange 事件)来更改或重置 input元素的 value 属性值。

总结

  1. ref:是一个对象,存储一个组件整个生命周期内的值,DOM 元素存储在 ref 对象中的 current 属性中;

  2. 受控组件:是由状态来控制属性值变化得组件,由父组件属性传递和事件来驱动状态的变化,使数据产生流动的效果;

  3. 自定义钩子:根据已知组件,方法的封装,在封装时尽可能保持与原组件,方法的参数,返回值结构保持一致。

小疑问😝

  1. 转换按钮没有绑定点击事件,为啥点击过后能调用 toggleColor 方法呢?而重置方法为啥就不会调用?
  2. 重置按钮的点击事件可以直接绑定 setHexColorStr 方法吗?可以探讨探讨呢?
相关推荐
bysking24 分钟前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓40 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v1 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry2 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢3 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt