Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Deferred Overview

Remix Deferred

Remix Deferred is currently implemented on top of React's Suspense model but is not limited to React. This will be a quick dive into how "promise over the wire" is accomplished.

SSR + Hydration

It isn't rocket science, but a quick recap of how frameworks such as react do SSR:

  1. Load data
  2. Render the app
  3. Serialize the data
// 1. load data
const data = await loadData(request);
// 2. render app
const appHtml = toString(<App data={data} />);

response.end(
  `
    <!DOCTYPE>
    <html>
      <div id="app">
        ${appHtml}
      </div>
      <!-- 3. serialize data -->
      <script>window.__data = ${JSON.stringify(data)};</script>
      <script src="entry.client.js"></script>
    </html>
  `
);

Then on the client side, hydration occurs by reading the serialized data from the window:

const data = window.__data;
hydrate(document.getElementById("app"), <App data={data} />);

Simple yet effective.

How Deferred Differs

Remix deferred operates on the same general pattern as your standard SSR + Hydration setup, with the caveat of if a promise is returned as a data key, it "teleports" over the network and arrives in the browser also as a promise.

A high level of how this is accomplished can be understood by the following snippet:

  1. Load data
  2. Render the app
  3. Separate critical & deferred data
  4. Serialize critical data
  5. Teleport promises
  6. Transport promise resolutions
// 1. load data
const data = await loadData(request);
// 2. render app
const appHtml = toString(<App data={data} />);

// 3. separate critical & deferred data
const criticalData = {};
const deferredData = {};
const deferredSetupScripts = [
  // function used to setup deferred promises
  `
    window.__deferred = {};
    function setupDeferredPromise(key) {
      const promise = new Promise((resolve, reject) => {
        window.__deferred[key] = {
          resolve,
          reject,
        };
      });
      window.__data[key] = promise;
    }
  `,
  // function used to resolve / reject deferred promises
  `
    function resolveDeferredPromise(key, reason, result) {
      if (reason) {
        window.__deferred[key].reject(reason);
      } else {
        window.__deferred[key].resolve(result);
      }
    }
  `,
];
for (const [key, value] of Object.entries(data)) {
  if (value && value instanceof Promise) {
    deferredData[key] = value;

    deferredSetupScripts.push(`
      setupDeferredPromise(${JSON.stringify(key)});
    `);
  } else {
    criticalData[key] = value;
  }
}

response.send(
  `
    <!DOCTYPE>
    <html>
      <div id="app">
        ${appHtml}
      </div>
      <script>
        // 4. serialize critical data
        window.__data = ${JSON.stringify(criticalData)};

        // 5. teleport promises
        ${deferredSetupScripts.join("\n")}
      </script>
      <script src="entry.client.js"></script>
    </html>
  `
);

// 5. transport promise resolutions
await Promise.allSettled(
  Object.entries(deferredData).map(async ([key, promise]) => {
    try {
      const result = await promise;
      response.send(
        `
          <script>
            resolveDeferredPromise(
              ${JSON.stringify(key)},
              undefined,
              ${JSON.stringify(result)}
            );
          </script>
        `
      );
    } catch (reason) {
      response.send(
        `
          <script>
            const reason = new Error(${JSON.stringify(reason.message)});
            ${
              reason.stack
                ? `reason.stack = ${JSON.stringify(reason.stack)}`
                : ""
            }
            resolveDeferredPromise(
              ${JSON.stringify(key)},
              reason,
            );
          </script>
        `
      );
    }
  })
);

response.end();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment