New Uppy 4.0 major: TypeScript rewrite, Google Photos, React hooks, and much more
Hold on to your leashes, folks! Uppy 4.0 is here, and it’s more exciting than a tennis ball at the dog park! Our beloved Uppy mascot, the adorable coding canine, has been hard at work fetching all the latest updates for you.
From a full TypeScript makeover to shiny new React hooks, and even Google Photos integration – this release is so packed with treats that we’re almost wagging our tails in excitement. Without further a-dog, let’s dig into what makes Uppy 4.0 the goodest of good boys in file uploading!
Migration guide
This post covers the most exciting new features of Uppy 4.0. We have an accompanying migration guide to help you transition to 4.0.
TypeScript rewrite
In the year 2024, people expect excellent types from their libraries. We used to author types separately by hand, but they were often inconsistent or incomplete. As of now, Uppy has been completely rewritten in TypeScript!
From now on you’ll be in safe hands when working with Uppy, whether it’s setting the right options, building plugins, or listening to events.
import Uppy from '@uppy/core';
const uppy = new Uppy();
// Event name autocompletion and inferred argument types
uppy.on('file-added', (file) => {
console.log(file);
});
One important thing to note is the new generics on @uppy/core
.
import Uppy from '@uppy/core';
// xhr-upload is for uploading to your own back end.
import XHRUpload from '@uppy/xhr-upload';
// Your own metadata on files
type Meta = { myCustomMetadata: string };
// The response from your server
type Body = { someThingMyBackendReturns: string };
const uppy = new Uppy<Meta, Body>().use(XHRUpload, {
endpoint: '/upload',
});
const id = uppy.addFile({
name: 'example.jpg',
data: new Blob(),
meta: { myCustomMetadata: 'foo' },
});
// This is now typed
const { myCustomMetadata } = uppy.getFile(id).meta;
await uppy.upload();
// This is strictly typed too
const { someThingMyBackendReturns } = uppy.getFile(id).response.body!;
Happy inferring!
Merging the two AWS S3 plugins
We used to have two separate plugins for uploading to S3 and S3-compatible
services: @uppy/aws-s3
and @uppy/aws-s3-multpart
. They have different use
cases. The advantages of multipart uploads are:
- Improved throughput – You can upload parts in parallel to improve throughput.
- Quick recovery from any network issues – Smaller part size minimizes the impact of restarting a failed upload due to a network error.
- Pause and resume object uploads – You can upload object parts over time. After you initiate a multipart upload, there is no expiry; you must explicitly complete or stop the multipart upload.
- Begin an upload before you know the final object size – You can upload an object as you are creating it.
However, the downside is request overhead, as it needs to do creation, signing, and completion requests besides the upload requests. For example, if you are uploading files that are only a couple kilobytes with a 100ms roundtrip latency, you are spending 400ms on overhead and only a few milliseconds on uploading. This really adds up if you upload a lot of small files.
AWS – and the internet in general, from what we found – tends to agree that you don’t want to use multipart uploads for files under 100 MB. This sometimes puts users of our libraries in an awkward position, though, as their end users may not exclusively upload very large files, or only small files. In this case, a portion of their users get a subpar experience.
We’ve merged the two plugins into @uppy/aws-s3
with a new
shouldUseMultipart
option! By default,
it switches to multipart uploads if the file is larger than 100 MB. You can pass
a boolean
or a function to determine this per file.
React hooks
People working with React are more likely to create their own user interface on top of Uppy than those working with “vanilla” setups. Working with our pre-built UI components is a plug-and-play experience, but building on top of Uppy’s state with React primitives has been tedious.
To address this, we’re introducing two new hooks: useUppyState
and
useUppyEvent
. Thanks to the TypeScript rewrite, we can now do powerful
inference in hooks as well.
useUppyState(uppy, selector)
Use this hook when you need to read Uppy’s state.
import { useState } from 'react';
import Uppy from '@uppy/core';
import { useUppyState } from '@uppy/react';
// IMPORTANT: passing an initializer function
// to prevent Uppy from being recreated on every render.
const [uppy] = useState(() => new Uppy());
const files = useUppyState(uppy, (state) => state.files);
const totalProgress = useUppyState(uppy, (state) => state.totalProgress);
// We can also get a specific plugin state.
// Note that the value on `plugins` depends on the `id` of the plugin.
const metaFields = useUppyState(
uppy,
(state) => state.plugins?.Dashboard?.metaFields,
);
You can see all the values you can access on the
State
type.
Using this hook, you can also access the state of any Uppy plugin. For example,
in order to access the state of ImageEditor
, you would have to look at the
types of the plugin.
import type { State } from '@uppy/core';
useUppyEvent(uppy, event, callback)
Listen to Uppy events in a React component.
The hook returns [results, clear]
. results
is an array of values from the
event. Depending on the event, this can be empty or have up to three values.
clear
is a function to clear the results
array.
Values remain in state until the next event (if that ever comes) or the moment when the state is manually cleared. Depending on your use case, you may want to keep the values in state or clear the state after something else happened.
import { useState } from 'react';
import Uppy from '@uppy/core';
import Transloadit from '@uppy/transloadit';
import { useUppyEvent } from '@uppy/react';
// IMPORTANT: passing an initializer function
// to prevent Uppy from being recreated 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', () => {
// Handle event here.
});
Google Photos
An often requested feature is finally here: Google Photos support!
Uppy can bring in files from the cloud with Companion.
Companion is a hosted, standalone, or middleware server that takes away the complexity of authentication and the cost of downloading files from remote sources, such as Instagram, Google Drive, and others.
This means a 5 GB video isn’t eating into your users’ data plans and you don’t have to worry about OAuth.
@uppy/google-drive
and
@uppy/google-photos
are separate plugins. However, you
can use the same OAuth app for both these plugins. Be sure to enable “Photos
Library API” in your OAuth app, though!
UX improvements for viewing remote files
When using Dashboard with any of our remote sources (Google
Drive, Facebook, etc.) you use our internal @uppy/provider-views
plugin to
navigate the folders and select files. In Uppy 4.0, we are making a few quality
of life improvements for users. The main changes are described in the table
below.
Folder states: checked, unchecked, partial
In 4.0, we introduce a new folder state – a "partially checked" folder. A folder acquires this state when certain files within the folder are "checked", and other files are "unchecked". |
|
PREVIOUSLY | NOW |
---|---|
Cache
When navigating in and out of folders, you no longer have to wait for the same API call — results are cached. |
|
PREVIOUSLY | NOW |
Restrictions
Uppy supports file restrictions, such as maximum number of files and maximum file size. In 4.0, we reworked our restrictions UI – users will get immediate feedback upon exceeding the number of selected files, and get a chance to re-enter the correct number of files after their first upload attempt. |
|
PREVIOUSLY | NOW |
We’re confident this turns our interface for remote sources into the most advanced one out there. We’ve seen some competing libraries not even aggregating results beyond the first page returned by the provider API.
Revamped options for @uppy/xhr-upload
In previous versions, the @uppy/xhr-upload
plugin had the options
getResponseData
, getResponseError
, validateStatus
and
responseUrlFieldName
. These were inflexible and too specific. Now we have
hooks similar to @uppy/tus
:
onBeforeRequest
to manipulate the request before it is sent.shouldRetry
to determine if a request should be retried. By default, three retries with exponential backoff. After three attempts it will throw an error, regardless of whether you returnedtrue
.onAfterResponse
called after a successful response, but before Uppy resolves the upload.
You could, for instance, use them to refresh your auth token when it expires:
import Uppy from '@uppy/core';
import XHR from '@uppy/xhr-upload';
let token = null;
async function getAuthToken() {
const res = await fetch('/auth/token');
const json = await res.json();
return json.token;
}
new Uppy().use(XHR, {
endpoint: '<your-endpoint>',
// Called again for every retry too.
async onBeforeRequest(xhr) {
if (!token) {
token = await getAuthToken();
}
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
},
async onAfterResponse(xhr) {
if (xhr.status === 401) {
token = await getAuthToken();
}
},
});
Check out the @uppy/xhr-upload
docs for more info.
Simpler configuration for @uppy/transloadit
To get started with @uppy/transloadit
, you would configure
assemblyOptions
with your auth key,
template ID, and other optional values. assemblyOptions
can be an object or a
function, which is called per file, which returns an object:
{
"params": {
"auth": { "key": "key-from-transloadit" },
"template_id": "id-from-transloadit",
"steps": {
// Overruling Template at runtime
},
"notify_url": "https://your-domain.com/assembly-status"
},
"signature": "generated-signature",
"fields": {
// Dynamic or static fields to send along
}
}
When you go to production, you always want to make sure to set the signature
.
Not using
Signature Authentication
can be a security risk. Signature Authentication is a security measure that
can prevent outsiders from tampering with your Assembly Instructions.
This means the majority of implementers will write something like this, as recommended:
import Uppy from '@uppy/core';
import Transloadit, { type AssemblyOptions } from '@uppy/transloadit';
new Uppy().use(Transloadit, {
async assemblyOptions(file) {
const response = await fetch('/transloadit-params');
return response.json() as AssemblyOptions;
},
});
However, now you are making a request to your back end for every file, while
the response likely remains the same, unless you are setting dynamic fields
per file.
This has now been improved to:
- Only call
assemblyOptions()
once. fields
is for global variables in your template.- All metadata on your files is automatically sent along to Transloadit. This
will end up in
file.user_meta
for you to dynamically access in your Template per file.
You can read more about Assembly Variables in the docs.
Companion
Streaming uploads by default
Streaming uploads are now the default in Companion. This comes with greatly improved upload speeds and allows uploading up to hundreds of gigabytes without needing a large server storage. We found that this improves speeds by about 37% for a Google Drive upload of a 1 GB file (source). This feature was also available before, but we wanted to have more real-world usage before setting it as the default.
With streaming upload disabled, the whole file will be downloaded first. The upload will then start when the download has completely finished.
When streaming upload is enabled, Companion will start downloading the file from the provider (such as Google Drive), while at the same time starting the upload to the destination (such as Tus), and sending every chunk of data consecutively.
For more information, see the Companion docs.
corsOrigins
option is now required
As a security measure, we now require the
corsOrigins
option to be set.
It serves two purposes: it sets the Access-Control-Allow-Origin
header as well
as the target origin for window.postMessage()
, which is needed to communicate
the OAuth token from the new tab you used to log in to a provider back to
Companion.
And more
The 4.0 release contains over 170 contributions, many too small to mention, but together resulting in Uppy continuing to grow and improve. We closely listen to the community and are always looking for ways to improve the experience, for users and developers alike.
Ready to upgrade? Be sure to check out the migration guide.