Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stabilize unstable_getImgProps() => getImageProps() #60739

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions docs/02-app/02-api-reference/01-components/image.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,100 @@ Try it out:

- [Demo light/dark mode theme detection](https://image-component.nextjs.gallery/theme)

## getImageProps

For more advanced use cases, you can call `getImageProps()` to get the props that would be passed to the underlying `<img>` element, and instead pass to them to another component, style, canvas, etc.

This also avoid calling React `useState()` so it can lead to better performance, but it cannot be used with the [`placeholder`](#placeholder) prop because the placeholder will never be removed.

### Picture

The example below uses the [`<picture>`](https://developer.mozilla.org/docs/Web/HTML/Element/picture) element to display a different image based on the user's [preferred color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).

```jsx filename="app/page.js"
import { getImageProps } from 'next/image'

export default function Page() {
const common = { alt: 'Theme Example', width: 800, height: 400 }
const { props: { srcSet: dark } } = getImageProps({ ...common, src: '/dark.png' })
const { props: { srcSet: light, ...rest } } = getImageProps({ ...common, src: '/light.png' })

return (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={dark} />
<source media="(prefers-color-scheme: light)" srcSet={light} />
<img {...rest} />
</picture>
)
```

### Art Direction

Similarly, you can implement [Art Direction](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#art_direction) with different `src` images.

```jsx filename="app/page.js"
import { getImageProps } from 'next/image'

export default function Home() {
const common = { alt: 'Art Direction Example', sizes: '100vw' }
const { props: desktop } = getImageProps({
...common,
width: 1440,
height: 875,
quality: 80,
src: '/desktop.jpg',
})
const { props: mobile } = getImageProps({
...common,
width: 750,
height: 1334,
quality: 70,
src: '/mobile.jpg',
})

return (
<picture>
<source media="(min-width: 1000px)" {...desktop} />
<source media="(min-width: 500px)" {...mobile} />
<img {...mobile} style={{ width: '100%', height: 'auto' }} />
</picture>
)
}
```

### Background CSS

You can even convert the `srcSet` string to the [`image-set()`](https://developer.mozilla.org/en-US/docs/Web/CSS/image/image-set) CSS function to optimize a background image.

```jsx filename="app/page.js"
import { getImageProps } from 'next/image'

function getBackgroundImage(srcSet = '') {
const imageSet = srcSet
.split(', ')
.map((str) => {
const [url, dpi] = str.split(' ')
return `url("${url}") ${dpi}`
})
.join(', ')
return `image-set(${imageSet})`
}

export default function Home() {
const {
props: { srcSet },
} = getImageProps({ alt: '', width: 128, height: 128, src: '/img.png' })
const backgroundImage = getBackgroundImage(srcSet)
const style = { height: '100vh', width: '100vw', backgroundImage }

return (
<main style={style}>
<h1>Hello World</h1>
</main>
)
}
```

## Known Browser Bugs

This `next/image` component uses browser native [lazy loading](https://caniuse.com/loading-lazy-attr), which may fallback to eager loading for older browsers before Safari 15.4. When using the blur-up placeholder, older browsers before Safari 12 will fallback to empty placeholder. When using styles with `width`/`height` of `auto`, it is possible to cause [Layout Shift](https://web.dev/cls/) on older browsers before Safari 15 that don't [preserve the aspect ratio](https://caniuse.com/mdn-html_elements_img_aspect_ratio_computed_from_attributes). For more details, see [this MDN video](https://www.youtube.com/watch?v=4-d_SoCHeWE).
Expand All @@ -810,6 +904,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c

| Version | Changes |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `v14.1.0` | `getImageProps()` is stable. |
| `v14.0.0` | `onLoadingComplete` prop and `domains` config deprecated. |
| `v13.4.14` | `placeholder` prop support for `data:/image...` |
| `v13.2.0` | `contentDispositionType` configuration added. |
Expand Down
9 changes: 4 additions & 5 deletions packages/next/src/shared/lib/image-external.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@ import type { ImageConfigComplete, ImageLoaderProps } from './image-config'
import type { ImageProps, ImageLoader, StaticImageData } from './get-img-props'

import { getImgProps } from './get-img-props'
import { warnOnce } from './utils/warn-once'
import { Image } from '../../client/image-component'

// @ts-ignore - This is replaced by webpack alias
import defaultLoader from 'next/dist/shared/lib/image-loader'

export const unstable_getImgProps = (imgProps: ImageProps) => {
warnOnce(
'Warning: unstable_getImgProps() is experimental and may change or be removed at any time. Use at your own risk.'
)
export const getImageProps = (imgProps: ImageProps) => {
const { props } = getImgProps(imgProps, {
defaultLoader,
// This is replaced by webpack define plugin
imgConf: process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete,
})
// Normally we don't care about undefined props because we pass to JSX,
// but this exported function could be used by the end user for anything
// so we delete undefined props to clean it up a little.
for (const [key, value] of Object.entries(props)) {
if (value === undefined) {
delete props[key as keyof typeof props]
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/app-dir/app-esm-js/app/app/components-ext.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NextImage, { unstable_getImgProps } from 'next/image.js'
import NextImage, { getImageProps } from 'next/image.js'
import Link from 'next/link.js'
import Script from 'next/script.js'

Expand All @@ -8,7 +8,7 @@ export function Components() {
return (
<>
<NextImage className="img" src={src} />
<p className="unstable_getImgProps">{typeof unstable_getImgProps}</p>
<p className="typeof-getImageProps">{typeof getImageProps}</p>
<Link className="link" href="/client">
link
</Link>
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/app-dir/app-esm-js/app/app/components.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NextImage, { unstable_getImgProps } from 'next/image'
import NextImage, { getImageProps } from 'next/image'
import Link from 'next/link'
import Script from 'next/script'

Expand All @@ -8,7 +8,7 @@ export function Components() {
return (
<>
<NextImage className="img" src={src} />
<p className="unstable_getImgProps">{typeof unstable_getImgProps}</p>
<p className="typeof-getImageProps">{typeof getImageProps}</p>
<Link className="link" href="/client">
link
</Link>
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/app-dir/app-esm-js/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ createNextDescribe(
async function validateDomNodes(selector: string) {
expect(await $(`${selector} .img`).prop('tagName')).toBe('IMG')
expect(await $(`${selector} .link`).prop('tagName')).toBe('A')
expect(await $(`${selector} .unstable_getImgProps`).text()).toContain(
expect(await $(`${selector} .typeof-getImageProps`).text()).toContain(
'function'
)
}
Expand Down
6 changes: 3 additions & 3 deletions test/integration/next-image-new/app-dir/app/picture/page.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import { getImageProps } from 'next/image'

export default function Page() {
const common = { alt: 'Hero', width: 400, height: 400, priority: true }
const {
props: { srcSet: dark },
} = getImgProps({ ...common, src: '/test.png' })
} = getImageProps({ ...common, src: '/test.png' })
const {
props: { srcSet: light, ...rest },
} = getImgProps({ ...common, src: '/test_light.png' })
} = getImageProps({ ...common, src: '/test_light.png' })
return (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={dark} />
Expand Down
2 changes: 1 addition & 1 deletion test/integration/next-image-new/app-dir/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ function runTests(mode) {
}
})

it('should render picture via getImgProps', async () => {
it('should render picture via getImageProps', async () => {
const browser = await webdriver(appPort, '/picture')
// Wait for image to load:
await check(async () => {
Expand Down
6 changes: 3 additions & 3 deletions test/integration/next-image-new/default/pages/picture.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import { getImageProps } from 'next/image'

export default function Page() {
const common = { alt: 'Hero', width: 400, height: 400, priority: true }
const {
props: { srcSet: dark },
} = getImgProps({ ...common, src: '/test.png' })
} = getImageProps({ ...common, src: '/test.png' })
const {
props: { srcSet: light, ...rest },
} = getImgProps({ ...common, src: '/test_light.png' })
} = getImageProps({ ...common, src: '/test_light.png' })
return (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={dark} />
Expand Down
2 changes: 1 addition & 1 deletion test/integration/next-image-new/default/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ function runTests(mode) {
}
})

it('should render picture via getImgProps', async () => {
it('should render picture via getImageProps', async () => {
const browser = await webdriver(appPort, '/picture')
// Wait for image to load:
await check(async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import { getImageProps } from 'next/image'

function loader({ src, width, quality }) {
return `${src}?wid=${width}&qual=${quality || 35}`
}

export default function Page() {
const { props: img1 } = getImgProps({
const { props: img1 } = getImageProps({
id: 'img1',
alt: 'img1',
src: '/logo.png',
width: '400',
height: '400',
priority: true,
})
const { props: img2 } = getImgProps({
const { props: img2 } = getImageProps({
id: 'img2',
alt: 'img2',
src: '/logo.png',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('Image Loader Config', () => {
runTests('/')
}
)
describe('dev mode - getImgProps', () => {
describe('dev mode - getImageProps', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
Expand All @@ -80,7 +80,7 @@ describe('Image Loader Config', () => {
runTests('/get-img-props')
})
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode - getImgProps',
'production mode - getImageProps',
() => {
beforeAll(async () => {
await nextBuild(appDir)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import { getImageProps } from 'next/image'

function loader({ src, width, quality }) {
return `${src}?wid=${width}&qual=${quality || 35}`
}

export default function Page() {
const { props: img1 } = getImgProps({
const { props: img1 } = getImageProps({
id: 'img1',
alt: 'img1',
src: '/logo.png',
width: '400',
height: '400',
priority: true,
})
const { props: img2 } = getImgProps({
const { props: img2 } = getImageProps({
id: 'img2',
alt: 'img2',
src: '/logo.png',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('Image Loader Config new', () => {
runTests('/')
}
)
describe('dev mode - getImgProps', () => {
describe('dev mode - getImageProps', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
Expand All @@ -73,7 +73,7 @@ describe('Image Loader Config new', () => {
runTests('/get-img-props')
})
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode - getImgProps',
'production mode - getImageProps',
() => {
beforeAll(async () => {
await nextBuild(appDir)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import React from 'react'
import { unstable_getImgProps as getImgProps } from 'next/image'
import { getImageProps } from 'next/image'
import testJpg from '../public/test.jpg'

const Page = () => {
const { props: img1 } = getImgProps({
const { props: img1 } = getImageProps({
id: 'internal-image',
src: '/test.png',
width: 400,
height: 400,
})
const { props: img2 } = getImgProps({
const { props: img2 } = getImageProps({
id: 'static-image',
src: testJpg,
width: 400,
height: 400,
})
const { props: img3 } = getImgProps({
const { props: img3 } = getImageProps({
id: 'external-image',
src: 'https://image-optimization-test.vercel.app/test.jpg',
width: 400,
height: 400,
})
const { props: img4 } = getImgProps({
const { props: img4 } = getImageProps({
id: 'eager-image',
src: '/test.webp',
width: 400,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('Unoptimized Image Tests', () => {
runTests('/')
}
)
describe('dev mode - getImgProps', () => {
describe('dev mode - getImageProps', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
Expand All @@ -130,7 +130,7 @@ describe('Unoptimized Image Tests', () => {
runTests('/get-img-props')
})
;(process.env.TURBOPACK ? describe.skip : describe)(
'production mode - getImgProps',
'production mode - getImageProps',
() => {
beforeAll(async () => {
await nextBuild(appDir)
Expand Down
Loading