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

@uppy/react: introduce useUppyEvent #5264

merged 4 commits into from
Jul 8, 2024

Conversation

Murderlon
Copy link
Member

@Murderlon Murderlon commented Jun 19, 2024

We already have useUppyState (#5059) but there is a need for useUppyEvent. Considering the following code, which is very common when building with React and Uppy.

const [transloaditResult, setTransloaditResult] = useState<ManuallyTypeThis>([])
const [stepName, result, assembly] = transloaditResult

useEffect(() => {
  function handler(stepName, result, assembly) {
    setTransloaditResult([stepName, result, assembly)
  }
  uppy.on("transloadit:result", handler);

  return () => {
    uppy.off("transloadit:result", handler);
  };
}, [uppy, setTransloaditResult]);

With this PR it's as simple as

const [results, clearResults] = useUppyEvent(uppy, 'transloadit:result')
const [stepName, result, assembly] = results

// optional: listen to another event to clear the results state
useUppyEvent(uppy, 'cancel-all', clearResults)

Note

One thing thing to consider is that values are stale until the next event (if that ever comes). Depending on your use case that may be expected or not. For instance, you listen for transloadit:result but once the user clicks "Done" in dashboard and the state is reset, you still have the stale reference to results of that event. That's why we return a "reset" function to clear the state.

Another thing to know is that not all events return something. In that case it's very likely because you want to do something else when that event triggers, not have a value in state. This is possible with a third argument which is a callback.

@Murderlon Murderlon requested review from mifi and aduh95 June 19, 2024 08:47
@Murderlon Murderlon self-assigned this Jun 19, 2024
Copy link
Contributor

github-actions bot commented Jun 19, 2024

Diff output files
diff --git a/packages/@uppy/react/lib/index.js b/packages/@uppy/react/lib/index.js
index 2739da8..951a3db 100644
--- a/packages/@uppy/react/lib/index.js
+++ b/packages/@uppy/react/lib/index.js
@@ -4,4 +4,5 @@ export { default as DragDrop } from "./DragDrop.js";
 export { default as FileInput } from "./FileInput.js";
 export { default as ProgressBar } from "./ProgressBar.js";
 export { default as StatusBar } from "./StatusBar.js";
+export { default as useUppyEvent } from "./useUppyEvent.js";
 export { default as useUppyState } from "./useUppyState.js";

@Murderlon Murderlon requested a review from remcohaszing June 19, 2024 09:20
@Murderlon Murderlon marked this pull request as ready for review June 20, 2024 10:03
@mifi
Copy link
Contributor

mifi commented Jul 3, 2024

Not sure about mixing state (like assemblyResults) into an event hook. State seems to me like something that should instead be exposed in useUppyState. But as a way to hook into events, it's a very useful hook, like

useUppyEvent(uppy, 'transloadit:result', (r) => console.log('done', r)),

@Murderlon
Copy link
Member Author

I agree and I would not have done that if Uppy holds everything in state, but in the case of events that's not always the case, yet you do need those event results to be stateful when building a UI. The reason why only doing your proposal is not ideal is because you have to manually type everything again:

const [transloaditResult, setTransloaditResult] = useState<UppyEventMap<M,B>['transloadit:result'] | []>([])

useUppyEvent(uppy, 'transloadit:result', setTransloaditResult),

Both my current design and this one don't feel quite right. But at least the current design takes away most complexity.

@mifi
Copy link
Contributor

mifi commented Jul 3, 2024

maybe we should instead introduce some new state then, where it makes sense? something like lastTransloaditResult.

I think having state stored inside the event hook could lead to unnecessary performance degradation. for example if someone useUppyEvent('progress', (p) => doSomethingWithProgress(p)), if this progress event gets fired multiple times per second (depending on uploader), it would cause a re-render on every single event trigger (because of the setState call inside the hook), and if they have a large/complex component this could cause significant slowdown. And a re-render would be kind of unexpected when you're just subscribing to an event listener

@Murderlon
Copy link
Member Author

Murderlon commented Jul 3, 2024

If you want to subscribe to progress, you want to re-render too, that's kind of the point. If you mean to showcase only using the callback instead of the returned state, then it's most likely because you use some stateful callback from a parent component, which means the parent and child are re-rendered. Again, that would be intended as you do something stateful in the parent UI.

maybe we should instead introduce some new state then, where it makes sense?

Uppy core has no knowledge of transloadit specific state so the types in core can't know this. We could try to populate the state modules and interface merging, but that's a very different approach and it simply moves the problem: now you move the stale state from useUppyEvent inside Uppy. We can't guess when to invalidate all this new state which used to be fire-and-forget events.

@mifi
Copy link
Contributor

mifi commented Jul 3, 2024

If you want to subscribe to progress, you want to re-render too, that's kind of the point.

Often when subcribing to events that happen very frequently, one wants to throttle/debounce these events before updating the UI (updating state), because else there would be too many re-renders, causing slowdowns. I can think of events like progress as well as file-added, upload-progress but there may be others too. I remember these problems have occurred in Uppy in the past where events were emitted very fast causing problems. but maybe we just tell users to instead manually uppy.on('event') if they hit these problems then

@Murderlon
Copy link
Member Author

All progress events are already throttled by us. Events like file-added should never be throttled imo.

@mifi
Copy link
Contributor

mifi commented Jul 3, 2024

to illustrate the problem: if someone wants to useUppyEvent('file-added', (file) => myListOfFiles.push(file)), and the user adds 1000 files, then that will cause setState to be called 1000 times, which might be unexpected for developer who uses this hook (and might cause a lock-up in the react ui)

@Murderlon
Copy link
Member Author

Murderlon commented Jul 3, 2024

That example doesn't make sense because you should never do .push on something from useState but call the state setter and I highly doubt people want global variables of myListOfFiles.

I think you're thinking about it the wrong way, when you access state (one way or another) it's because you need it because your UI is stateful. There is no use case where it isn't stateful, then you simply don't use it at all.

also for your example you probably want useUppyState(uppy, (state) => state.files) instead of file-added.

@mifi
Copy link
Contributor

mifi commented Jul 3, 2024

yes, maybe the file-added event is not very useful in the context of react

@Murderlon
Copy link
Member Author

So is this good to go or are there concrete requested changes?

@mifi
Copy link
Contributor

mifi commented Jul 5, 2024

Lgtm

@Murderlon Murderlon merged commit 903d435 into 4.x Jul 8, 2024
17 checks passed
@Murderlon Murderlon deleted the useUppyEvent branch July 8, 2024 08:45
@github-actions github-actions bot mentioned this pull request Jul 10, 2024
github-actions bot added a commit that referenced this pull request Jul 10, 2024
| Package                   | Version | Package                   | Version |
| ------------------------- | ------- | ------------------------- | ------- |
| @uppy/angular             |   0.7.0 | @uppy/onedrive            |   4.0.0 |
| @uppy/audio               |   2.0.0 | @uppy/progress-bar        |   4.0.0 |
| @uppy/aws-s3              |   4.0.0 | @uppy/provider-views      |   4.0.0 |
| @uppy/aws-s3-multipart    |   4.0.0 | @uppy/react               |   4.0.0 |
| @uppy/box                 |   3.0.0 | @uppy/react-native        |   0.6.0 |
| @uppy/companion           |   5.0.0 | @uppy/redux-dev-tools     |   4.0.0 |
| @uppy/companion-client    |   4.0.0 | @uppy/remote-sources      |   2.0.0 |
| @uppy/compressor          |   2.0.0 | @uppy/screen-capture      |   4.0.0 |
| @uppy/core                |   4.0.0 | @uppy/status-bar          |   4.0.0 |
| @uppy/dashboard           |   4.0.0 | @uppy/store-default       |   4.0.0 |
| @uppy/drag-drop           |   4.0.0 | @uppy/store-redux         |   4.0.0 |
| @uppy/drop-target         |   3.0.0 | @uppy/svelte              |   4.0.0 |
| @uppy/dropbox             |   4.0.0 | @uppy/thumbnail-generator |   4.0.0 |
| @uppy/facebook            |   4.0.0 | @uppy/transloadit         |   4.0.0 |
| @uppy/file-input          |   4.0.0 | @uppy/tus                 |   4.0.0 |
| @uppy/form                |   4.0.0 | @uppy/unsplash            |   4.0.0 |
| @uppy/golden-retriever    |   4.0.0 | @uppy/url                 |   4.0.0 |
| @uppy/google-drive        |   4.0.0 | @uppy/utils               |   6.0.0 |
| @uppy/google-photos       |   0.2.0 | @uppy/vue                 |   2.0.0 |
| @uppy/image-editor        |   3.0.0 | @uppy/webcam              |   4.0.0 |
| @uppy/informer            |   4.0.0 | @uppy/xhr-upload          |   4.0.0 |
| @uppy/instagram           |   4.0.0 | @uppy/zoom                |   3.0.0 |
| @uppy/locales             |   4.0.0 | uppy                      |   4.0.0 |

- meta: Bump docker/setup-qemu-action from 3.0.0 to 3.1.0 (dependabot[bot] / #5314)
- meta: Bump docker/build-push-action from 6.2.0 to 6.3.0 (dependabot[bot] / #5313)
- @uppy/core: bring back resetProgress (Merlijn Vos / #5320)
- docs: add note regarding `COMPANION_CLIENT_ORIGINS_REGEX` (Antoine du Hamel / #5322)
- @uppy/companion: make streaming upload default to `true` (Mikael Finstad / #5315)
- docs: change slug for aws docs (Merlijn Vos / #5321)
- @uppy/core: export UppyOptions, UppyFile, Meta, Body (Merlijn Vos / #5319)
- meta: fix React linter rules (Antoine du Hamel / #5317)
- meta: enforce use of extension in import type declarations (Antoine du Hamel / #5316)
- @uppy/companion: remove `oauthOrigin` (Antoine du Hamel / #5311)
- docs,@uppy/companion-client: don't close socket when pausing (Mikael Finstad / #4821)
- @uppy/aws-s3: fix signing on client for bucket name with dots (Antoine du Hamel / #5312)
- @uppy/react: introduce useUppyEvent (Merlijn Vos / #5264)
- @uppy/companion: do not list Node.js 20.12 as compatible (Antoine du Hamel / #5309)
- @uppy/provider-views: `.openFolder()` - return progress indication (Evgenia Karunus / #5306)
- examples,@uppy/companion: Release: uppy@3.27.3 (github-actions[bot] / #5304)
- @uppy/companion: fix `TypeError` when parsing request (Antoine du Hamel / #5303)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants