From e2b5cb76bf93f9ee57d16b86a47bfdaf77dd2130 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 1 Nov 2020 19:45:31 -0800 Subject: [PATCH] Use suspense to ensure desktop images load atomically --- packages/webamp/demo/js/DemoDesktop.tsx | 40 +++++---- packages/webamp/demo/js/DesktopIcon.tsx | 3 +- packages/webamp/demo/js/DesktopLinkIcon.tsx | 3 +- packages/webamp/demo/js/IconImage.tsx | 9 ++ packages/webamp/demo/js/SuspenseImage.tsx | 97 +++++++++++++++++++++ 5 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 packages/webamp/demo/js/IconImage.tsx create mode 100644 packages/webamp/demo/js/SuspenseImage.tsx diff --git a/packages/webamp/demo/js/DemoDesktop.tsx b/packages/webamp/demo/js/DemoDesktop.tsx index 4ce435cd..61b4af08 100644 --- a/packages/webamp/demo/js/DemoDesktop.tsx +++ b/packages/webamp/demo/js/DemoDesktop.tsx @@ -1,5 +1,5 @@ import WebampLazy from "../../js/webampLazy"; -import React from "react"; +import React, { Suspense } from "react"; import WebampIcon from "./WebampIcon"; // import Mp3Icon from "./Mp3Icon"; import SkinIcon from "./SkinIcon"; @@ -66,23 +66,27 @@ const DemoDesktop = ({ webamp }: Props) => { marginLeft: HORIZONTAL_MARGIN, }} > - {icons.map((icon, i) => { - const row = Math.floor(i / columns); - const column = i % columns; - return ( -
- {icon} -
- ); - })} + + {icons.map((icon, i) => { + const row = Math.floor(i / columns); + const column = i % columns; + return ( +
+ {icon} +
+ ); + })} +
); }; diff --git a/packages/webamp/demo/js/DesktopIcon.tsx b/packages/webamp/demo/js/DesktopIcon.tsx index 791caba8..d76753f1 100644 --- a/packages/webamp/demo/js/DesktopIcon.tsx +++ b/packages/webamp/demo/js/DesktopIcon.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef } from "react"; import classnames from "classnames"; +import IconImage from "./IconImage"; interface Props { iconUrl: string; @@ -37,7 +38,7 @@ const DesktopIcon = ({ iconUrl, onOpen, name, onDragStart }: Props) => { className={classnames("desktop-icon", { selected })} onDragStart={onDragStart} > - +
{name}
); diff --git a/packages/webamp/demo/js/DesktopLinkIcon.tsx b/packages/webamp/demo/js/DesktopLinkIcon.tsx index 7f88d95a..08130604 100644 --- a/packages/webamp/demo/js/DesktopLinkIcon.tsx +++ b/packages/webamp/demo/js/DesktopLinkIcon.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef } from "react"; import classnames from "classnames"; +import IconImage from "./IconImage"; interface Props { iconUrl: string; @@ -43,7 +44,7 @@ const DesktopLinkIcon = ({ iconUrl, href, name }: Props) => { }} className={classnames("desktop-icon", { selected })} > - +
{name}
); diff --git a/packages/webamp/demo/js/IconImage.tsx b/packages/webamp/demo/js/IconImage.tsx new file mode 100644 index 00000000..6fc1b74c --- /dev/null +++ b/packages/webamp/demo/js/IconImage.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import SuspenseImage from "./SuspenseImage"; + +type Props = { + src: string; +}; +export default function IconImage({ src }: Props) { + return ; +} diff --git a/packages/webamp/demo/js/SuspenseImage.tsx b/packages/webamp/demo/js/SuspenseImage.tsx new file mode 100644 index 00000000..435fb776 --- /dev/null +++ b/packages/webamp/demo/js/SuspenseImage.tsx @@ -0,0 +1,97 @@ +import React from "react"; + +// A Resource is an object with a read method returning the payload +interface Resource { + read: () => Payload; +} + +type status = "pending" | "success" | "error"; + +// this function let us get a new function using the asyncFn we pass +// this function also receives a payload and return us a resource with +// that payload assigned as type +function createResource( + asyncFn: () => Promise +): Resource { + // we start defining our resource is on a pending status + let status: status = "pending"; + // and we create a variable to store the result + let result: any; + // then we immediately start running the `asyncFn` function + // and we store the resulting promise + const promise = asyncFn().then( + (r: Payload) => { + // once it's fulfilled we change the status to success + // and we save the returned value as result + status = "success"; + result = r; + }, + (e: Error) => { + // once it's rejected we change the status to error + // and we save the returned error as result + status = "error"; + result = e; + } + ); + // lately we return an error object with the read method + return { + read(): Payload { + // here we will check the status value + switch (status) { + case "pending": + // if it's still pending we throw the promise + // throwing a promise is how Suspense know our component is not ready + throw promise; + case "error": + // if it's error we throw the error + throw result; + case "success": + // if it's success we return the result + return result; + } + }, + }; +} + +// First we need a type of cache to avoid creating resources for images +// we have already fetched in the past +const cache = new Map(); + +// then we create our loadImage function, this function receives the source +// of the image and returns a resource +function loadImage(source: string): Resource { + // here we start getting the resource from the cache + let resource = cache.get(source); + // and if it's there we return it immediately + if (resource) return resource; + // but if it's not we create a new resource + resource = createResource( + () => + // in our async function we create a promise + new Promise((resolve, reject) => { + // then create a new image element + const img = new window.Image(); + // set the src to our source + img.src = source; + // and start listening for the load event to resolve the promise + img.addEventListener("load", () => resolve(source)); + // and also the error event to reject the promise + img.addEventListener("error", () => + reject(new Error(`Failed to load image ${source}`)) + ); + }) + ); + // before finishing we save the new resource in the cache + cache.set(source, resource); + // and return return it + return resource; +} + +export default function SuspenseImage( + props: React.ImgHTMLAttributes +): JSX.Element { + if (props.src != null) { + loadImage(props.src).read(); + } + return ; +}