uppy/examples/react
Merlijn Vos 850c1cb1c5
Add useImageEditor (#6122)
- Create image editor abstraction in `@uppy/components`
- Refactor `Editor` (Preact) and `ImageEditor` (Uppy plugin) to put all
methods in the Uppy plugin class so they can be reused by the headless
abstraction
- Create framework hooks for React, Vue, Svelte
- Update all examples to showcase the new hook
- Symlink the `cropper.scss` (just regular CSS) from
`@uppy/image-editor` into `@uppy/components`, update `build:css` in all
framework packages to copy that file too into their own `dist/` output.
- cropper-js needs basic CSS to do the rotations and stuff which would
be very tedious to write yourself so we ship that CSS file to consumers
too so they can just import it.
- Fix memory leak where we created the image on each render inside the
original Preact plugin UI

```ts
import type { UppyFile } from '@uppy/core'
import { useImageEditor } from '@uppy/react'

interface ImageEditorProps {
  file: UppyFile<any, any>
  close: () => void
}

export function ImageEditor({ file, close }: ImageEditorProps) {
  const {
    state,
    getImageProps,
    getSaveButtonProps,
    getCancelButtonProps,
    getRotateButtonProps,
    getFlipHorizontalButtonProps,
    getZoomButtonProps,
    getCropSquareButtonProps,
    getCropLandscapeButtonProps,
    getCropPortraitButtonProps,
    getResetButtonProps,
    getRotationSliderProps,
  } = useImageEditor({ file })

  return (
    <div className="p-4 max-w-2xl w-full">
      <div className="flex justify-between items-center mb-4">
        <h2 className="text-xl font-bold">Edit Image</h2>
        <button
          type="button"
          onClick={close}
          className="text-gray-500 hover:text-gray-700"
        >
          ✕
        </button>
      </div>

      <div className="mb-4">
        {/* biome-ignore lint/a11y/useAltText: alt is provided by getImageProps() */}
        <img
          className="w-full max-h-[400px] rounded-lg border-2"
          {...getImageProps()}
        />
      </div>

      <div className="mb-4">
        <label className="block text-sm font-medium mb-2">
          Fine Rotation: {state.angle}°
          <input className="w-full mt-1" {...getRotationSliderProps()} />
        </label>
      </div>

      <div className="flex gap-2 flex-wrap mb-4">
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getRotateButtonProps(-90)}
        >
          ↶ -90°
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getRotateButtonProps(90)}
        >
          ↷ +90°
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getFlipHorizontalButtonProps()}
        >
          ⇆ Flip
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getZoomButtonProps(0.1)}
        >
          + Zoom
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getZoomButtonProps(-0.1)}
        >
          - Zoom
        </button>
      </div>

      <div className="flex gap-2 flex-wrap mb-4">
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getCropSquareButtonProps()}
        >
          1:1
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getCropLandscapeButtonProps()}
        >
          16:9
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getCropPortraitButtonProps()}
        >
          9:16
        </button>
        <button
          className="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
          {...getResetButtonProps()}
        >
          Reset
        </button>
      </div>

      <div className="flex gap-4 justify-end">
        <button
          className="bg-gray-500 text-white px-4 py-2 rounded-md"
          {...getCancelButtonProps({ onClick: close })}
        >
          Cancel
        </button>
        <button
          className="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
          {...getSaveButtonProps({ onClick: close })}
        >
          Save
        </button>
      </div>
    </div>
  )
}

export default ImageEditor

```


https://github.com/user-attachments/assets/4b0a99cc-8489-41a2-aa3b-ce88110025bf

---------

Co-authored-by: Prakash <qxprakash@gmail.com>
2026-01-29 11:13:12 +01:00
..
src Add useImageEditor (#6122) 2026-01-29 11:13:12 +01:00
test @uppy/components: fix dropzone global id (#5967) 2025-09-17 10:39:57 +02:00
.stackblitzrc add typescript as a dev dep in stackblitz env (#5976) 2025-09-17 14:37:34 +05:30
index.html Headless components (#5727) 2025-05-22 09:59:43 +02:00
package.json Upgrade playwright in all packages (#6086) 2025-12-05 10:27:58 +01:00
tsconfig.json example-react: fix tsconfig 2025-10-21 11:18:03 +02:00
tsconfig.node.json Headless components (#5727) 2025-05-22 09:59:43 +02:00
vite.config.ts Migrate from Eslint/Prettier/Stylelint to Biome (#5794) 2025-07-01 14:55:41 +02:00
vitest.config.ts Migrate from Cypress to Vitest Browser Mode (#5828) 2025-07-28 11:27:37 +02:00