Introduction
Let us continue building our chakra components using styled-components & styled-system. In this tutorial we will be cloning the Chakra UI Image component.
- I would like you to first check the chakra docs for image.
 - All the code for this tutorial can be found under the atom-image branch here.
 
Prerequisite
Please check the Chakra Image Component code here. In this tutorial we will -
- Create an 
useImagehook. - Create an 
Imagecomponent. - Create story for the 
Imagecomponent. 
Setup
- First let us create a branch, from the main branch run -
 
git checkout -b atom-image
Under the
components/atomfolder create an new folder calledimage. Underimagefolder create 3 files -image.tsx,use-image.tsandindex.ts.So our folder structure stands like - src/components/atoms/image.
useImage hook
I would again request you to please check the chakra docs. The Image component takes in a lot of very useful props.
Internally the Image component uses the useImage hook, this hook does some neat tricks. And the benefit of separating the image handling logic into it's own hook is that we can use this hook in other components like
Avatar.First open the
utils/dom.tsfile and paste the following code -
export function canUseDOM(): boolean {
  return !!(
    typeof window !== "undefined" &&
    window.document &&
    window.document.createElement
  );
}
export const isBrowser = canUseDOM();
Now under the src folder create a new folder called hooks. Under
src/hookscreate 2 filesuse-safe-layout-effect.ts&index.ts.useSafeLayoutEffect enables us to safely call
useLayoutEffecton the browser (for SSR reasons). React currently throws a warning when using useLayoutEffect on the server. To get around it, we can conditionally useEffect on the server (no-op) and useLayoutEffect in the browser. Underuse-safe-layout-effect.tspaste the following code -
import * as React from "react";
import { isBrowser } from "../utils";
export const useSafeLayoutEffect = isBrowser
  ? React.useLayoutEffect
  : React.useEffect;
- And under 
hooks/index.tspaste the following code - 
export * from "./use-safe-layout-effect";
- Now let us start with the useImage hook let me first paste the code for you under 
atoms/image/use-image.ts- 
import * as React from "react";
import { useSafeLayoutEffect } from "../../../hooks";
type Status = "loading" | "failed" | "pending" | "loaded";
type ImageEvent = React.SyntheticEvent<HTMLImageElement, Event>;
export interface UseImageProps {
  src?: string;
  srcSet?: string;
  sizes?: string;
  onLoad?(event: ImageEvent): void;
  onError?(error: string | ImageEvent): void;
  ignoreFallback?: boolean;
  crossOrigin?: React.ImgHTMLAttributes<any>["crossOrigin"];
}
export function useImage(props: UseImageProps) {
  const { src, srcSet, onLoad, onError, crossOrigin, sizes, ignoreFallback } =
    props;
  const imageRef = React.useRef<HTMLImageElement | null>();
  const [status, setStatus] = React.useState<Status>("pending");
  React.useEffect(() => {
    setStatus(src ? "loading" : "pending");
  }, [src]);
  const flush = () => {
    if (imageRef.current) {
      imageRef.current.onload = null;
      imageRef.current.onerror = null;
      imageRef.current = null;
    }
  };
  const load = React.useCallback(() => {
    if (!src) return;
    flush();
    const img = new Image();
    img.src = src;
    if (crossOrigin) {
      img.crossOrigin = crossOrigin;
    }
    if (srcSet) {
      img.srcset = srcSet;
    }
    if (sizes) {
      img.sizes = sizes;
    }
    img.onload = (event) => {
      flush();
      setStatus("loaded");
      onLoad?.(event as unknown as ImageEvent);
    };
    img.onerror = (error) => {
      flush();
      setStatus("failed");
      onError?.(error as any);
    };
    imageRef.current = img;
  }, [src, crossOrigin, srcSet, sizes, onLoad, onError]);
  useSafeLayoutEffect(() => {
    if (ignoreFallback) return undefined;
    if (status === "loading") {
      load();
    }
    return () => {
      flush();
    };
  }, [status, load, ignoreFallback]);
  return ignoreFallback ? "loaded" : status;
}
export type UseImageReturn = ReturnType<typeof useImage>;
The basic use of
useImagehook is to return the status of our image, whether it is loading or loaded.We can also pass some cool
onError&onLoadcallback functions as props to theImagecomponent to handle those scenarios theuseImagehook takes care of calling these.More on
useImagelater, let us use it in the Image component.
Image Component
- Under the 
utils/objects.tsfile paste the following code - 
export function omit<T extends Dict, K extends keyof T>(object: T, keys: K[]) {
  const result: Dict = {};
  Object.keys(object).forEach((key) => {
    if (keys.includes(key as K)) return;
    result[key] = object[key];
  });
  return result as Omit<T, K>;
}
- Under the folder 
atoms/image/image.tsxpaste the following code - 
import * as React from "react";
import styled from "styled-components";
import { system, BorderRadiusProps, LayoutProps } from "styled-system";
import { omit } from "../../../utils";
import { useImage, UseImageProps } from "./use-image";
interface ImageOptions {
  fallbackSrc?: string;
  fallback?: React.ReactElement;
  loading?: "eager" | "lazy";
  fit?: React.CSSProperties["objectFit"];
  align?: React.CSSProperties["objectPosition"];
  ignoreFallback?: boolean;
  boxSize?: LayoutProps["size"];
  borderRadius?: BorderRadiusProps["borderRadius"];
}
export interface ImageProps
  extends UseImageProps,
    ImageOptions,
    Omit<React.ComponentPropsWithoutRef<"img">, keyof UseImageProps> {}
const BaseImage = styled.img`
  ${system({
    boxSize: {
      properties: ["width", "height"],
    },
    borderRadius: {
      property: "borderRadius",
    },
  })}
`;
export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
  (props, ref) => {
    const {
      fallbackSrc,
      fallback,
      src,
      align,
      fit,
      loading,
      ignoreFallback,
      crossOrigin,
      alt,
      ...delegated
    } = props;
    const shouldIgnore = loading != null || ignoreFallback;
    const status = useImage({
      ...props,
      ignoreFallback: shouldIgnore,
    });
    const shared = {
      objectFit: fit,
      objectPosition: align,
      ...(shouldIgnore ? delegated : omit(delegated, ["onError", "onLoad"])),
    };
    if (status !== "loaded") {
      if (fallback) return fallback;
      return <BaseImage ref={ref} src={fallbackSrc} alt={alt} {...shared} />;
    }
    return (
      <BaseImage
        ref={ref}
        src={src}
        alt={alt}
        crossOrigin={crossOrigin}
        loading={loading}
        {...shared}
      />
    );
  }
);
First things to notice is that the
Imagecomponent takes inboxSize&borderRadiusprops so we added these to the system().There are 2 separate props namely
fallbackwhich is a React component andfallbackSrc, if the status != "loaded" we return thefallbackif passed or a placeholder instead, whose src is passed using thefallbackSrcprop.If we pass either the
loadingprop orignoreFallbackprop we ignoreFallback anduseImagehook will return 'loaded' meaning we won't show any fallback.Guys I know I am doing a pretty bad job of explaining this, but again try passing these props and write some console logs in the code you will understand it better.
Story
- With the above our 
Imagecomponent is completed, let us create a story. - Under the 
src/components/atoms/image/image.stories.tsxfile we add the below story code - 
import * as React from "react";
import { Flex, Stack } from "../layout";
import { Image } from "./image";
export default {
  title: "Atoms/Image",
};
export const Default = {
  render: () => (
    <Stack direction="row" spacing="3xl">
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sage-adebayo"
        ignoreFallback
        fit="cover"
        alt="Segun Adebayo"
      />
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sage-adebayo"
        fallbackSrc="https://via.placeholder.com/150"
        fit="cover"
        alt="Segun Adebayo"
      />
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sage-adebayo"
        fallback={
          <Flex
            bg="orange500"
            align="center"
            justify="center"
            color="white"
            size="150px"
            borderRadius="9999px"
          >
            Loading...
          </Flex>
        }
        fit="cover"
        alt="Segun Adebayo"
      />
    </Stack>
  ),
};
- Now run 
npm run storybookcheck the stories. Try changing your browser net speed to slow 3G and check the fallback for the second and third image. For the first image it won't show anything as fallback because we passed in ignoreFallback. 
Build the Library
- Under the 
image/index.tsfile paste the following - 
export * from "./image";
export * from "./use-image";
- Under the 
/atom/index.tsfile paste the following - 
export * from "./layout";
export * from "./typography";
export * from "./feedback";
export * from "./icon";
export * from "./icons";
export * from "./form";
export * from "./image";
Now
npm run build.Under the folder
example/src/App.tsxwe can test ourImagecomponent. Copy paste the following code and runnpm run startfrom theexampledirectory. Be sure to set network speed of your browser to slow 3G.
import * as React from "react";
import { Flex, Stack, Image } from "chakra-ui-clone";
export function App() {
  return (
    <Stack m="lg" direction="row" spacing="3xl">
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sage-adebayo"
        ignoreFallback
        fit="cover"
        alt="Segun Adebayo"
      />
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sage-adebayo"
        fallbackSrc="https://via.placeholder.com/150"
        fit="cover"
        alt="Segun Adebayo"
      />
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sa-adebayo"
        fallbackSrc="https://via.placeholder.com/150"
        fit="cover"
        alt="Segun Adebayo"
        onError={() => alert("File Failed to Load")}
      />
      <Image
        boxSize="150px"
        borderRadius="9999px"
        src="https://bit.ly/sage-adebayo"
        fallback={
          <Flex
            bg="orange500"
            align="center"
            justify="center"
            color="white"
            size="150px"
            borderRadius="9999px"
          >
            Loading...
          </Flex>
        }
        fit="cover"
        alt="Segun Adebayo"
      />
    </Stack>
  );
}
Summary
There you go guys in this tutorial we created Image component just like chakra ui . You can find the code for this tutorial under the atom-image branch here. In the next tutorial we will create Avatar component. Until next time PEACE.
    
Top comments (0)