React-stripe-elements: API Review: Hooks & Beyond

Created on 30 Jan 2019  路  27Comments  路  Source: stripe/react-stripe-elements

Summary

With the introduction of hooks, the API could update to allow the components to meet current standards of where React is and where it's going.

Hooks example:

const stripe = useStripe(); // would replace injectStripe(Component)
...
render (
  return <CheckoutForm {...stripe} />;
)

For those who don't want to use HOC, you can provide a useStripe hook that accomplishes the same thing that injectStripe currently does that follows the useContext design.

Elements Component Update:

<Elements>
 {props => (
   <form>
       <CardNumberElement {...props} />
   </form>
  }
</Elements>

Another common design now to return functions to pass props to children. This could be an alternate to the hook if so desired. Formik and ReactSpring are good examples of doing this.

Motivation

As React continues to version into the future, the API should reflect common patterns that will be used.

Similar APIs

Alternatives

Do nothing and everything still works. Implement changes and everything still works. Win, win.

Scope

Depending on what is decided on can range from InjectStripe to Elements.

Risks

Don't think so, but humans can be dumb... 馃槀

Most helpful comment

Thanks @dantman. After a bunch of trial and error, i ended up with an approach similar to what you suggest. 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.

All 27 comments

Hi @caseybaggz, thanks for opening this API review! I'm acknowledging that we'll take a look and get back to you with feedback soon.

Hi @caseybaggz

Thank you for this proposal.

I like the suggestion to add a useStripe hook that can be used instead of injectStripe. Based on my understanding of the hooks API this should work nicely.

However, adding this is currently not very high priority for us, and we will likely wait until hook usage becomes a little more popular/wide-spread.
(Would you be interested in submitting a PR for this?)

@cweiss-stripe sure thing!

@cweiss-stripe, since we are updating, react in this future PR, we will need to upgrade some legacy code (specifically, context), I would recommend following the React pattern of having a "current" and "legacy" package which should be pretty easy.

Once we get this update in you just introduce a new package that is "react-stripe-elements-legacy" which just points to the previous release, branch, tag, or however you want it maintained?

This way it has wider support case for people on different versions without forcing errors in the console due to non-compatible versions?

Thoughts?

馃憤 I think injectStripe() is getting in the way of using React.forwardRef() in my case

I have a suggestion to add that would be very handy, too!
If you have multiple components across your app accessing the StripeJs script and you always need to make sure the script is loaded, you write something like this multiple times (using hooks already):

const [stripe, setStripe] = useState(null);

useEffect(() => {
  if (window.Stripe) {
    setStripe(window.Stripe(process.env.REACT_APP_STRIPE_API_KEY));
  } else {
    document.querySelector('#stripe-js').addEventListener('load', () => {
      setStripe(window.Stripe(process.env.REACT_APP_STRIPE_API_KEY));
    });
  }
}, []);

I would be awesome if it were already built-in like this:

import { useStripeJs } from 'react-stripe-elements';

const stripe = useStripeJs('#stripe-js', process.env.REACT_APP_STRIPE_API_KEY));

With the parameters being: useStripeJs(scriptElementId, stripeApiKey)
I am already using this as a custom hook in my web apps.

Hey guys 馃憢 with the community moving away from HOC and towards hooks, is there a roadmap item for implementing something similar to this proposal?

Looking for some useStripe hook as well!

I created a question on StackOverflow in hopes that someone can come up with an answer
https://stackoverflow.com/questions/57265054/how-to-create-usestripe-hook-for-react-stripe-elements

@AndreiRailean it's probably easiest to wait for it to be natively supported and hide the injectStripe in an internal component of yours. How will depend on how you're building things. For me I was using react-final-form and wanted to do everything in the submit handler so I just wrote a custom field that provides the stripe object as a form value.

If you absolutely want a useStripe hook. Because this library uses legacy context and I think the <Elements> component injects more, you'll need to add a wrapper component that you use everywhere you use <Elements>. e.g. <Elements><UseStripeHack>{/* ... */}</UseStripeHack></Elements>. UseStripeHack would basically be a component that creates a custom context, uses injectStripe and then passes the stripe prop on to the provider for that custom context, and then you'd have your own useStripe hook that uses that context.

Thanks @dantman. After a bunch of trial and error, i ended up with an approach similar to what you suggest. 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.

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.

Not really, it's not really an <Elements> problem. <Elements> can provide a context too. The issue is that #374 is a blocker. The library uses the legacy context API and you cannot use legacy context in hooks, so they absolutely have to move to the official context API before they can add even an optional a useStripe. But they want to keep supporting React 15.x which only has the obsolete context API.

It looks like react-stripe-elements is not using the context API. Is there a specific reason for it? It feels like by using the Context API, we would get the hook support for free.

If it's helpful to anyone, it is possible to convert from the legacy context API to the official context API (and therefore making it possible to use the useContext hook) by doing something like the following. (This skips over the loading of stripe.js, but feel free to ask if that part isn't clear.)

const StripeContext = createContext();

export function StripeHookProvider( { children, stripeJs } ) {
    const stripeData = {
        stripe: null, // This must be set inside the injected component
    };
    return (
        <StripeProvider stripe={ stripeJs }>
            <Elements>
                <StripeInjectedWrapper stripeData={ stripeData }>{ children }</StripeInjectedWrapper>
            </Elements>
        </StripeProvider>
    );
}

function StripeHookProviderInnerWrapper( { stripe, stripeData, children } ) {
    const updatedStripeData = { ...stripeData, stripe };
    return <StripeContext.Provider value={ updatedStripeData }>{ children }</StripeContext.Provider>;
}

const StripeInjectedWrapper = injectStripe( StripeHookProviderInnerWrapper );

Then you can pull in the Context with a Hook like this.

export function useStripe() {
    const stripeData = useContext( StripeContext );
    return stripeData || { stripe: null };
}

Here's how you'd use these inside checkout components.

function TopLevel() {
  return (
    <StripeHookProvider>
      <CheckoutParent />
    </StripeHookProvider>
  );
}

function CheckoutParent() {
  return <CheckoutChild />;
} 

function CheckoutChild() {
  const { stripe } = useStripe();
  //...
}

I'm still working on this, but I figured I would share it for those who are using Typescript and taking @sirbrillig advice/workaround:

import React, { createContext, useContext, ReactNode } from 'react';
import { StripeProvider, injectStripe, Elements } from 'react-stripe-elements';

const StripeContext = createContext({ stripe: {} as stripe.Stripe });

interface StripeWrapped {
  stripe: stripe.Stripe;
}

interface StripeHookProviderInnerWrapperProps {
  stripe?: stripe.Stripe;
  stripeData: StripeWrapped;
  children: ReactNode;
}

interface StripeHookProviderProps {
  apiKey: string;
  children: ReactNode;
}

export const StripeHookProvider: React.FC<StripeHookProviderProps> = (props) => {
  const stripeData = {
    stripe: {} as stripe.Stripe,
  };
  return (
    <StripeProvider apiKey={props.apiKey}>
      <Elements>
        <StripeInjectedWrapper stripeData={stripeData}>{props.children}</StripeInjectedWrapper>
      </Elements>
    </StripeProvider>
  );
};

const StripeHookProviderInnerWrapper = (props: StripeHookProviderInnerWrapperProps) => {
  const updatedStripeData = { stripe: { ...props.stripeData.stripe, ...props.stripe } };

  return <StripeContext.Provider value={updatedStripeData}>{props.children}</StripeContext.Provider>;
};

const StripeInjectedWrapper = injectStripe(StripeHookProviderInnerWrapper);

export const useStripe = () => {
  const stripeData = useContext(StripeContext);

  return stripeData || { stripe: {} as stripe.Stripe };
};

Thanks @jorgedavila25, works like a charm. Why are you returning returning the stripe.Stripe into stripeData instead of just returning stripe.Stripe?

Ah... he might have been following my example, and the reason I had done it was because you might want to provide additional properties in the hook. In the version I actually implemented I also have properties like isStripeLoading, and stripeErrorMessage. However, in this example, returning the stripe object itself is probably more elegant as you suggested.

I see :) Thank you both for the example, I was looking such.

@sirbrillig @jorgedavila25 @AndreiRailean I think it's better to include the hook provider inside a custom Elements component instead of wrapping your entire app, as that allows multiple Elements blocks to be used within the app. Here's how I'm doing it.

import React, { createContext, useState, useEffect, useContext } from 'react';
import {
  StripeProvider as StripeProviderInner,
  Elements as ElementsInner,
  injectStripe
} from 'react-stripe-elements';

export const StripeProvider = ({ children }) => {
  const [stripe, setStripe] = useState(null);

  const initializeStripe = () => {
    const key = process.env.STRIPE_PUBLISHABLE_KEY;
    setStripe(window.Stripe(key));
  };

  useEffect(() => {
    if (window.Stripe) {
      initializeStripe();
    } else {
      // Otherwise wait for Stripe script to load
      document.querySelector('#stripe-js').addEventListener('load', () => {
        initializeStripe();
      });
    }
  }, []);

  return <StripeProviderInner stripe={stripe}>{children}</StripeProviderInner>;
};

const HookContext = createContext();
const HookProvider = injectStripe(({ stripe, elements, children }) => {
  return (
    <HookContext.Provider value={[stripe, elements]}>
      {children}
    </HookContext.Provider>
  );
});

export const Elements = ({ children }) => {
  return (
    <ElementsInner>
      <HookProvider>{children}</HookProvider>
    </ElementsInner>
  );
};

export const useStripe = () => useContext(HookContext);

Can someone at Stripe give some feedback on the current bits of code and their feasibility of integration? There's a couple solid pieces of code here but it makes no sense for us all to maintain our own.

If the Stripe team will accept a pull request, let us know. If not, let us know.

I managed to get Stripe to work with hooks following the examples above but now I'm stuck on getting the element. In the example on the Readme it has:

const cardElement = this.props.elements.getElement('card');

How would I get the card using hooks?

EDIT:
I wrote my provider like this which seems to do the trick:

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

const Context = createContext()

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

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

export default injectStripe(HookProvider)

Then in my form I do:

const [stripe, elements ] = useStripe();

@gragland do you have an example of how to use the code you provided?

This is what I've done, where from 'useStripe' is just a file (useStrip.js) I put @gragland code in.

Seems to work well!

import { StripeProvider, Elements, useStripe } from 'useStripe';
import { CardElement}  from 'react-stripe-elements';

function BillingCardForm({}) {
  const [stripe, elements] = useStripe();
  const onSubmit = (ev) => {
    ev.preventDefault();
    const cardElement = elements.getElement('card');
    console.error('card element is..', cardElement);
  }

  return (
    <div>
      <CardElement style={{base: {fontSize: '18px'}}} />
      <button onClick={onSubmit}>Submit</button>
    </div>
  )
}

function BillingContainer({}) {
return (
  <StripeProvider>
    <div>
      <Elements>
        <BillingCardForm> </BillingCardForm>
      </Elements>  
    </div>
  </StripeProvider>
);

Is this currently being planned/worked on? Having a hook would be a very nice addition.

We have released https://github.com/stripe/react-stripe-js which supports hooks. Hopefully that new library answers your needs!

@hofman-stripe thanks. Is this a higher level library for react-stripe-elements or something?

React Stripe.js includes a new React API as well! Closing the issue here since it would be better to open there if there are still issues with the API.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cleemputc picture cleemputc  路  5Comments

kongakong picture kongakong  路  4Comments

stephenhuh picture stephenhuh  路  4Comments

michael-reeves picture michael-reeves  路  3Comments

shortcircuit3 picture shortcircuit3  路  5Comments