Skip to content

Instantly share code, notes, and snippets.

@TheThirdRace
Last active February 6, 2024 15:20
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save TheThirdRace/7f270a786629f119b57d1b2227a4b113 to your computer and use it in GitHub Desktop.
Save TheThirdRace/7f270a786629f119b57d1b2227a4b113 to your computer and use it in GitHub Desktop.
Image component: merge Chakra-ui `2.8.0` with NextJs `13.4.13` and remove most pain points
/**
* # `<Image>`
*
* This component is a merge between `next/image` and `Chakra-ui`.
* - last updated on 2023-08-08 with `next/image` 13.4.13 and `chakra-ui/react` 2.8.0
* - https://github.com/vercel/next.js/blob/v13.4.13/packages/next/src/client/image-component.tsx
* - https://github.com/vercel/next.js/blob/canary/packages/next/src/client/image-component.tsx
* - https://github.com/vercel/next.js/commits/canary/packages/next/src/client/image-component.tsx
* - https://github.com/vercel/next.js/compare/v13.4.4...canary
*
* Associated `gist`: <https://gist.github.com/TheThirdRace/7f270a786629f119b57d1b2227a4b113>
*
* ## Pros
*
* - Use NextJs backend solution so you get `static` or `on the fly` image optimization
* - Offer the same optimizations as `next/image` (lazy loading, priority, async decoding, no CLS, blur placeholder, etc.)
* - Use Chakra's theme (`variant`) so you have full control of styling
* - `<img>` is back to `display: inline-block` by default
* - Forward ref to `<img>`
* - No more fiddling with `onLoadComplete` callback from `next/image`
* - You can determine when an image is completely loaded
* - You can pass a callback `ref` and check if `data-loaded` is `true`
* - You can use `css` to target `[data-loaded=true]`
* - All icons are served automatically through the `1x` or `2x` pixel density optimization
* - Passing `sizesMax={0}` can force a bigger image to be served in the `1x` and `2x` mode
* - All images are served automatically through an `srcset` auto-build function
* - Load configs through `NextJs` config
* - No more fiddling trying to build a `sizes` manually
* - Simply pass `sizesMax={ImageMaxWidth}` or don't pass `sizesMax` at all (defaults to highest possible value)
* - `sizesMax` allows you to limit image width according to your design, not the viewport
* - No more loading a 3840px wide image on a 4K screen when your `main` section is 1200px
* - Use semantic HTML tags
* - `<img>` is used for the image
* - `<picture>` is used for the wrapper/container (optional)
* - `height` & `width` are extremely recommended, but not mandatory
* - Can use a blurry placeholder for better user experience and core vitals
* - Automatic when using static images (`import`)
* - You can manually pass a data uri for dynamic images
* - Low `height` and `width` images like icons won't apply the blurry placeholder as it lower performance
* - `loader` function allow to build the final `src` url, so you can support many image providers
* - Possible to use with a **secure** `Content-Security-Policy` header
* - Extra performance by using `content-visibility: auto` on the `<picture>` wrapper
* - Not available by default on `<img>` to avoid scrolling up issues on Chrome
* - Could be added manually on `<img>` through styles if wanted
* - Smaller than `next/image` solution by almost 200 lines of code
* - Smaller by almost 450 lines of codes if you count all the extra messages from development (which are loaded in PROD)
*
* ## Cons
*
* - Doesn't support Chakra's inline styling (by personal choice, could easily be added)
* - Using a different `backgroundSize`/`backgroundPosition` from default requires to style the `blur` placeholder
* - Use native `loading=lazy`, meaning the feature isn't supported for all browsers yet
* - Automatic blurry placeholder generation only works when your source image is a avif, jpg, png or webp
* - Same restrictions as NextJs since the component use their image optimization solution
* - Be advised, the "source" image is not the image served to your users, it's the unoptimized image before optimization
* - Using `<img>` without it's wrapper (`<picture>`) will give a very low CLS instead of none (ex: 0.03)
* - Serving "responsive" images can increase data consumption, but this should be negligible because:
* - Images are optimized to a low size to begin with
* - Those most affected are users with big screens, which usually don't mind more data
* - Users don't resize their browser window non-stop
*
* ## Tips & Tricks
*
* ### Optimization
*
* - Pass `width` & `height` whenever you can, it's the biggest optimization you're gonna get out of the box
* - Use `import` method for your images, it improves your Core Web Vitals and the user experience
*
* ### `<picture>` wrapper
*
* - Will be added automatically under these circumstances
* - Pass `width` & `height` props
* - Define a style for Image's `layPicture` part in the theme
* - `<picture>` wrapper is mandatory to reach a cumulative layout shift (CLS) of 0
* - This implementation will always have a CLS of 0, no matter if it's a newer or older browser
* - The new `next/image` in NextJS `13.x` won't have 0 CLS, it'll get close on newer browser, but older browsers will have huge CLS
* - You won't be penalized by Google ranking as long as you keep CLS < 0.1, which makes the wrapper "optional"
*
* ### `sizesMax`
*
* - Pass `sizesMax={0}` to force an image to be optimized with `srcset` containing `1x, 2x` variants
* - Mostly for icons, but you could use this for normal images too
* - Don't pass `sizesMax` to force an image to be optimized for the current viewport width
* - If an image is less than the full screen's width, you can pass its max size like this `sizesMax={992}`
*/
import { chakra } from '@chakra-ui/react'
import Head from 'next/head'
import { type ImageProps as NextImageProps, type StaticImageData } from 'next/image'
import { forwardRef, useImperativeHandle, useState, type Dispatch, type ReactElement, type SetStateAction } from 'react'
import {
defaultLoader,
useImageAttributes,
useImageOnLoad,
useImageStyle,
type GenerateImageAttributesReturn,
type ImageProps
} from '~/helper/Image'
import { Rename } from '~/shared/type/Typescript'
/** *******************************************************************************************************************
* Types
*/
type ImageNativeProps = Partial<Pick<HTMLImageElement, 'alt'>> &
Partial<Rename<Pick<HTMLImageElement, 'height'>, 'height', 'htmlHeight'>> &
Partial<Rename<Pick<HTMLImageElement, 'width'>, 'width', 'htmlWidth'>> & {
'data-set-load-state': Dispatch<SetStateAction<boolean>>
}
type ImagePriorityProps = Pick<NextImageProps, 'crossOrigin' | 'priority'> &
Pick<GenerateImageAttributesReturn, 'sizes' | 'src' | 'srcset'>
type StaticImageProps = Pick<StaticImageData, 'height' | 'src' | 'width'> & Pick<StaticImageData, 'blurDataURL'>
/** *******************************************************************************************************************
* * Components *
*/
const ImageNative = forwardRef<HTMLImageElement, ImageNativeProps>(
({ alt, htmlWidth, htmlHeight, 'data-set-load-state': setLoadState, ...chakraInternals }: ImageNativeProps, ref) => {
// Handle refs to the same element
// 1. `imgRef` => from `useRef` and is used to link `ref` with `callbackRef` (link between internal and external refs)
// 2. `ref` => from `forwardRef` and is used to give access to the internal ref from the parent
// 3. `callbackRef` => from `useCallback` and is used to set image loaded state even on static rendered pages
//
// Inspired by
// - https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780
// - https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
const { callbackRef, imgRef } = useImageOnLoad({ setLoadState })
useImperativeHandle<HTMLImageElement | null, HTMLImageElement | null>(ref, () => imgRef.current)
return (
// eslint-disable-next-line @next/next/no-img-element
<img
alt={alt}
height={htmlHeight}
ref={callbackRef} // ? use callback ref to catch when it updates
width={htmlWidth}
// eslint-disable-next-line react/jsx-props-no-spreading
{...chakraInternals}
/>
)
}
)
const ImagePriority = ({ crossOrigin, sizes, src, srcset }: ImagePriorityProps): ReactElement => {
return (
// Note how we omit the `href` attribute, as it would only be relevant
// for browsers that do not support `imagesrcset`, and in those cases
// it would likely cause the incorrect image to be preloaded.
//
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
<Head>
<link
as='image'
crossOrigin={crossOrigin}
fetchpriority='high' // eslint-disable-line react/no-unknown-property
href={srcset ? undefined : src}
imageSizes={sizes}
imageSrcSet={srcset}
key={`__nimg-${src}${srcset}${sizes}`}
rel='preload'
/>
</Head>
)
}
export const Image = forwardRef<HTMLImageElement, ImageProps>(
(
{
alt,
blurDataURL: paramBlurDataURL,
crossOrigin,
height: paramHeight,
loader = defaultLoader,
priority = false,
quality,
sizesMax,
src: paramSrc,
sx,
title,
variant,
width: paramWidth,
...chakraInternals
}: ImageProps,
ref
): ReactElement => {
// Manage values according to image mode: Static or Dynamic
const { blurDataURL, height, src, width } =
typeof paramSrc === 'string'
? {
blurDataURL: paramBlurDataURL,
height: paramHeight,
src: paramSrc,
width: paramWidth
}
: ({
...paramSrc,
...(paramHeight ? { height: paramHeight } : {}),
...(paramWidth ? { width: paramWidth } : {})
} as StaticImageProps)
// Keep trace of when the image is loaded
const [imgLoaded, setImgLoaded] = useState(false)
// Retrieve styling
const { styles, withWrapper } = useImageStyle({ blurDataURL, height, imgLoaded, src, variant, width })
// Retrieve image attributes
const {
src: imgSrc,
srcset: imgSrcSet,
sizes: imgSizes
} = useImageAttributes({
loader,
quality,
sizesMax,
src,
width
})
// Image component
const imgProps = {
as: ImageNative,
alt,
decoding: 'async' as const,
...(priority ? { fetchpriority: 'high' } : { loading: 'lazy' as const }),
htmlHeight: height,
htmlWidth: width,
'data-set-load-state': setImgLoaded,
'data-loaded': imgLoaded,
ref,
// ? `src` must be the last parameter within those 3
// ? Safari has a bug that would download the image in `src` before `sizes` and `srcset`
// ? are set and then download a second image when both are set.
// ?
// ? By putting `src` in last position, Safari won't initiate a download until `src` is
// ? updated in the DOM correctly,
sizes: imgSizes,
srcSet: imgSrcSet,
src: imgSrc,
// ? --------------------------------------------------------------------------------------,
sx: styles.image,
title,
// eslint-disable-next-line react/jsx-props-no-spreading,
...chakraInternals
}
const img = (
<>
<chakra.img
// eslint-disable-next-line react/jsx-props-no-spreading
{...imgProps}
/>
<noscript>
<chakra.img
// eslint-disable-next-line react/jsx-props-no-spreading
{...imgProps}
sx={styles.imageNoScript}
/>
</noscript>
</>
)
// Add a `<picture>` wrapper if required
const image = withWrapper ? <chakra.picture sx={{ ...styles.picture, ...sx }}>{img}</chakra.picture> : img
return (
<>
{image}
{priority ? (
<ImagePriority crossOrigin={crossOrigin} sizes={imgSizes} src={imgSrc} srcset={imgSrcSet} />
) : undefined}
</>
)
}
)
import { useMultiStyleConfig, type ChakraProps, type SystemStyleObject, type ThemingProps } from '@chakra-ui/react'
import { mergeWith } from '@chakra-ui/utils'
import { imageConfigDefault, type ImageConfigComplete } from 'next/dist/shared/lib/image-config'
import { type ImageLoaderProps, type ImageProps as NextImageProps } from 'next/image'
import { Dispatch, MutableRefObject, SetStateAction, useCallback, useMemo, useRef } from 'react'
/** *******************************************************************************************************************
* Types
*/
export type ImageProps = Pick<NextImageProps, 'blurDataURL' | 'crossOrigin' | 'priority' | 'src'> &
Partial<Pick<HTMLImageElement, 'alt' | 'height' | 'title' | 'width'>> &
Pick<ChakraProps, 'sx'> &
Pick<ThemingProps, 'variant'> & {
loader?: ImageLoaderWithConfig
quality?: number
sizesMax?: SizesMax
}
type GenerateCumulativeLayoutShiftFixProps = Pick<ImageProps, 'height' | 'sizesMax' | 'width'>
type GenerateImageAttributesProps = Required<Pick<ImageProps, 'loader'>> &
Pick<ImageProps, 'quality' | 'sizesMax' | 'width'> &
Pick<HTMLImageElement, 'src'>
export type GenerateImageAttributesReturn = Pick<HTMLImageElement, 'src'> &
Partial<Pick<HTMLImageElement, 'sizes' | 'srcset'>>
type ImageConfig = ImageConfigComplete & { allSizes: number[] }
type ImageLoaderWithConfig = (resolverProps: ImageLoaderPropsWithConfig) => string
type ImageLoaderPropsWithConfig = ImageLoaderProps & {
config: Readonly<ImageConfig>
}
type IsLayoutProps = Pick<ImageProps, 'sizesMax' | 'width'>
type UseImageOnLoadProps = {
setLoadState: Dispatch<SetStateAction<boolean>>
}
/**
* ! Makes sure `contentMaxWidthInPixel` from `page.ts` is included in `SizeMax`
* ! Makes sure values here are in sync with `next.config.js`
*/
export type SizesMax = 0 | 320 | 480 | 640 | 750 | 828 | 992 | 1080 | 1200 | 1440 | 1920 | 2048 | 2560 | 3840
type UseImageOnLoadReturn = {
callbackRef: (img: HTMLImageElement) => void
imgRef: MutableRefObject<HTMLImageElement | null>
}
type UseImageStyleProps = Pick<ImageProps, 'blurDataURL' | 'height' | 'sizesMax' | 'variant' | 'width'> &
Pick<HTMLImageElement, 'src'> & {
imgLoaded: boolean
}
type UseImageStyleReturn = {
styles: {
image: SystemStyleObject
imageNoScript: SystemStyleObject
picture: SystemStyleObject
}
withWrapper: boolean
}
/** *******************************************************************************************************************
* * Image configurations *
* https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/image-config.ts
*/
const defaultBlurDataURL =
// 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAA3NCSVQICAjb4U/gAAAAIElEQVQYlWNUUlJiwA1Y/v37h0/6////FOgeSMMpshsAm54bX5qzRrgAAAAASUVORK5CYII=',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNU+g8AAUkBI5mqlHIAAAAASUVORK5CYII='
const defaultQuality = 75
const tmpConfig: ImageConfigComplete = mergeWith(
{},
imageConfigDefault,
process.env.__NEXT_IMAGE_OPTS as unknown as ImageConfigComplete
)
const imageConfig: ImageConfig = mergeWith({}, tmpConfig, {
allSizes: [...tmpConfig.imageSizes, ...tmpConfig.deviceSizes].sort((a, b) => a - b)
})
const { allSizes: configAllSizes, deviceSizes: configDeviceSizes, imageSizes: configImageSizes } = imageConfig
/** *******************************************************************************************************************
* * Functions *
*/
export const defaultLoader = ({ config, src, width, quality = defaultQuality }: ImageLoaderPropsWithConfig): string =>
src.endsWith('.svg') && !config.dangerouslyAllowSVG
? src
: `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`
const isLayoutFixed = ({ sizesMax, width = configDeviceSizes[configDeviceSizes.length - 1] }: IsLayoutProps): boolean =>
sizesMax === 0 || width < configDeviceSizes[0] || configImageSizes.includes(width)
const generateCumulativeLayoutShiftFix = ({ height, sizesMax, width }: GenerateCumulativeLayoutShiftFixProps) => {
let clsFix = {}
if (height && width) {
clsFix = {
aspectRatio: `${width}/${height}`,
...(isLayoutFixed({ sizesMax, width })
? {
height: `${height}px`,
width: `${width}px`
}
: {
paddingBlockStart: `calc(${height} / ${width} * 100%)`
})
}
}
return clsFix
}
export const useImageAttributes = ({
loader,
quality,
sizesMax,
src,
width = configDeviceSizes[configDeviceSizes.length - 1]
}: GenerateImageAttributesProps): GenerateImageAttributesReturn => {
return useMemo(() => {
let imgAttributes: GenerateImageAttributesReturn
if (src && (src.startsWith('data:') || src.startsWith('blob:'))) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
imgAttributes = { src, srcset: undefined, sizes: undefined }
} else if (isLayoutFixed({ sizesMax, width })) {
const widths = [
...new Set(
/**
* This means that most OLED screens that say they are 3x resolution, are actually 3x in the green color,
* but only 1.5x in the red and blue colors.
*
* Showing a 3x resolution image in the app vs a 2x resolution image will be visually the same, though the
* 3x image takes significantly more data. Even true 3x resolution screens are wasteful as the human eye
* cannot see that level of detail without something like a magnifying glass.
*
* https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html
*/
[width, width * 2].map(
(w) => configAllSizes.find((s) => s >= w) || configAllSizes[configAllSizes.length - 1]
)
)
]
imgAttributes = {
sizes: undefined,
src: loader({ config: imageConfig, src, quality, width: widths[1] }),
srcset: widths
.map((w, i) => `${loader({ config: imageConfig, src, quality, width: w })} ${i + 1}x`)
.join(', ')
}
} else {
const maxSizes = sizesMax || configDeviceSizes[configDeviceSizes.length - 1]
const widths = [...configDeviceSizes.filter((w) => w < maxSizes), maxSizes]
imgAttributes = {
sizes: widths
.map((w, i) => {
return i < widths.length - 1 ? ` (max-width: ${w}px) ${w}px` : ` ${w}px`
})
.join(','),
src: loader({ config: imageConfig, src, quality, width: widths[widths.length - 1] }),
srcset: widths.map((w) => `${loader({ config: imageConfig, src, quality, width: w })} ${w}w`).join(', ')
}
}
return imgAttributes
}, [loader, quality, sizesMax, src, width])
}
export const useImageOnLoad = ({ setLoadState }: UseImageOnLoadProps): UseImageOnLoadReturn => {
// Handle refs to the same element
// 1. `imgRef` => from `useRef` and is used to link `ref` with `callbackRef` (link between internal and external refs)
// 2. `ref` => from `forwardRef` and is used to give access to the internal ref from the parent
// 3. `callbackRef` => from `useCallback` and is used to set image loaded state even on static rendered pages
//
// Inspired by
// - https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780
// - https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
const imgRef = useRef<HTMLImageElement | null>(null)
const callbackRef = useCallback(
// ? Because the page could be static rendered, the image could already be loaded before React registers the image's `onload` event, meaning it would never fire
// ? That's why we use a `ref` handler instead, see https://stackoverflow.com/q/39777833/266535
(img: HTMLImageElement) => {
// Check if a node is actually passed. Otherwise node would be null.
if (img) {
// You can now do what you need to, addEventListeners, measure, etc.
const handleLoad = () => {
if (!img.src.startsWith('data:') && !img.src.startsWith('blob:')) {
const p = 'decode' in img ? img.decode() : Promise.resolve()
p.catch(() => {})
.then(() => setLoadState(true))
.catch(() => {})
}
}
if (img.complete) {
// ? If the real image fails to load, this will still remove the blurred image
handleLoad()
} else {
img.onload = handleLoad // eslint-disable-line no-param-reassign, unicorn/prefer-add-event-listener
}
}
imgRef.current = img // Save a reference to the node
},
[setLoadState]
)
return {
callbackRef,
imgRef
}
}
export const useImageStyle = ({
blurDataURL = defaultBlurDataURL,
height,
imgLoaded,
sizesMax,
src,
variant,
width
}: UseImageStyleProps): UseImageStyleReturn => {
// Retrieve styles from theme
const { layPicture, layPictureCls, layImage, layImageCls, layImageNoScript, preImage, preImageBlur, preImageCls } =
useMultiStyleConfig('Image', { variant })
// Do we need a wrapper?
const withWrapperFromProps = !!(width && height)
const withWrapperFromTheme = !!(layPicture && layPicture.constructor === Object && Object.keys(layPicture).length)
// Do we need a blur placeholder?
const withBlurPlaceholder = !!(
!imgLoaded &&
blurDataURL &&
(!height || height > 48) &&
(!width || width > 48) &&
!src.startsWith('data:') &&
!src.startsWith('blob:')
)
return {
styles: {
image: {
// Styles for `<img>` when used with `<picture>` wrapper
// if wrapper is activated by theme then `variant` can override styles from wrapper
// if wrapper is activated by props then styles from wrapper will override `variant`
...(withWrapperFromTheme ? { ...layImageCls, ...preImageCls } : {}),
...layImage,
...preImage,
...(withWrapperFromProps ? { ...layImageCls, ...preImageCls } : {}),
...(withBlurPlaceholder
? {
'--blurBackgroundImage': `url("${blurDataURL}")`,
...preImageBlur
}
: {})
},
imageNoScript: {
...layImageNoScript
},
picture: {
...generateCumulativeLayoutShiftFix({ height, sizesMax, width }),
// Styles for `<picture>` wrapper
// if wrapper is activated by theme then `variant` can override styles from wrapper
// if wrapper is activated by props then styles from wrapper will override `variant`
...(withWrapperFromTheme ? layPictureCls : {}),
...layPicture,
...(withWrapperFromProps ? layPictureCls : {})
}
},
withWrapper: withWrapperFromProps || withWrapperFromTheme
}
}
import { type ComponentMultiStyleConfig } from '@chakra-ui/react'
import { anatomy, type PartsStyleObject } from '@chakra-ui/theme-tools'
const partsAnatomy = anatomy('image').parts(
'layPicture',
'layPictureCls',
'layImage',
'layImageCls',
'layImageNoScript',
'preImage',
'preImageBlur'
)
export type ImageStyleObject = PartsStyleObject<typeof partsAnatomy>
export const Image: ComponentMultiStyleConfig = {
parts: [],
baseStyle: {
layPicture: {},
layPictureCls: {
// layout
display: 'block', // necessary for firefox
position: 'relative',
// box model
boxSizing: 'border-box',
// misc
contentVisibility: 'auto',
overflow: 'hidden'
},
layImage: {
// layout
display: 'inline-block'
},
layImageCls: {
// layout
display: 'block', // necessary for firefox
inset: 0,
position: 'absolute'
},
layImageNoScript: {
// layout
position: 'absolute',
top: 0
},
preImage: {
// box model
height: 'auto',
maxWidth: 'inherit',
width: 'auto'
// misc
// ! not activated because it cause jumpiness while scrolling up in Chrome
// contentVisibility: 'auto'
// containIntrinsicSize: 'width height' // obviously need to be adjusted
},
preImageBlur: {
// visual
backgroundImage: 'var(--blurBackgroundImage)',
backgroundPosition: '0% 0%',
backgroundSize: 'cover',
filter: 'blur(1.25rem)'
},
preImageCls: {
// box model
maxHeight: '100%',
maxWidth: '100%',
minHeight: '100%',
minWidth: '100%'
}
},
variants: {},
defaultProps: {}
}
images: {
/**
* ! Highly suggested to have your max content width defined here (it will better optimize the image size)
* ! For example, on my website, an image is at most 992px which is the centered part of the viewport where I put content
* ! Makes sure values here are in sync with `helper/Image.ts`
*/
deviceSizes: [320, 480, 640, 750, 828, 992, 1080, 1200, 1440, 1920, 2048, 2560, 3840],
domains: [],
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 86400 // if `no max-age` or `s-max-age` defined for an image, cache it `1 day`
},
// ex: Rename<NextLinkProps, 'as', 'asRoute'>
export type Rename<T, K extends keyof T, N extends string> = Pick<T, Exclude<keyof T, K>> & { [P in N]: T[K] }
@handofthecode
Copy link

Could you share your implementation for this import?
import { Rename } from '../../type/Typescript'

@TheThirdRace
Copy link
Author

@handofthecode
Just added it, got that off Stackoverflow... very useful :)

@ifxli
Copy link

ifxli commented Oct 22, 2021

@TheThirdRace thanks for the solution.
btw, could you share the usage example?
Because while using your implementation, I have an issue with setting width and height of image.

@TheThirdRace
Copy link
Author

TheThirdRace commented Oct 22, 2021

@ifxli

import thethirdraceLogo from '~/public/icon/64/ttr-glow.png'

export const PageNavHome = (): ReactElement => {
	return (
		<Image alt='TheThirdRace.com logo' height={48} priority quality={80} src={thethirdraceLogo} width={48} />
	)
}

Even though I import the file, the truth is the file on disk is usually not the size you need. That's why you set the width and height.

The advantages of going with import method is you get the best caching mechanism (immutable) and the blur placeholder for free.

If you're satisfied with the 2nd best caching mechanism (Etag) and no blur placeholder, you could use a plain URL for the src:

  <Image
    alt='your alt text here'
    height={300}
    sizesMax={600}
    src='/img/skyrim/dragonborn.png' // or a full URL: 'https://instagram/skyrim/arrow-to-the-knee.png'
    width={600}
  />

@TheThirdRace
Copy link
Author

TheThirdRace commented Oct 22, 2021

@ifxli
I also realize I might need to update this Gist to latest version because I fixed a couple stuff when integrating changes from NextJs 11.1.2

Edit: I just updated the Gist to latest version including NextJs 11.1.2

It should solve your width & height problem, there was a bug in the previous version where the image would not respect the width and height while using import

@ifxli
Copy link

ifxli commented Oct 22, 2021

@TheThirdRace thank you for the update.
I obviously had the problem of setting the width and height.

@ifxli
Copy link

ifxli commented Oct 22, 2021

@TheThirdRace could you upload the helper/Lifecycle file as well?

@TheThirdRace
Copy link
Author

@ifxli Just updated the helper/Lifecycle.ts for you.

Originally, everything was mostly in the same file. As I ported more and more features, I had to separate stuff in multiple files to keep it tidy. I also made a big refactor at some point, which created new files too...

So thanks for pointing out which files I was missing in the gist. It also gave me the nudge to update it with the latest version of Chakra and NextJs.

@bline
Copy link

bline commented Jan 7, 2022

@TheThirdRace Any chance this will be released into the wild as it's own component?

@TheThirdRace
Copy link
Author

@bline Yes, but not any time soon :(

Given my repo is private, it makes it very hard to share public packages. Or at least, I think it does... I don't have much experience in creating packages 😅

If I can find a quick and easy way to simply move all my components to a separate package without impacting my private repo, I would definitely proceed this way. The only reason I keep my repo private is for some proprietary content (business logic), I would gladly share all my components as a public library if I can and it doesn't give me headaches to manage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment