react-plaid-link: Intermittently stops working?

I am using the following code

export const LinkPlaidItemButton: React.FC<LinkPlaidItemButtonProps> = (props) => {
  const { plaidItemId } = props;
  const isUpdateMode = !!plaidItemId;

  const [linkPlaidItem, { data, loading, error }] = useLinkPlaidItemLazyQuery({
    // Generate a new token every time the button is clicked, an old one could be expired otherwise
    fetchPolicy: 'no-cache',
    variables: { plaidItemId },
  });
  const token = data?.getPlaidLinkToken.linkToken ?? null;
  console.log('got token', token);

  const { open, ready } = usePlaidLink({
    token,
    onEvent: (eventName, metadata) => console.log(eventName, metadata),
    onLoad: () => console.log('link loaded'),
    onExit: (error) => console.log(error),
    onSuccess: () => {}
  });

  useEffect(() => {
    if (token && ready) {
      open();
    }
  }, [token, ready, open]);

  if (error) throw error;

  return (
    <LoadingButton
      variant="contained"
      loading={loading || Boolean(token && !ready)}
      onClick={() => linkPlaidItem()}
    >
      Add Account
    </LoadingButton>
  );
};

Occasionally, when launching link in update mode, it just… does nothing. None of the above console logs fire (except the token, which is present), there are no new network requests, it just appears to die. The ready state is never fired.

Usually waiting a minute and trying again fixes it. Does anyone know what is going on?

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 25 (9 by maintainers)

Most upvoted comments

@neilsanghrajka 3.3.1 was just published. Sorry it took me a bit, got a new laptop last week and had to get all my permissions and dev environment set up again…

@frenkelor @AsLogd @sarink and everyone else, please try out 3.3.1. It includes a bump to the react-script-hook dependency which fixes a bug where the script loading state never fired in some cases, which in turn would cause this hook to think the script was never successfully injected into your page, thus never setting ready=true.

@skylarmb Any timelines on when this fix will be deployed?

Wow great catch @AsLogd! I will publish a version that bumps react-script-hook right away.

@skylarmb I’ve been looking into a related issue. In my case, I had two components that used usePlaidLink, the second one would never reach ready state. I found out that react-script-hook had a bug in 1.5.0 were a second useScript hook would not trigger useEffect. I could replicate the issue using this code in the hooks.legacy.js story.:

import React, { useCallback } from 'react';

import { usePlaidLink } from '../src';

const ButtonPlaid = props => {
  const onSuccess = useCallback(
    (token, metadata) => console.log('onSuccess', token, metadata),
    []
  );

  const onEvent = useCallback(
    (eventName, metadata) => console.log('onEvent', eventName, metadata),
    []
  );

  const onExit = useCallback(
    (err, metadata) => console.log('onExit', err, metadata),
    []
  );

  const config = {
    clientName: props.clientName || '',
    env: props.env || 'sandbox',
    product: props.product || ['auth'],
    publicKey: props.publicKey,
    token: props.token,
    onSuccess,
    onEvent,
    onExit,
    // –– optional parameters
    // webhook: props.webhook || null,
    // countryCodes: props.countryCodes || ['US'],
    // language: props.language || 'en',
    // ...
  };
  const { open, ready, error } = usePlaidLink(config);
  console.log('ready ', props.id, ready);
  return (
    <>
      <button
        type="button"
        className="button"
        onClick={() => open()}
        disabled={!ready}
      >
        Open Plaid Link
      </button>
    </>
  );
};

const App = props => {
  return (
    <React.Fragment>
      <ButtonPlaid {...props} id="first" />
      <ButtonPlaid {...props} id="second" />
    </React.Fragment>
  );
};

export default App;

After upgrading to react-script-hook@1.6.0 the issue was fixed.

If this is the case, then it’s possible that the reason this issue is intermitent is because of race conditions in the code (the hook that gets called first will perform the useEffect, but the others won’t).

This is with 3.3

@skylarmb you can repro with the code in the original post. The token is null on some renders and that’s intentional, because it isn’t available yet.

Rolling my own solution was simple enough that I don’t mind. However I do believe there is some bug to be found in this lib regarding re-renders and the ready flag.

I also think that the API is lacking. token should be replaced with getToken , a function that returns a promise which resolves to a token. ready should not be fired until this has resolved, link is downloaded, and link is initialized.

Perhaps the lib can even memoize getToken and only call it again if the token is expired. Right now this problem is left up to the consumers (which is why you’ll see that I’ve decided to generate a fresh token every time, rather than deal with it), but I don’t see why it should be, since every consumer has the exact same problem

I ended up writing my own hook that calls our api to generate a token and interfaces with window.Plaid directly. It’s actually quite simple and has been a lot more stable for me than using this lib

For reference:

import { useEffect } from 'react';

const PLAID_LINK_STABLE_URL = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';

export const useAppPlaidLink = (options?: { plaidItemId?: string }) => {
  const plaidItemId = options?.plaidItemId;
  const { loading: scriptLoading, error: scriptError } = useScript(PLAID_LINK_STABLE_URL);

  const [getPlaidLinkToken, { data: tokenData, loading: tokenLoading, error: tokenError }] =
    useLazyQuery(GET_PLAID_LINK_TOKEN, {
      fetchPolicy: 'no-cache',
      variables: { plaidItemId },
    });
  const token = tokenData?.getPlaidLinkToken.linkToken;

  const error = scriptError || tokenError;
  const loading = scriptLoading || tokenLoading;

  useEffect(() => {
    if (!loading && !error && token && window.Plaid) {
      const plaid = window.Plaid.create({
        token,
        onSuccess: (publicToken, metadata) => {
          console.log('success', publicToken, metadata);
        },
      });
      plaid.open();
      return () => plaid.destroy();
    }
  }, [loading, error, token, plaidItemId]);

  return {
    error,
    loading,
    open: getPlaidLinkToken,
  };
};
// Usage
const PlaidLinkButton = () => {
  const { open, loading } = useAppPlaidLink();
  return (
    <LoadingButton loading={loading} onClick={() => open()}>
      Add Account
    </LoadingButton>
  );
};