Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create useStripe hook for react-stripe-elements

Stripe has react-stripe-elements, which provides injectStripe HOC. We're in 2019 and HOCs are not cool anymore. Stripe doesn't appear to be in a hurry, i assume it's because they want to support older versions of React, so are looking at lowest common denominator solutions only.

I'm looking for a way to get stripe via a hook (instead of a prop provided by injectStripe).

Usage would look like this const stripe = useStripe() so that later I can use stripe.handleCardSetup and other API methods

import { CardElement } from 'react-stripe-elements'

const CardForm = ({secret}) => {
  const stripe = useStripe()

  const handleSubmit = async () => {
    const { setupIntent, error } = await stripe.handleCardSetup(secret)
    // ...
  }

  return (
    <>
      <CardElement />
      <button onClick={handleSubmit} />
    </>
  )
}

how would you define useStripe hook from existing APIs and components? stripe should be the same as what you'd get if you used injectStripe hook

Related issue on GH: API Review: Hooks & Beyond

like image 660
Andrei R Avatar asked Jul 30 '19 05:07

Andrei R


People also ask

How do you connect stripes to React?

The Elements provider allows you to use Element components and access the Stripe object in any nested component. Render an Elements provider at the root of your React app so that it is available everywhere you need it. To use the Elements provider, call loadStripe from @stripe/stripe-js with your publishable key.

Is Stripe elements an iframe?

The element is an iframe, which is another window, which has its own window object and document object. The parent page cannot access these objects, unless the iframe is in the same origin.


2 Answers

After a bunch of trial and error, i ended up with this, which is quite simple. Here it is for stripe peeps to adopt and for others who need useStripe hook

// StripeHookProvider.jsx

import React, { useContext, createContext } from 'react'
import { injectStripe } from 'react-stripe-elements'

const Context = createContext()

export const useStripe = () => useContext(Context)

const HookProvider = ({ children, stripe }) => {
  return <Context.Provider value={stripe}>{children}</Context.Provider>
}

export default injectStripe(HookProvider)

Import and add StripeHookProvider to your component hierarchy like this. NOTE: because it relies on injectStripe, it must be a child of <Elements>

    <StripeProvider {...{ stripe }}>
      <Elements>
        <StripeHookProvider>
          <MyForm />
        </StripeHookProvider>
      </Elements>
    </StripeProvider>

then inside MyForm you use it like this

// MyForm.jsx
import { useStripe } from './StripeHookProvider'

const MyForm = () => {
  const stripe = useStripe()

  const handleSubmit = () => {
    const { setupIntent, error } = await stripe.handleCardSetup(secret ... // and so on
  }
}

This way you only need to do injectStripe in one place and it's tucked away neatly out of sight. As you can see from this code, it's not rocket science and should be no problem for stripe peeps to add to their code, hopefully eliminating <StripeHookProvider> altogether since they have a reference to stripe object somewhere in context.

I tried a bunch of approaches and this is the only one that worked. I didn't dig through stripe's JS code to see what happens, but stripe you get from injectStripe is the only thing you can use. The one you setup via window.Stripe(apiKey) is not the same and it won't work as it doesn't observe <Elements> properly so it doesn't know the state of the form.

Perhaps, the reason injectStripe has to be used on children of <Elements> is the same reason Stripe crew is taking a while to build this hook.

like image 194
Andrei R Avatar answered Nov 15 '22 04:11

Andrei R


I've done it myself, and I've prepared a gist for you :)

The useStripe hook uses a useScript hook that its responsible to load async scripts like the stripe one..

Here is the gist:useScript.js

I've simplified it, you may not need the locale either.

Here is the useScript:

import React from 'react'

let cache = []
const isCached = src => cache.includes(src)

export const useScript = src => {
  const [state, setState] = React.useState({
    isLoaded: isCached(src),
    hasError: false,
  })

  React.useEffect(() => {
    if (!src || isCached(src)) {
      return
    }

    cache.push(src)

    const script = document.createElement('script')
    script.src = src
    script.async = true

    const onScriptLoad = () => {
      setState(s => ({...s, isLoaded: true, hasError: false}))
    }
    const onScriptError = () => {
      const index = cache.indexOf(src)
      if (index >= 0) {
        cache.splice(index, 1)
      }
      script.remove()
      setState(s => ({...s, hasError: true, isLoaded: true}))
    }

    script.addEventListener('load', onScriptLoad)
    script.addEventListener('error', onScriptError)

    document.body.appendChild(script)

    return () => {
      script.removeEventListener('load', onScriptLoad)
      script.removeEventListener('error', onScriptError)
    }
  }, [src])

  return state
}

Here is the useStripe hook:

import React from 'react'

import {useScript} from './useScript'

let cache = {}

export const useStripe = ({locale, stripeKey}) => {
  const {isLoaded, error} = useScript('https://js.stripe.com/v3/')
  const [stripe, setStripe] = React.useState(cache[locale])

  React.useEffect(() => {
    if (isLoaded && !error && !cache[locale]) {
      cache[locale] = window.Stripe(stripeKey, {locale})
      setStripe(cache[locale])
    }
  }, [isLoaded, error, locale])

  return {
    stripe,
    error,
  }
}

This is just the hook, but if you need the context API's you can move the useStripe code inside the Provider!

like image 24
cl0udw4lk3r Avatar answered Nov 15 '22 05:11

cl0udw4lk3r