diff --git a/bin/build-lib.js b/bin/build-lib.js
index e1a2a03bb..e185ebb54 100644
--- a/bin/build-lib.js
+++ b/bin/build-lib.js
@@ -10,7 +10,7 @@ const { mkdir, stat, writeFile } = fs.promises
const PACKAGE_JSON_IMPORT = /^\..*\/package.json$/
const SOURCE = 'packages/{*,@uppy/*}/src/**/*.{js,ts}?(x)'
// Files not to build (such as tests)
-const IGNORE = /\.test\.[jt]s$|__mocks__|svelte|angular|companion\//
+const IGNORE = /\.test\.jsx?$|\.test\.tsx?$|__mocks__|svelte|angular|companion\//;
// Files that should trigger a rebuild of everything on change
const META_FILES = [
'babel.config.js',
diff --git a/docs/framework-integrations/react.mdx b/docs/framework-integrations/react.mdx
index de84311b5..e04684f24 100644
--- a/docs/framework-integrations/react.mdx
+++ b/docs/framework-integrations/react.mdx
@@ -61,9 +61,6 @@ The following components are exported from `@uppy/react`:
- `` renders [`@uppy/progress-bar`](/docs/progress-bar)
- `` renders [`@uppy/status-bar`](/docs/status-bar)
-{/* prettier-ignore */}
-{/* Commented out until the hook is live
-
### Hooks
`useUppyState(uppy, selector)`
@@ -86,12 +83,10 @@ const metaFields = useUppyState(
```
You can see all the values you can access on the
-[`State`](https://github.com/transloadit/uppy/blob/main/packages/%40uppy/core/types/index.d.ts#L190)
+[`State`](https://github.com/transloadit/uppy/blob/c45407d099d87e25cecaf03c5d9ce59c582ca0dc/packages/%40uppy/core/src/Uppy.ts#L155-L181)
type. If you are accessing plugin state, you would have to look at the types of
the plugin.
-\*/}
-
## Examples
### Example: basic component
diff --git a/e2e/clients/react/App.jsx b/e2e/clients/react/App.jsx
index cfd68bcc5..4818918fe 100644
--- a/e2e/clients/react/App.jsx
+++ b/e2e/clients/react/App.jsx
@@ -10,24 +10,35 @@ import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import '@uppy/drag-drop/dist/style.css'
-export default function App () {
- const RemoteSourcesOptions = {
- companionUrl: 'http://companion.uppy.io',
- sources: ['GoogleDrive', 'OneDrive', 'Unsplash', 'Zoom', 'Url'],
- }
- const uppyDashboard = new Uppy({ id: 'dashboard' }).use(RemoteSources, { ...RemoteSourcesOptions })
- const uppyModal = new Uppy({ id: 'modal' })
- const uppyDragDrop = new Uppy({ id: 'drag-drop' }).use(ThumbnailGenerator)
+const uppyDashboard = new Uppy({ id: 'dashboard' }).use(RemoteSources, {
+ companionUrl: 'http://companion.uppy.io',
+ sources: ['GoogleDrive', 'OneDrive', 'Unsplash', 'Zoom', 'Url'],
+})
+const uppyModal = new Uppy({ id: 'modal' })
+const uppyDragDrop = new Uppy({ id: 'drag-drop' }).use(ThumbnailGenerator)
+
+export default function App() {
const [open, setOpen] = useState(false)
+ // TODO: Parcel is having a bad time resolving React inside @uppy/react for some reason.
+ // We are using Parcel in an odd way and I don't think there is an easy fix.
+ // const files = useUppyState(uppyDashboard, (state) => state.files)
// drag-drop has no visual output so we test it via the uppy instance
window.uppy = uppyDragDrop
return (
-
+
+ {/*
Dashboard file count: {Object.keys(files).length}
*/}
diff --git a/package.json b/package.json
index 6803491a6..9fa7b5171 100644
--- a/package.json
+++ b/package.json
@@ -168,7 +168,7 @@
},
"resolutions": {
"@types/eslint@^7.2.13": "^8.2.0",
- "@types/react": "^17",
+ "@types/react": "^18",
"@types/webpack-dev-server": "^4",
"@vitest/utils": "patch:@vitest/utils@npm%3A1.2.1#./.yarn/patches/@vitest-utils-npm-1.2.1-3028846845.patch",
"p-queue": "patch:p-queue@npm%3A7.4.1#./.yarn/patches/p-queue-npm-7.4.1-e0cf0a6f17.patch",
diff --git a/packages/@uppy/react/package.json b/packages/@uppy/react/package.json
index 189ca8b38..a52b27c11 100644
--- a/packages/@uppy/react/package.json
+++ b/packages/@uppy/react/package.json
@@ -21,11 +21,16 @@
"url": "git+https://github.com/transloadit/uppy.git"
},
"dependencies": {
- "@uppy/utils": "workspace:^"
+ "@uppy/utils": "workspace:^",
+ "use-sync-external-store": "^1.2.0"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^6.4.2",
+ "@testing-library/react": "^14.2.2",
"@types/react": "^18.0.8",
- "react": "^18.1.0"
+ "@types/use-sync-external-store": "^0.0.6",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
},
"peerDependencies": {
"@uppy/core": "workspace:^",
@@ -34,7 +39,7 @@
"@uppy/file-input": "workspace:^",
"@uppy/progress-bar": "workspace:^",
"@uppy/status-bar": "workspace:^",
- "react": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ "react": "^18.0.0"
},
"peerDependenciesMeta": {
"@uppy/dashboard": {
diff --git a/packages/@uppy/react/src/index.ts b/packages/@uppy/react/src/index.ts
index f241b05d8..e57425608 100644
--- a/packages/@uppy/react/src/index.ts
+++ b/packages/@uppy/react/src/index.ts
@@ -4,4 +4,4 @@ export { default as DragDrop } from './DragDrop.ts'
export { default as ProgressBar } from './ProgressBar.ts'
export { default as StatusBar } from './StatusBar.ts'
export { default as FileInput } from './FileInput.ts'
-export { default as useUppy } from './useUppy.ts'
+export { default as useUppyState } from './useUppyState.ts'
diff --git a/packages/@uppy/react/src/useUppy.ts b/packages/@uppy/react/src/useUppy.ts
deleted file mode 100644
index e549d8207..000000000
--- a/packages/@uppy/react/src/useUppy.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useEffect, useRef } from 'react'
-import { Uppy as UppyCore } from '@uppy/core'
-import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
-
-/**
- * @deprecated Initialize Uppy outside of the component.
- */
-export default function useUppy(
- factory: () => UppyCore,
-): UppyCore | undefined {
- if (typeof factory !== 'function') {
- throw new TypeError(
- 'useUppy: expected a function that returns a new Uppy instance',
- )
- }
-
- const uppy = useRef | undefined>(undefined)
- if (uppy.current === undefined) {
- uppy.current = factory()
-
- if (!(uppy.current instanceof UppyCore)) {
- throw new TypeError(
- `useUppy: factory function must return an Uppy instance, got ${typeof uppy.current}`,
- )
- }
- }
-
- useEffect(() => {
- return () => {
- uppy.current?.close({ reason: 'unmount' })
- uppy.current = undefined
- }
- }, [uppy])
-
- return uppy.current
-}
diff --git a/packages/@uppy/react/src/useUppyState.test.tsx b/packages/@uppy/react/src/useUppyState.test.tsx
new file mode 100644
index 000000000..7a8d37631
--- /dev/null
+++ b/packages/@uppy/react/src/useUppyState.test.tsx
@@ -0,0 +1,46 @@
+/* eslint-disable react/react-in-jsx-scope */
+/* eslint-disable import/no-extraneous-dependencies */
+import React from 'react'
+import { describe, expect, expectTypeOf, it } from 'vitest'
+import { renderHook, render, act } from '@testing-library/react'
+import Uppy from '@uppy/core'
+
+import useUppyState from './useUppyState.ts'
+
+describe('useUppyState', () => {
+ it('should return and update value with the correct type', () => {
+ const uppy = new Uppy()
+ const { result, rerender } = renderHook(() =>
+ useUppyState(uppy, (state) => state.totalProgress),
+ )
+ expectTypeOf(result.current).toEqualTypeOf()
+ expect(result.current).toBe(0)
+ act(() => uppy.setState({ totalProgress: 50 }))
+ rerender()
+ expect(result.current).toBe(50)
+ rerender()
+ expect(result.current).toBe(50)
+ })
+
+ it('does not re-render unnecessarily', () => {
+ const uppy = new Uppy()
+ let renderCount = 0
+
+ const Component = React.memo((props: { uppy: Uppy }) => {
+ const files = useUppyState(props.uppy, (state) =>
+ Object.values(state.files),
+ )
+ renderCount++
+ return