Skip to content

Instantly share code, notes, and snippets.

@TheThirdRace
Last active February 6, 2024 15:20
Show Gist options
  • 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] }
@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