contentful / extensions

Repository providing samples using the UI Extensions SDK
ISC License
203 stars 211 forks source link

initCallback is not always called #354

Closed pixelass closed 3 years ago

pixelass commented 3 years ago

We use a react-hook to get access to the SDK in our contentful-extension.

Since our extensions are running in next.js, we use this mechanism to allow server side rendering. When deployed this works as expected but if we host this extension on localhost the chance of the callback being called seems to be 1:100. (see logs in the code-sample below)

We are using this extension in Contentful (not looking at the localhost itself as we are aware that it won't work outside app.contentful.com)

Here's a screenshot of what we see in contentful: (we show a loader until SDK has been loaded)

image

Here's the actual code that we use:

import React from "react";
import { FieldExtensionSDK } from "contentful-ui-extensions-sdk";

export const useContentfulSdk = () => {
    const [contentfulSdk, setContentfulSdk] = React.useState<null | FieldExtensionSDK>(null);
    React.useEffect(() => {
        (async () => {
            const { init } = await import("contentful-ui-extensions-sdk");
            console.log(typeof sdk); // just for debugging. works 100% ("function")

            init(sdk => {
                console.log("SDK loaded"); // just for debugging. works ~1% 💩
                setContentfulSdk(sdk as FieldExtensionSDK);
            });
        })();
    }, []);
    return contentfulSdk;
};
andipaetzold commented 3 years ago

Hey @pixelass,

I never built a Contentful extension using next.js but I can explain the mechanics behind init that might help you to solve the issue.

When importing contentful-ui-extensions-sdk, the SDK waits for a connection from the Contentful web app. This connection is attempted by the web app as soon as the iframe finished loading. init can be called later, but we also recommend calling it immediately while the page is loading.

I assume that your code fails because the second line is removed by the compiler as it is only a type import. Therefore, your extension only starts listening to the connection of the Contentful web app after dynamically importing the SDK using await import("contentful-ui-extensions-sdk"). Depending on the timings, the web app already tried to connect at that point and failed. The web app does not retry to connect to an extension.

The callback passed as an argument to init is only called when the web app was able to connect to your extension.

I hope this explanation helps you to solve your issue.

pixelass commented 3 years ago

@andipaetzold thank you for that comment. It's basically what I thought might happen. we played around with several things but what's irritating is that we never see the issue once our extension is deployed and not called from localhost.

We cannot import contentful-ui-extensions-sdk outside the React.useEffect since the package references window and is therefore not compatible with SSR without some kind of lazy loading.

andipaetzold commented 3 years ago

Okay, this seems to be a general issue with SSR rendering and our SDK then.

What could work is importing the SDK via a script tag in your html head.

<script src="https://unpkg.com/contentful-ui-extensions-sdk@3"></script>

That way, no JavaScript is executed on the server but the SDK is still loaded immediately on page load. You can then access the init function via window.contentfulExtension.init. This is not a perfect solution as you will probably lose type safety or have to add some custom typings - but it should allow you to build a Contentful extension using SSR.

pixelass commented 3 years ago

@andipaetzold That sounds like a valid workaround. Thank you I'll give it a try and report back.

pixelass commented 3 years ago

We made these changes and they seem to work as expected:

While this could be fixed in this library (to support SSR out of the box) we'll consider it as fixed and close this issue. Feel free to comment if you need help setting this up.

import React from "react";
import Head from "next/head";
import "@contentful/forma-36-react-components/dist/styles.css";
import "../styles/index.css";

// @ts-ignore
const App = ({ Component, pageProps }) => {
    return (
        <>
            <Head>
                <title>Contentful Extensions</title>
                <script src="https://unpkg.com/contentful-ui-extensions-sdk@3" />
            </Head>
            <Component {...pageProps} />
        </>
    );
};

export default App;
import React from "react";
import { FieldExtensionSDK } from "contentful-ui-extensions-sdk";

declare global {
    interface Window {
        contentfulExtension: any;
    }
}

export const useContentfulSdk = () => {
    const [contentfulSdk, setContentfulSdk] = React.useState<null | FieldExtensionSDK>(null);
    React.useEffect(() => {
        (async () => {
            window.contentfulExtension.init((sdk: FieldExtensionSDK) => {
                setContentfulSdk(sdk);
            });
        })();
    }, []);
    return contentfulSdk;
};