likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 21

初探 React 钩子

React 访问 DOM 节点

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

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

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

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>
  )
}

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

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

受控组件

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

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

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 重新渲染,将值又赋值进输入框,形成一个闭合回路。

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

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

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

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

自定义钩子

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

// initValue 初始值参数
const [value, setValue] = useState(initValue);

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

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

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>
  )
}

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

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

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

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

(custVal = initValue) => setValue(custVal)

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

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 方法吗?可以探讨探讨呢?