Pārlūkot izejas kodu

@uppy/react: introduce useUppyEvent (#5264)

Merlijn Vos 9 mēneši atpakaļ
vecāks
revīzija
903d435299

+ 22 - 2
docs/framework-integrations/react.mdx

@@ -7,7 +7,7 @@ import TabItem from '@theme/TabItem';
 
 # React
 
-[React][] components for the Uppy UI plugins and a `useUppyState` hook.
+[React][] components for the Uppy UI plugins and hooks.
 
 ## Install
 
@@ -63,7 +63,7 @@ The following components are exported from `@uppy/react`:
 
 ### Hooks
 
-`useUppyState(uppy, selector)`
+#### `useUppyState(uppy, selector)`
 
 Use this hook when you need to access Uppy’s state reactively. Most of the
 times, this is needed if you are building a custom UI for Uppy in React.
@@ -87,6 +87,26 @@ You can see all the values you can access on the
 type. If you are accessing plugin state, you would have to look at the types of
 the plugin.
 
+#### `useUppyEvent(uppy, event, callback)`
+
+Listen to Uppy events in a React component.
+
+The first item in the array is an array of results from the event. Depending on
+the event, that can be empty or have up to three values. The second item is a
+function to clear the results. Values remain in state until the next event (if
+that ever comes). Depending on your use case, you may want to keep the values in
+state or clear the state after something else happenend.
+
+```ts
+// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
+const [uppy] = useState(() => new Uppy());
+
+const [results, clearResults] = useUppyEvent(uppy, 'transloadit:result');
+const [stepName, result, assembly] = results; // strongly typed
+
+useUppyEvent(uppy, 'cancel-all', clearResults);
+```
+
 ## Examples
 
 ### Example: basic component

+ 1 - 0
packages/@uppy/react/src/index.ts

@@ -5,3 +5,4 @@ export { default as ProgressBar } from './ProgressBar.ts'
 export { default as StatusBar } from './StatusBar.ts'
 export { default as FileInput } from './FileInput.ts'
 export { default as useUppyState } from './useUppyState.ts'
+export { default as useUppyEvent } from './useUppyEvent.ts'

+ 38 - 0
packages/@uppy/react/src/useUppyEvent.test.ts

@@ -0,0 +1,38 @@
+/* eslint-disable react/react-in-jsx-scope */
+/* eslint-disable import/no-extraneous-dependencies */
+import { describe, expect, expectTypeOf, it, vi } from 'vitest'
+import { renderHook, act } from '@testing-library/react'
+
+import Uppy from '@uppy/core'
+import type { Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
+import { useUppyEvent } from '.'
+
+describe('useUppyEvent', () => {
+  it('should return and update value with the correct type', () => {
+    const uppy = new Uppy()
+    const callback = vi.fn()
+    const { result, rerender } = renderHook(() =>
+      useUppyEvent(uppy, 'file-added', callback),
+    )
+    act(() =>
+      uppy.addFile({
+        source: 'vitest',
+        name: 'foo1.jpg',
+        type: 'image/jpeg',
+        data: new File(['foo1'], 'foo1.jpg', { type: 'image/jpeg' }),
+      }),
+    )
+    expectTypeOf(result.current).toEqualTypeOf<
+      [[file: UppyFile<Meta, Record<string, never>>] | [], () => void]
+    >()
+    expect(result.current[0][0]!.name).toBe('foo1.jpg')
+    rerender()
+    expect(result.current[0][0]!.name).toBe('foo1.jpg')
+    act(() => result.current[1]())
+    expectTypeOf(result.current).toEqualTypeOf<
+      [[file: UppyFile<Meta, Record<string, never>>] | [], () => void]
+    >()
+    expect(result.current[0]).toStrictEqual([])
+    expect(callback).toHaveBeenCalledTimes(1)
+  })
+})

+ 38 - 0
packages/@uppy/react/src/useUppyEvent.ts

@@ -0,0 +1,38 @@
+import type { Uppy, UppyEventMap } from '@uppy/core'
+import type { Meta, Body } from '@uppy/utils/lib/UppyFile'
+import { useEffect, useState } from 'react'
+
+type EventResults<
+  M extends Meta,
+  B extends Body,
+  K extends keyof UppyEventMap<M, B>,
+> = Parameters<UppyEventMap<M, B>[K]>
+
+export default function useUppyEvent<
+  M extends Meta,
+  B extends Body,
+  K extends keyof UppyEventMap<M, B>,
+>(
+  uppy: Uppy<M, B>,
+  event: K,
+  callback?: (...args: EventResults<M, B, K>) => void,
+): [EventResults<M, B, K> | [], () => void] {
+  const [result, setResult] = useState<EventResults<M, B, K> | []>([])
+  const clear = () => setResult([])
+
+  useEffect(() => {
+    const handler = ((...args: EventResults<M, B, K>) => {
+      setResult(args)
+      // eslint-disable-next-line node/no-callback-literal
+      callback?.(...args)
+    }) as UppyEventMap<M, B>[K]
+
+    uppy.on(event, handler)
+
+    return function cleanup() {
+      uppy.off(event, handler)
+    }
+  }, [uppy, event, callback])
+
+  return [result, clear]
+}