Preact: Getting "TypeError: Converting circular structure to JSON" loading PayPal script

Created on 21 Jul 2019  路  7Comments  路  Source: preactjs/preact

I have used to following solution with Nextjs and react successfully loading a PayPal script.

Preact returns this error:

Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'AnimatedInterpolation'
    |     property 'payload' -> object with constructor 'Array'
    |     index 0 -> object with constructor 'AnimatedValue'
    |     property 'children' -> object with constructor 'Array'
    --- index 0 closes the circle
    at JSON.stringify (<anonymous>)
    at o (js?client-id=sb:1)
    at pr (js?client-id=sb:1)
    at Ar (js?client-id=sb:1)
    at n.e.getPropsRef (js?client-id=sb:1)
    at n.e.buildChildPayload (js?client-id=sb:1)
    at n.e.buildWindowName (js?client-id=sb:1)
    at js?client-id=sb:1
    at i (js?client-id=sb:1)
    at n.e.dispatch (js?client-id=sb:1)
    at n.e.then (js?client-id=sb:1)
    at js?client-id=sb:1
    at Function.n.try (js?client-id=sb:1)
    at n.e.render (js?client-id=sb:1)
    at js?client-id=sb:1
    at Function.n.try (js?client-id=sb:1)
    at r (js?client-id=sb:1)
    at Object.render (js?client-id=sb:1)
    at a.u.componentDidMount (js?client-id=sb:1)
    at N (preact.module.js:1)
    at p.dll_47b5104c397335299b17../node_modules/preact/dist/preact.module.js.p.forceUpdate (preact.module.js:1)
    at w (preact.module.js:1)

My ShoppingCart Component contains this relevant code:

const [ loaded, error ] = useScript(paypalURL)
let PayPalButton
let paypal

if (loaded) {
    paypal = window.paypal
    PayPalButton = paypal && paypal.Buttons.driver('react', { React, ReactDOM })
}

// inside return function
{loaded && (
    <PayPalButton
        style={{
            size: 'medium', // tiny, small, medium
            color: 'black', // orange, blue, silver
            shape: 'rect' // pill, rect
        }}
        createOrder={(data, actions) => {
            return actions.order.create({
             purchase_units: [
                {
                amount: {
                    value: '0.01'
                }
                }
                ]
                })
        }}
        onApprove={(data, actions) => {
             return actions.order.capture().then(function(details) {
            alert('Transaction completed by ' + details.payer.name.given_name)
             })
        }}
    />
)}

useScript code (https://usehooks.com/useScript/):

const cachedScripts = [];

function useScript(src) {
  // Keeping track of script loaded and error state
  const [state, setState] = useState({
    loaded: false,
    error: false,
  });

  useEffect(
    () => {
      // If cachedScripts array already includes src that means another instance ...
      // ... of this hook already loaded this script, so no need to load again.
      if (cachedScripts.includes(src)) {
        setState({
          loaded: true,
          error: false,
        });
      } else {
        cachedScripts.push(src);

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

        // Script event listener callbacks for load and error
        const onScriptLoad = () => {
          setState({
            loaded: true,
            error: false,
          });
        };

        const onScriptError = () => {
          // Remove from cachedScripts we can try loading again
          const index = cachedScripts.indexOf(src);
          if (index >= 0) cachedScripts.splice(index, 1);
          script.remove();

          setState({
            loaded: true,
            error: true,
          });
        };

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

        // Add script to document body
        document.body.appendChild(script);

        // Remove event listeners on cleanup
        return () => {
          script.removeEventListener('load', onScriptLoad);
          script.removeEventListener('error', onScriptError);
        };
      }
    },
    [src], // Only re-run effect if script src changes
  );

  return [state.loaded, state.error];
}

Can you please help me fix the code so that it works?

Do I need to change this: _paypal.Buttons.driver('react', { React, ReactDOM })_ ?

Versions:
"preact": "^10.0.0-rc.0",
"preact-render-to-string": "^5.0.5"
"preact-ssr-prepass": "^1.0.0"
"next": "^9.0.1-canary.1"

Most helpful comment

Is there a way to implement the vanilla-js way into my React-app?

See https://reactjs.org/docs/integrating-with-other-libraries.html for general guidance on integrating UI rendered by third-party (non-React) libraries into a React app.

In brief, you need to first render a DOM element using React for use as a container, then get a reference to that DOM element and pass it to the other library to render its UI.

Using modern React "hooks" APIs this might look something like this:

import { useEffect, useRef } from "preact/hooks"

function App() {
  const paypalButtonContainer = useRef(null);
  useEffect(() => {
    // After the component is mounted, ask the PayPal scripts to render the buttons into the container
    paypal.Buttons().render(paypalButtonContainer.current);
  }, [paypalButtonContainer]);

  // Render a container for the buttons.
  return <div ref={paypalButtonContainer}></div>
}

Before trying to integrate this into your React/Preact app, you might want to try getting this working in just a plain HTML/JS app.

I have read here on GitHub that it is not a good solution due the big bundle size.

Working but slightly inefficient code beats non-working code 馃槢, especially when it comes to people paying you money for something.

All 7 comments

Can you provide some context about what you are trying to do? It looks like you are trying to integrate the PayPal buttons into a site? I don't see any mention of the React APIs on that site.

From a quick skim of the stack trace, it looks like the error is to do with the Paypal components trying to render some non-trivial React components into the place where your PayPalButton is mounted in the DOM. Unless you're willing to spend time debugging why these components are not working with Preact, and keeping it working as PayPal release new updates (which can happen even when you are not updating your own site), I'd suggest sticking to the documented vanilla JS APIs that the PayPal buttons have.

Thank you for the quick answer @robertknight! Yes, sorry I missed describing the main intention, to integrate the PayPal Checkout component.

The official PayPal repository recommends the following solution for react:
https://github.com/paypal/paypal-checkout-components/blob/master/demo/react.htm

The only difference to my solution is how the script is loaded.

Do you have an idea why it works with React but not Preact?

Do you have an idea why it works with React but not Preact?

I do not. That's a possibly non-trivial debugging exercise. If you can post a non-working demo somewhere it might help someone to debug the issue.

Even if using React I suggest it would be unwise to use the approach from that not-well-supported-looking demo as the buttons could potentially break if you happen to be using a different major version of React in your application than the one that the buttons were tested with.

If you were using npm to import the PayPal scripts then you could fix them at a version that worked with the version of React you are using. Since you are loading the PayPal scripts from their website, you have no control over exactly which version of their scripts you get.

The error is replicated in this small repository:
https://github.com/secretlifeof/paypal-preact-demo

I am still relatively new to programming. Yes bad version control seems like a good argument. I have read here on GitHub that it is not a good solution due the big bundle size.

Is there a way to implement the vanilla-js way into my React-app?

Is there a way to implement the vanilla-js way into my React-app?

See https://reactjs.org/docs/integrating-with-other-libraries.html for general guidance on integrating UI rendered by third-party (non-React) libraries into a React app.

In brief, you need to first render a DOM element using React for use as a container, then get a reference to that DOM element and pass it to the other library to render its UI.

Using modern React "hooks" APIs this might look something like this:

import { useEffect, useRef } from "preact/hooks"

function App() {
  const paypalButtonContainer = useRef(null);
  useEffect(() => {
    // After the component is mounted, ask the PayPal scripts to render the buttons into the container
    paypal.Buttons().render(paypalButtonContainer.current);
  }, [paypalButtonContainer]);

  // Render a container for the buttons.
  return <div ref={paypalButtonContainer}></div>
}

Before trying to integrate this into your React/Preact app, you might want to try getting this working in just a plain HTML/JS app.

I have read here on GitHub that it is not a good solution due the big bundle size.

Working but slightly inefficient code beats non-working code 馃槢, especially when it comes to people paying you money for something.

I'm closing this issue as this I think the query has been addressed.

You made me laugh loudly reading your comment :)

Thank you again for writing such a complete answer! It was really helpful. I will try to implement this as you describe.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mizchi picture mizchi  路  3Comments

jasongerbes picture jasongerbes  路  3Comments

matthewmueller picture matthewmueller  路  3Comments

nopantsmonkey picture nopantsmonkey  路  3Comments

SabirAmeen picture SabirAmeen  路  3Comments