React - 自定义Hook - useCompositions

2,405 阅读2分钟

前言

在工作中时常会遇到这样一个场景:页面上方有一个搜索框,根据搜索关键字去请求接口获取列表数据,将数据渲染至下方列表中做展示。

这时我们会遇到一个问题:在输入框中输入中文时,会发现每输入一个中文拼音字符,都会触发一次 Input 输入框 onChange 事件,进而触发请求接口逻辑;

这显然不是我们所期望的,我们希望可以在输入中文拼音并按下空格选择中文后,触发接口请求。

这时我们就需要使用 onCompositionStart 和 onCompositionEnd 事件来处理中文拼音的触发请求时机。

下面将介绍两种解决中文拼音输入问题的方式:

  • 普通方式;
  • 自定义 Hook 方式(基于普通方式封装为一个 Hook 实现功能复用)。

普通方式:

Input 基础示例:

import React { useState, useRef } from 'react';

const App = () => {
    const [value, setValue] = useState<string>('');
    
    const onChange = (event: React.ChangeEvent<TInputElement>) => {
        setValue(event.target.value);
        fetchListDataApi(); // 伪代码,强调用做请求数据的独立方法
    }
    
    return (
        <Input value={value} onChange={onChange} />
    )
}

定义 compositionLock:

compositionLock 是一个 boolean 值,用于在 onCompositionStart 和 onCompositionEnd 中控制输入中文拼音的一个

这里我们借助 useRef 对数据持久化的特性,来保存 compositionLock 值。

const App () => {
    // ...
    const compositionLockRef = React.useRef<boolean>(false);
    
    // ...
}

onCompositionStart 和 onCompositionEnd 事件定义:

input 元素提供了 onCompositionStart 和 onCompositionEnd 这两个 DOM 事件,在事件对象 event 中可以根据 type 区分是 start 还是 end,因此可以复用一个处理函数。

完整代码如下:

import React { useState, useRef } from 'react';

const App = () => {
    const [value, setValue] = useState<string>('');
    const compositionLockRef = React.useRef<boolean>(false);
    
    const onChange = (event: React.ChangeEvent<TInputElement>) => {
        setValue(event.target.value); // 将中文拼音更新到 state 和 view 中
        if (compositionLockRef.current) return; // 允许输入中文时更新视图 value,但不触发数据逻辑
        fetchListDataApi(event.target.value); // fetch API
    }
    
    const onComposition = (event: React.CompositionEvent<TInputElement>) => {
        if (event.type === 'compositionend') {
          compositionLockRef.current = false;
          fetchListDataApi(event.currentTarget.value); // fetch API
        } else {
          compositionLockRef.current = true;
        }
    }
    
    return (
        <Input 
            value={value} 
            onChange={onChange}
            onCompositionStart={onComposition}
            onCompositionEnd={onComposition}
        />
    )
}

自定义 Hook - useCompositions

从上面的实现可以看到,思路也比较简单清晰;

但试想:项目中有多处都用到了 input 搜索请求,显然 Copy 这样的代码不是最可取的选择,能不能有一种方式可以做到复用 onCompositionStart 和 onCompositionEnd 的处理逻辑呢?于是,useCompositions 诞生!

我们可以编写一个 Hook,由它来提供 compositionLock 锁和 onComposition 方法,外边不关注它的具体实现,只需要将 onComposition 方法绑定到 input 元素的 onCompositionStart 和 onCompositionEnd 事件上即可。

实现:

import React from 'react';

type TInputElement = HTMLInputElement | HTMLTextAreaElement;

export interface CompositionsResult {
  value: string, 
  setValue: React.Dispatch<React.SetStateAction<string>>,
  onChange: (event: React.ChangeEvent<TInputElement>) => void,
  onComposition: (event: React.CompositionEvent<TInputElement>) => void,
}

function useCompositions(
  defaultValue: string,
  onSearch?: (value: string) => void,
): CompositionsResult{
  const [value, setValue] = React.useState<string>(defaultValue);
  const compositionLockRef = React.useRef<boolean>(false);

  const handleSearch = (value: string) => {
    onSearch && onSearch(value);
  }

  const onChange = (event: React.ChangeEvent<TInputElement>) => {
    const newValue = event.target.value;
    setValue(newValue);
    if (compositionLockRef.current) return; // 允许输入中文时更新视图 value,但不触发数据逻辑
    handleSearch(newValue);
  }

  const onComposition = (event: React.CompositionEvent<TInputElement>) => {
    if (event.type === 'compositionend') {
      compositionLockRef.current = false;
      handleSearch(event.currentTarget.value);
    } else {
      compositionLockRef.current = true;
    }
  }

  return {
    value,
    setValue,
    onChange,
    onComposition
  }
}

export default useCompositions;

使用:

const App = () => {
    // useDebounce 用于处理防抖的 hook
    const handlerSearch = useDebounce((val: string) => {
        fetchListDataApi(val); // fetch API
    });
    const { value, onChange, onComposition } = useCompositions('', handlerSearch);
    
    return (
        <Input 
            value={value} 
            onChange={onChange}
            onCompositionStart={onComposition}
            onCompositionEnd={onComposition}
        />
    )
}