Explorar el Código

react: add useUppy() hook (#2666)

* react: add useUppy() hook

* add test

* docs: replace useRef/useEffect recommendation with useUppy()

* fix typo
Renée Kooi hace 4 años
padre
commit
b03c5af053

+ 2 - 0
packages/@uppy/react/index.js

@@ -3,3 +3,5 @@ exports.DashboardModal = require('./lib/DashboardModal')
 exports.DragDrop = require('./lib/DragDrop')
 exports.ProgressBar = require('./lib/ProgressBar')
 exports.StatusBar = require('./lib/StatusBar')
+
+exports.useUppy = require('./lib/useUppy')

+ 2 - 0
packages/@uppy/react/index.mjs

@@ -3,3 +3,5 @@ export { default as DashboardModal } from './lib/DashboardModal.js'
 export { default as DragDrop } from './lib/DragDrop.js'
 export { default as ProgressBar } from './lib/ProgressBar.js'
 export { default as StatusBar } from './lib/StatusBar.js'
+
+export { default as useUppy } from './lib/useUppy.js'

+ 9 - 0
packages/@uppy/react/src/useUppy.d.ts

@@ -0,0 +1,9 @@
+import Uppy = require('@uppy/core')
+
+declare function useUppy<
+  Types extends Uppy.TypeChecking = Uppy.LooseTypes
+>(
+  factory: () => Uppy.Uppy<Types>
+): Uppy.Uppy<Types>
+
+export = useUppy

+ 25 - 0
packages/@uppy/react/src/useUppy.js

@@ -0,0 +1,25 @@
+const { useEffect, useRef } = require('react')
+const UppyCore = require('@uppy/core').Uppy
+
+module.exports = function useUppy (factory) {
+  if (typeof factory !== 'function') {
+    throw new TypeError('useUppy: expected a function that returns a new Uppy instance')
+  }
+
+  const uppy = useRef(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()
+    }
+  }, [])
+
+  return uppy.current
+}

+ 61 - 0
packages/@uppy/react/src/useUppy.test.js

@@ -0,0 +1,61 @@
+const h = require('react').createElement
+const { mount, configure } = require('enzyme')
+const ReactAdapter = require('enzyme-adapter-react-16')
+const Uppy = require('@uppy/core')
+
+beforeAll(() => {
+  configure({ adapter: new ReactAdapter() })
+})
+
+const useUppy = require('./useUppy')
+
+describe('useUppy()', () => {
+  it('is created and deleted according to component lifecycle', () => {
+    const oninstall = jest.fn()
+    const onuninstall = jest.fn()
+
+    function JustInstance () {
+      const uppy = useUppy(() => {
+        oninstall()
+        return new Uppy()
+          .on('cancel-all', onuninstall)
+      })
+
+      return <div x={uppy} />
+    }
+
+    const el = mount(<JustInstance />)
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).not.toHaveBeenCalled()
+
+    el.unmount()
+
+    expect(oninstall).toHaveBeenCalled()
+    expect(onuninstall).toHaveBeenCalled()
+  })
+
+  it('checks types', () => {
+    function NullUppy () {
+      const uppy = useUppy(() => null)
+
+      return <div x={uppy} />
+    }
+
+    expect(() => {
+      mount(<NullUppy />)
+    }).toThrow('factory function must return an Uppy instance')
+
+    function GarbageUppy () {
+      const uppy = useUppy(() => ({
+        garbage: 'lala'
+      }))
+
+      return <div x={uppy} />
+    }
+
+    expect(() => {
+      mount(<GarbageUppy />)
+    }).toThrow('factory function must return an Uppy instance')
+  })
+})

+ 3 - 0
packages/@uppy/react/types/index.d.ts

@@ -5,3 +5,6 @@ export { default as DashboardModal } from '../src/DashboardModal'
 export { default as DragDrop } from '../src/DragDrop'
 export { default as ProgressBar } from '../src/ProgressBar'
 export { default as StatusBar } from '../src/StatusBar'
+
+import useUppy = require('../src/useUppy')
+export { useUppy }

+ 9 - 1
packages/@uppy/react/types/index.test-d.tsx

@@ -1,8 +1,10 @@
 import React = require('react')
 import Uppy = require('@uppy/core')
-import { expectError } from 'tsd'
+import { expectType, expectError } from 'tsd'
 import * as components from '../'
 
+const { useUppy } = components
+
 const uppy = Uppy<Uppy.StrictTypes>()
 
 function TestComponent() {
@@ -56,3 +58,9 @@ expectError(<components.DashboardModal replaceTargetContent />)
   // use onRequestClose instead.
   expectError(<components.DashboardModal onRequestCloseModal />)
 }
+
+function TestHook () {
+  expectType<Uppy.Uppy<Uppy.StrictTypes>>(useUppy(() => uppy))
+  expectType<Uppy.Uppy<Uppy.LooseTypes>>(useUppy(() => Uppy()))
+  expectError(useUppy(uppy))
+}

+ 12 - 38
website/src/docs/react-initializing.md

@@ -12,52 +12,26 @@ When using Uppy's React components, an Uppy instance must be passed in to the `u
 
 ## Functional Components
 
-With React Hooks, the `useRef` hook can be used to create an instance once and remember it for all rerenders. The `useEffect` hook can close the Uppy instance when the component unmounts.
+Functional components are re-run on every render. This makes it very easy to accidentally recreate a fresh Uppy instance every time, causing state to be reset and resources to be wasted.
+
+The `@uppy/react` package provides a hook `useUppy()` that can manage an Uppy instance's lifetime for you. It will be created when your component is first rendered, and destroyed when your component unmounts.
 
 ```js
-function MyComponent () {
-  const uppy = useRef(undefined);
-  // Make sure we only initialize it the first time:
-  if (uppy.current === undefined) {
-    uppy.current = new Uppy()
-      .use(Transloadit, {})
-  }
+const Uppy = require('@uppy/core')
+const Tus = require('@uppy/tus')
+const { DashboardModal, useUppy } = require('@uppy/react')
 
-  React.useEffect(() => {
-    // Return a cleanup function:
-    return () => uppy.current.close()
-  }, [])
+function MyComponent () {
+  const uppy = useUppy(() => {
+    return new Uppy()
+      .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files' })
+  })
 
   return <DashboardModal uppy={uppy} />
 }
 ```
 
-With `useRef`, the Uppy instance and its functions are stored on the `.current` property.
-`useEffect()` must receive the `[]` dependency array parameter, or the Uppy instance will be destroyed every time the component rerenders.
-To make sure you never forget these requirements, you could wrap it up in a custom hook:
-
-```js
-function useUppy (factory) {
-  const uppy = React.useRef(undefined)
-  // Make sure we only initialize it the first time:
-  if (uppy.current === undefined) {
-    uppy.current = factory()
-  }
-
-  React.useEffect(() => {
-    return () => uppy.current.close()
-  }, [])
-  return uppy.current
-}
-
-// Then use it as:
-const uppy = useUppy(() =>
-  new Uppy()
-    .use(Tus, {})
-)
-```
-
-(The function wrapper is required here so you don't create an unused Uppy instance on each rerender.)
+Importantly, the `useUppy()` hook takes a _function_ that returns an Uppy instance. This way, the `useUppy()` hook can decide when to create it. Otherwise you would still be creating an unused Uppy instance on every render.
 
 ## Class Components