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

@uppy/react: introduce useUppyEvent #5264

Merged
merged 4 commits into from
Jul 8, 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
24 changes: 22 additions & 2 deletions docs/framework-integrations/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/@uppy/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 38 additions & 0 deletions packages/@uppy/react/src/useUppyEvent.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 38 additions & 0 deletions packages/@uppy/react/src/useUppyEvent.ts
Original file line number Diff line number Diff line change
@@ -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<
Murderlon marked this conversation as resolved.
Show resolved Hide resolved
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]
}