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
useImage
hook. - Create an
Image
component. - Create story for the
Image
component.
Setup
- First let us create a branch, from the main branch run -
git checkout -b atom-image
Under the
components/atom
folder create an new folder calledimage
. Underimage
folder create 3 files -image.tsx
,use-image.ts
andindex.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.ts
file 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/hooks
create 2 filesuse-safe-layout-effect.ts
&index.ts
.useSafeLayoutEffect enables us to safely call
useLayoutEffect
on 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.ts
paste the following code -
import * as React from "react";
import { isBrowser } from "../utils";
export const useSafeLayoutEffect = isBrowser
? React.useLayoutEffect
: React.useEffect;
- And under
hooks/index.ts
paste 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
useImage
hook is to return the status of our image, whether it is loading or loaded.We can also pass some cool
onError
&onLoad
callback functions as props to theImage
component to handle those scenarios theuseImage
hook takes care of calling these.More on
useImage
later, let us use it in the Image component.
Image Component
- Under the
utils/objects.ts
file 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.tsx
paste 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
Image
component takes inboxSize
&borderRadius
props so we added these to the system().There are 2 separate props namely
fallback
which is a React component andfallbackSrc
, if the status != "loaded" we return thefallback
if passed or a placeholder instead, whose src is passed using thefallbackSrc
prop.If we pass either the
loading
prop orignoreFallback
prop we ignoreFallback anduseImage
hook 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
Image
component is completed, let us create a story. - Under the
src/components/atoms/image/image.stories.tsx
file 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 storybook
check 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.ts
file paste the following -
export * from "./image";
export * from "./use-image";
- Under the
/atom/index.ts
file 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.tsx
we can test ourImage
component. Copy paste the following code and runnpm run start
from theexample
directory. 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)