Skip to content

Commit

Permalink
fix: migrate class components to functions+hooks (#3701)
Browse files Browse the repository at this point in the history
Co-authored-by: Cody Olsen <81981+stipsan@users.noreply.github.com>
  • Loading branch information
stipsan and stipsan committed Oct 4, 2022
1 parent 8d0f1a4 commit 829e5ec
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 241 deletions.
1 change: 0 additions & 1 deletion packages/sanity/src/_exports/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export type {FIXME_SanityDocument} from '../form/store/formState' // eslint-disa
export type {
HashFocusManagerChildArgs,
HashFocusManagerProps,
HashFocusManagerState,
SimpleFocusManagerProps,
SimpleFocusManagerState,
} from '../form/studio'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import React from 'react'
import React, {
memo,
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import Debug from 'debug'
import {debounce} from 'lodash'
import {calculateStyles} from './calculateStyles'
import {DEFAULT_HOTSPOT, DEFAULT_CROP} from './constants'
import {HotspotImageContainer} from './HotspotImage.styles'
Expand Down Expand Up @@ -31,28 +38,39 @@ export interface HotspotImageProps {
onError?: (event: React.SyntheticEvent<HTMLImageElement, Event>) => void
onLoad?: () => void
}
export interface HotspotImageState {
containerAspect: number | null
}
export class HotspotImage extends React.PureComponent<HotspotImageProps, HotspotImageState> {
static defaultProps = {
alignX: 'center',
alignY: 'center',
className: '',
crop: DEFAULT_CROP,
hotspot: DEFAULT_HOTSPOT,
aspectRatio: 'none',
}

state: HotspotImageState = {
containerAspect: null,
}

containerElement: HTMLDivElement | null = null
imageElement: HTMLImageElement | null = null

componentDidMount() {
const imageElement = this.imageElement

export const HotspotImage = memo(function HotspotImage(props: HotspotImageProps) {
const {
alignX = 'center',
alignY = 'center',
alt,
aspectRatio = 'none',
className = '',
crop = DEFAULT_CROP,
hotspot = DEFAULT_HOTSPOT,
onError,
onLoad,
src,
srcAspectRatio,
srcSet,
style,
} = props
const [containerAspect, setContainerAspect] = useState<number | null>(null)
const containerElementRef = useRef<HTMLDivElement | null>(null)
const imageElementRef = useRef<HTMLImageElement | null>(null)

const updateContainerAspect = useCallback(() => {
if (!containerElementRef.current) return
if (aspectRatio === 'auto') {
const parentNode = containerElementRef.current.parentNode as HTMLElement
startTransition(() => setContainerAspect(parentNode.offsetWidth / parentNode.offsetHeight))
} else {
setContainerAspect(null)
}
}, [aspectRatio])

useEffect(() => {
const imageElement = imageElementRef.current

// Fixes issues that may happen if the component is rendered on server and mounted after the image has finished loading
// In these situations, neither the onLoad or the onError events will be called.
Expand All @@ -64,121 +82,63 @@ export class HotspotImage extends React.PureComponent<HotspotImageProps, Hotspot
imageElement.naturalWidth !== undefined

if (alreadyLoaded) {
debug(
"Image '%s' already loaded, refreshing (from cache) to trigger onLoad / onError",
this.props.src
)
debug("Image '%s' already loaded, refreshing (from cache) to trigger onLoad / onError", src)
// eslint-disable-next-line no-self-assign
imageElement.src = imageElement.src
}

this.updateContainerAspect(this.props)
updateContainerAspect()

if (typeof window !== 'undefined') {
window.addEventListener('resize', this.handleResize)
}
}
window.addEventListener('resize', updateContainerAspect)

componentWillUnmount() {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.handleResize)
return () => {
window.removeEventListener('resize', updateContainerAspect)
}
}

// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps: HotspotImageProps) {
if (nextProps.aspectRatio !== this.props.aspectRatio) {
this.updateContainerAspect(nextProps)
}
}

updateContainerAspect(props: HotspotImageProps) {
if (!this.containerElement) return
if (props.aspectRatio === 'auto') {
const parentNode = this.containerElement.parentNode as HTMLElement
this.setState({
containerAspect: parentNode.offsetWidth / parentNode.offsetHeight,
})
} else {
this.setState({
containerAspect: null,
})
}
}

getTargetAspectValue() {
const {aspectRatio, srcAspectRatio, crop} = this.props
}, [src, updateContainerAspect])

const targetAspect = useMemo(() => {
if (aspectRatio === 'none') {
return crop ? getCropAspect(crop, srcAspectRatio) : srcAspectRatio
}

if (aspectRatio === 'auto') {
return this.state.containerAspect
return containerAspect
}

return aspectRatio || null
}

setImageElement = (el: HTMLImageElement | null) => {
this.imageElement = el
}

handleResize = debounce(() => this.updateContainerAspect(this.props))

setContainerElement = (el: HTMLDivElement | null) => {
this.containerElement = el
}

render() {
const {
srcAspectRatio,
crop,
hotspot,
src,
srcSet,
alignX = 'center',
alignY = 'center',
className,
style,
alt,
onError,
onLoad,
} = this.props

const targetAspect = this.getTargetAspectValue()

const targetStyles = calculateStyles({
container: {aspectRatio: targetAspect || srcAspectRatio},
image: {aspectRatio: srcAspectRatio},
hotspot,
crop,
align: {
x: alignX,
y: alignY,
},
})
return (
<HotspotImageContainer
className={`${className}`}
style={style}
ref={this.setContainerElement}
>
<div style={targetStyles.container}>
<div style={targetStyles.padding} />
<div style={targetStyles.crop}>
<img
ref={this.setImageElement}
src={src}
alt={alt}
srcSet={srcSet}
onLoad={onLoad}
onError={onError}
style={targetStyles.image}
/>
</div>
}, [aspectRatio, containerAspect, crop, srcAspectRatio])

const targetStyles = useMemo(
() =>
calculateStyles({
container: {aspectRatio: targetAspect || srcAspectRatio},
image: {aspectRatio: srcAspectRatio},
hotspot,
crop,
align: {
x: alignX,
y: alignY,
},
}),
[alignX, alignY, crop, hotspot, srcAspectRatio, targetAspect]
)

return (
<HotspotImageContainer className={`${className}`} style={style} ref={containerElementRef}>
<div style={targetStyles.container}>
<div style={targetStyles.padding} />
<div style={targetStyles.crop}>
<img
ref={imageElementRef}
src={src}
alt={alt}
srcSet={srcSet}
onLoad={onLoad}
onError={onError}
style={targetStyles.image}
/>
</div>
</HotspotImageContainer>
)
}
}
</div>
</HotspotImageContainer>
)
})
Original file line number Diff line number Diff line change
@@ -1,64 +1,42 @@
import React from 'react'
/* eslint-disable @typescript-eslint/no-shadow */
import {useEffect, useState, type ReactElement} from 'react'

interface ImageLoaderProps {
src: string
children: (props: {
isLoading: boolean
image: HTMLImageElement | null
error: Error | null
}) => React.ReactNode
}
interface ImageLoaderState {
isLoading: boolean
image: HTMLImageElement | null
error: Error | null
}) => ReactElement | null
}

export class ImageLoader extends React.Component<ImageLoaderProps, ImageLoaderState> {
state: ImageLoaderState = {
isLoading: true,
image: null,
error: null,
}
export function ImageLoader(props: ImageLoaderProps) {
const {src, children} = props
const [isLoading, setIsLoading] = useState(true)
const [image, setImage] = useState<HTMLImageElement | null>(null)
const [error, setError] = useState<Error | null>(null)

UNSAFE_componentWillMount() {
this.loadImage(this.props.src)
}
useEffect(() => {
setImage(null)
setError(null)
setIsLoading(true)

loadImage(src: string) {
const image = new Image()
this.setState({
image: null,
error: null,
})

image.onload = () => {
this.setState({
image: image,
error: null,
isLoading: false,
})
setImage(image)
setError(null)
setIsLoading(false)
}

image.onerror = () => {
this.setState({
error: new Error(`Could not load image from ${JSON.stringify(this.props.src)}`),
isLoading: false,
})
setError(new Error(`Could not load image from ${JSON.stringify(src)}`))
setIsLoading(false)
}

image.referrerPolicy = 'strict-origin-when-cross-origin'
image.src = src
}

UNSAFE_componentWillReceiveProps(nextProps: ImageLoaderProps) {
if (nextProps.src !== this.props.src) {
this.loadImage(nextProps.src)
}
}
}, [src])

render() {
const {error, image, isLoading} = this.state
return this.props.children({image, error, isLoading})
}
return children({image, error, isLoading})
}

0 comments on commit 829e5ec

Please sign in to comment.