blog.owlcode.net

owl
React

StripeElementsのエラーをReactHookFormでvalidationしたい

2021-12-05

やりたいこと

form内にReactHookFormでvalidationするinputStripeElementsが混在している場合に、SubmitBlurのタイミングでStripeElementsの入力内容も検証して、エラーがあれば他inputと同様のフォーマットで表示したい。
イメージとしては以下のような感じ。
no-style

複数画面で使うことも考慮してReactHookFormでvalidation可能なStripeElementsをコンポーネントとして作成して使いまわせるようにしたい。

 

完成形

ReactHookFormのuseFormContextを利用すると良い感じに実装できる。

// CreditCardInput.tsx(カード情報入力コンポーネントのView部分)
import { CardElement } from '@stripe/react-stripe-js'
import React, { VFC } from 'react'
import { useCreditCardInput } from './useCreditCardInput'

const CreditCardInput: VFC = () => {
  const { onChange, onBlur } = useCreditCardInput()

  return <CardElement onChange={onChange} onBlur={onBlur} options={stripeElementOptions} />
}

export default CreditCardInput
// useCreditCardInput.ts(ロジック部分)
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'
import { useRef } from 'react'
import { useController, useFormContext } from 'react-hook-form'

export const useCreditCardInput = () => {
  const cardError = useRef<string | undefined>(undefined)
  const elements = useElements()
  const stripe = useStripe()

  const { control, setValue, trigger } = useFormContext<{ creditCard: unknown }>()
  const {
    field: { onChange, onBlur },
  } = useController({
    control: control,
    name: 'creditCard',
    rules: {
      required: '入力してください。',
      validate: () => {
        return cardError.current
      },
    },
  })

  // CardElementのChange, Blur時にvalidate処理を設定
  elements?.getElement(CardElement)?.on('change', (event) => {
    cardError.current = event.error?.message

    if (!event.empty) {
      setValue('creditCard', true)
    } else {
      setValue('creditCard', null)
    }
  })
  elements?.getElement(CardElement)?.on('blur', () => {
    trigger('creditCard')
  })

  return {
    elements,
    stripe,
    onChange,
    onBlur,
  }
}

// CreditCardForm.tsx(フォームのView部分)
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import React, { VFC } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { CreditCardInput, NameInput, MailInput } from './'

const stripePromise = loadStripe('XXXXXXXXXXXXXXXXXX')

const CreditCardFormWrapper: VFC = () => {
  return (
    <Elements stripe={stripePromise}>
      <CreditCardForm />
    </Elements>
  )
}

export const CreditCardForm: VFC = () => {
  const { onSubmit, methods } = useCreditCardForm()
  const errors = methods.formState.errors

  return (
    <FormProvider {...methods}>
      <form onSubmit={onSubmit}>
        <NameInput
          {...methods.register('name', { required: '入力してください' })}
        />
        <p>{errors.name?.message}</p>
        <MailInput
          {...methods.register('mail', { required: '入力してください' })}
        />
        <p>{errors.mail?.message}</p>
        <CreditCardInput />
        <p>{errors.creditCard?.message}</p>
      </form>
    </FormProvider>
  )
}

export default CreditCardFormWrapper
// useCreditCardForm.ts(ロジック部分)
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'

export const useCreditCardForm = () => {
  const methods = useForm<{name: string, mail: string, creditCard: unknown }>()
  const elements = useElements()
  const stripe = useStripe()

  const onSubmit = methods.handleSubmit(async () => {
    if (!stripe || !elements) return

    const cardElement = elements.getElement(CardElement)
    if (!cardElement) return

    const result = await stripe.createToken(cardElement)

    if (result.error || !result.token.card) {
      const errorMessage = result.error?.message || 'クレジットカードの登録に失敗しました。'
      methods.setError('creditCard', { type: 'validate', message: errorMessage }) // 👈setErrorでトークン作成時に発生したエラーを設定する
    }
  })

  return {
    methods,
    onSubmit,
  }
}

 

End

業務で作成する必要が出たので作った。
StripeElementsReactHookFormも有名ライブラリなので、この組み合わせで使ってる人の記事から真似して楽できるかな〜と思ってたら全く実装例が見つからず、自分で実装検討した。
どちらのライブラリもドキュメントが丁寧でありがたい。
今のとこ良い感じだけど、もしもっと筋の良いやり方があれば知りたい。