瀏覽代碼

@uppy/react: introduce useUppyEvent (#5264)

Merlijn Vos 9 月之前
父節點
當前提交
903d435299

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

@@ -7,7 +7,7 @@ import TabItem from '@theme/TabItem';
 
 
 # React
 # React
 
 
-[React][] components for the Uppy UI plugins and a `useUppyState` hook.
+[React][] components for the Uppy UI plugins and hooks.
 
 
 ## Install
 ## Install
 
 
@@ -63,7 +63,7 @@ The following components are exported from `@uppy/react`:
 
 
 ### Hooks
 ### Hooks
 
 
-`useUppyState(uppy, selector)`
+#### `useUppyState(uppy, selector)`
 
 
 Use this hook when you need to access Uppy’s state reactively. Most of the
 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.
 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
 type. If you are accessing plugin state, you would have to look at the types of
 the plugin.
 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
 ## Examples
 
 
 ### Example: basic component
 ### 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 StatusBar } from './StatusBar.ts'
 export { default as FileInput } from './FileInput.ts'
 export { default as FileInput } from './FileInput.ts'
 export { default as useUppyState } from './useUppyState.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]
+}