React Native实现模仿web端通过id获取input DOM

56

React Native中无法通过document.getElementById来获取input DOM,从而手动调用input.focus。以下介绍一种方法,通过context与ref来实现类似的效果。

React Native无法像web端一样绑定id,以下实现一个可传入id的input组件CIdInput,还有一个InputRefsContext,提供了CIdInput.focus的方法focusInput和通过id将CIdInput.ref注册的方法registerInput。

import { useMemoizedFn } from 'ahooks'
import {
  createContext,
  FC,
  forwardRef,
  memo,
  PropsWithChildren,
  useContext,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
} from 'react'
import { TextInput } from 'react-native'

import { CInput, CInputProps } from './input'

interface CIdInputRef {
  focus: () => void
}

interface InputRefsContextType {
  registerInput: (id: string, ref: CIdInputNumberRef) => void
  // 通过调用focusInput来实现子孙组件中的CIdInput的聚焦效果
  focusInput: (id: string) => void
}

export const InputRefsContext = createContext<InputRefsContextType | undefined>(undefined)

// CInputProps为自定义组件Input的props
interface CIdInputProps extends CInputProps {
  id: string
}

// 获取context的hooks
export const useInputRefs = () => {
  const context = useContext(InputRefsContext)
  if (context === undefined) {
    throw new Error('useInputRefs must be used within an InputRefsProvider')
  }
  return context
}
// 为了方便多层嵌套,利用contenxt.Provider
export const InputRefsContextProvider: FC<PropsWithChildren> = memo(({ children }) => {
  const inputRefs = useRef<{ [key: string]: CIdInputNumberRef }>({})
  const registerInput = useMemoizedFn((id: string, ref: CIdInputNumberRef) => {
    inputRefs.current[id] = ref
  })

  const focusInput = useMemoizedFn((id: string) => {
    const input = inputRefs.current[id]
    if (input) input.focus()
  })

  const inputRefsContextValue = useMemo(() => {
    return { registerInput, focusInput }
  }, [registerInput, focusInput])

  return <InputRefsContext.Provider value={inputRefsContextValue}>{children}</InputRefsContext.Provider>
})


const CIdInput = forwardRef<CIdInputRef, CIdInputProps>(({ id, ...props }, ref) => {
  const inputRef = useRef<TextInput>(null)
  const { registerInput } = useInputRefs()

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
  }))

  useEffect(() => {
    // 根据id将input绑定到InputRefsContext.Provider的inputRefs中
    if (inputRef.current) {
      registerInput(id, inputRef.current)
    }
  }, [id, registerInput])

  return <CInput {...props} ref={inputRef} />
})

export { CIdInput }

在我们嵌套多层组件时,需要在祖先组件ParentComponent通过id获取子孙组件中的input的dom,在web端可以直接使用document.getElementById去获取,在React Native中实现了上述CIdInput组件后,可以实现通过contextProvider实现效果。

// 深层嵌套的组件
const DeepNestedInputs: React.FC = () => {
  return (
    <View>
      <CIdInput id="input1" placeholder="Input 1" />
      <View>
        <CIdInput id="input2" placeholder="Input 2" />
      </View>
      <View>
        <View>
          <CIdInput id="input3" placeholder="Input 3" />
        </View>
      </View>
    </View>
  );
}

const ParentComponent: React.FC = () => {
    const { focusInput } = useInputRefs()
    return (
        <InputRefsContextProvider>
            <View>
                <DeepNestedInputs />
                <Button title="Focus Input 1" onPress={() => focusInput?.('input1')} />
                <Button title="Focus Input 2" onPress={() => focusInput?.('input2')} />
                <Button title="Focus Input 3" onPress={() => focusInput?.('input3')} />
            </View>
        </InputRefsContextProvider>
    )
}