From 9f92e8eca19292c73388f0cd8a8fdbbd9418f257 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Wed, 19 Jun 2024 10:44:46 +0200 Subject: [PATCH 1/4] @uppy/react: introduce useUppyEvent --- packages/@uppy/react/src/useUppyEvent.test.ts | 31 +++++++++++++++++++ packages/@uppy/react/src/useUppyEvent.ts | 31 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/@uppy/react/src/useUppyEvent.test.ts create mode 100644 packages/@uppy/react/src/useUppyEvent.ts diff --git a/packages/@uppy/react/src/useUppyEvent.test.ts b/packages/@uppy/react/src/useUppyEvent.test.ts new file mode 100644 index 0000000000..7ac20fc9d3 --- /dev/null +++ b/packages/@uppy/react/src/useUppyEvent.test.ts @@ -0,0 +1,31 @@ +/* eslint-disable react/react-in-jsx-scope */ +/* eslint-disable import/no-extraneous-dependencies */ +import { describe, expect, expectTypeOf, it } 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 './useUppyEvent.ts' + +describe('useUppyEvent', () => { + it('should return and update value with the correct type', () => { + const uppy = new Uppy() + const { result, rerender } = renderHook(() => + useUppyEvent(uppy, 'file-added'), + ) + 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>] | [] + >() + expect(result.current[0]!.name).toBe('foo1.jpg') + rerender() + expect(result.current[0]!.name).toBe('foo1.jpg') + }) +}) diff --git a/packages/@uppy/react/src/useUppyEvent.ts b/packages/@uppy/react/src/useUppyEvent.ts new file mode 100644 index 0000000000..92e2bdad11 --- /dev/null +++ b/packages/@uppy/react/src/useUppyEvent.ts @@ -0,0 +1,31 @@ +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, +> = Parameters[K]> + +export default function useUppyEvent< + M extends Meta, + B extends Body, + K extends keyof UppyEventMap, +>(uppy: Uppy, event: K): EventResults | [] { + const [result, setResult] = useState | []>([]) + + useEffect(() => { + const handler = ((...args: EventResults) => { + setResult(args) + }) as UppyEventMap[K] + + uppy.on(event, handler) + + return function cleanup() { + uppy.off(event, handler) + } + }, [uppy, event]) + + return result +} From 2e6e0020a309727da407f134531b6a41fe3534d0 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 20 Jun 2024 11:52:56 +0200 Subject: [PATCH 2/4] Add callback arugment and return reset function --- packages/@uppy/react/src/useUppyEvent.test.ts | 17 ++++++++++++----- packages/@uppy/react/src/useUppyEvent.ts | 13 ++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/@uppy/react/src/useUppyEvent.test.ts b/packages/@uppy/react/src/useUppyEvent.test.ts index 7ac20fc9d3..8470badd91 100644 --- a/packages/@uppy/react/src/useUppyEvent.test.ts +++ b/packages/@uppy/react/src/useUppyEvent.test.ts @@ -1,6 +1,6 @@ /* eslint-disable react/react-in-jsx-scope */ /* eslint-disable import/no-extraneous-dependencies */ -import { describe, expect, expectTypeOf, it } from 'vitest' +import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { renderHook, act } from '@testing-library/react' import Uppy from '@uppy/core' @@ -10,8 +10,9 @@ import useUppyEvent from './useUppyEvent.ts' 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'), + useUppyEvent(uppy, 'file-added', callback), ) act(() => uppy.addFile({ @@ -22,10 +23,16 @@ describe('useUppyEvent', () => { }), ) expectTypeOf(result.current).toEqualTypeOf< - [file: UppyFile>] | [] + [[file: UppyFile>] | [], () => void] >() - expect(result.current[0]!.name).toBe('foo1.jpg') + expect(result.current[0][0]!.name).toBe('foo1.jpg') rerender() - expect(result.current[0]!.name).toBe('foo1.jpg') + expect(result.current[0][0]!.name).toBe('foo1.jpg') + act(() => result.current[1]()) + expectTypeOf(result.current).toEqualTypeOf< + [[file: UppyFile>] | [], () => void] + >() + expect(result.current[0]).toStrictEqual([]) + expect(callback).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/@uppy/react/src/useUppyEvent.ts b/packages/@uppy/react/src/useUppyEvent.ts index 92e2bdad11..54f23a95c4 100644 --- a/packages/@uppy/react/src/useUppyEvent.ts +++ b/packages/@uppy/react/src/useUppyEvent.ts @@ -12,12 +12,19 @@ export default function useUppyEvent< M extends Meta, B extends Body, K extends keyof UppyEventMap, ->(uppy: Uppy, event: K): EventResults | [] { +>( + uppy: Uppy, + event: K, + callback?: (...args: EventResults) => void, +): [EventResults | [], () => void] { const [result, setResult] = useState | []>([]) + const clear = () => setResult([]) useEffect(() => { const handler = ((...args: EventResults) => { setResult(args) + // eslint-disable-next-line node/no-callback-literal + callback?.(...args) }) as UppyEventMap[K] uppy.on(event, handler) @@ -25,7 +32,7 @@ export default function useUppyEvent< return function cleanup() { uppy.off(event, handler) } - }, [uppy, event]) + }, [uppy, event, callback]) - return result + return [result, clear] } From abbe8db1185371d733f20acb50a0c12be434d151 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 20 Jun 2024 12:01:03 +0200 Subject: [PATCH 3/4] Add docs --- docs/framework-integrations/react.mdx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/framework-integrations/react.mdx b/docs/framework-integrations/react.mdx index c4b0e04990..6be7bfd227 100644 --- a/docs/framework-integrations/react.mdx +++ b/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 From ea0e7239a4e675e02292c056a118bfbd85a5b9fc Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 20 Jun 2024 14:20:01 +0200 Subject: [PATCH 4/4] Export hook, import in test --- packages/@uppy/react/src/index.ts | 1 + packages/@uppy/react/src/useUppyEvent.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@uppy/react/src/index.ts b/packages/@uppy/react/src/index.ts index e574256089..818c8db46f 100644 --- a/packages/@uppy/react/src/index.ts +++ b/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' diff --git a/packages/@uppy/react/src/useUppyEvent.test.ts b/packages/@uppy/react/src/useUppyEvent.test.ts index 8470badd91..9dd45454bc 100644 --- a/packages/@uppy/react/src/useUppyEvent.test.ts +++ b/packages/@uppy/react/src/useUppyEvent.test.ts @@ -5,7 +5,7 @@ import { renderHook, act } from '@testing-library/react' import Uppy from '@uppy/core' import type { Meta, UppyFile } from '@uppy/utils/lib/UppyFile' -import useUppyEvent from './useUppyEvent.ts' +import { useUppyEvent } from '.' describe('useUppyEvent', () => { it('should return and update value with the correct type', () => {