Skip to main content

Uppy 4.0 is here: TypeScript rewrite, Google Photos, React hooks, and much more.

New Uppy 4.0 major: TypeScript rewrite, Google Photos, React hooks, and much more

· 11 min read
Screenshot of Uppy Dashboard with text outlining the major new features in 4.0

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!

Uppy 4.0 - TypeScript rewrite, Google Photos, and much more | Product Hunt

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!

info

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 returned true.
  • 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.

Uppy 4.0 - TypeScript rewrite, Google Photos, and much more | Product Hunt