Upload to Azure Blob Storage with React
How to upload multiple files to blob storage in a browser with a Shared Access Signature (SAS) token generated from your back-end.
We’ll use React 16.11 and the @azure/storage-blob library to upload the files.
The final code is on Github which also contains examples on listing containers, blob items and deleting and downloading blob items.
We’ll go over
- Creating a component to select and upload files
- Creating a service to manage view state for the uploads
- Securing the upload to Blob Storage with a SAS token
- Creating a service to wrap the
uploadBrowserData
method in the @azure/storage-blob library to upload a file to Blob Storage - Creating a component to display upload progress
Create File Upload Component
The InputFileComponent
component allows the user to select one or more files to upload
We’ll cover
- Selecting one or more files to upload
- Calling the method on the view state service to start the upload
What’s happening?
We’re getting the upload service functionality with useContext
so that view state can remain outside of the component lifecycle.
The input is hidden and the file dialogue is opened with the button. The uploadItems
method is called on the BlobUploadsViewStateService
when the user selects files. We could add some validation here, but to me, it makes more sense to validate in the view state service.
Create uploads view state service
The BlobUploadsViewStateService
service manages the shared view state for the components. I’m quite familiar with redux patterns so I’ve created a service into which you can supply items to trigger actions and have observables listening for changes. It also means we can keep the business logic out of the components and the components will generally have a single responsibility.
We’ll cover
- Listing to an observable to trigger uploads
- Getting a SAS token
- Uploading a file to blob storage
- Keeping a list of all upload progress in the view state
What’s happening?
The public uploadItems
method accepts a list of files and calls the next
method on the uploadQueueInner$
subject. The uploadQueue$
getter is listening to the subject as an observable and will emit each file in the files list as a separate item. This is one place the files could be validated.
The uploadedItems$
property is listening to the uploadQueue$
and will call the uploadFile
method in the service for each file item emitted. Something will need to subscribe to the uploadedItems$
for uploads to start and display upload progress (we’ll cover this below in the upload progress component)
The uploadFile
method gets the latest SAS token and calls the uploadToBlobStorage
on the Blob Storage Wrapper Service with the token and file details (we’ll cover this below).
The uploadToBlobStorage
method returns an observable that emits the loadedBtyes each time it changes and can be used to track upload progress.
We then map the upload response to a percentage in the mapUploadResponse
method and include extra detail. The method also has the startWith
operator so any subscribers will be notified as soon as the upload starts rather than waiting for progress to be emitted.
We then call the finalise method when the upload is complete to refresh the items in the blob container.
The mapped response from the uploadFile
method is then piped into a custom scan
operator function to reduce the emitted values into an array. This allows us to store all uploads in progress and add further uploads when uploads are already in progress.
Secure the upload with a SAS token
The service above calls a method to get a SAS before every upload from a service. The method in this code example returns a hard codes SAS token I generated in the Azure portal, but in the real world, you would call an API to generate and return the SAS token. Here is a basic example in C# using the Azure.Storage.Blobs (v12.0.0) package to generate an Account SAS which can be used for many operations. You can also create SAS tokens for specific blob items or containers when you want/need to be more granular.
Wrap the uploadBrowserData method in the @azure/storage-blob library
We wrap the @azure/storage-blob library to return observables rather than promises as it works well when emitting progress events.
We’ll cover
- Creating a blobServiceClient from the @azure/storage-blob library
- Calling the
uploadBrowserData
method and returning an observable of the loadedBytes
Here is a snippet from the full service
What’s happening?
The uploadToBlobStorage
method in the wrapper service accepts the file to be uploaded and an object with the SAS token. The blobServiceClient
was created using a connection string which included the SAS token.
We then pass the BlockBlobClient
created using the blobServiceClient
and the file into the uploadFile
method which wraps the uploadBrowserData
method. We listen to the onProgress
events and emit the value each time the progress changes. We then emit the file size and complete the observable when the upload completes.
Create a component to display upload progress
In this code example, the view state services manage the shared data and the components display the data. This good practice allows us to separate the file input and upload progress components and keeps our components small and have fewer responsibilities (hopefully just one).
What’s happening?
We get the uploads view state from the context and utilise theuseEffect
hook to manage the subscription to uploadedItems$
on the BlobUploadsViewStateService
service we referenced above.
We then update the components state with the items and render the items to the user.
Conclusion
This is one way the upload to Azure blob storage could be implemented and I’ve attempted to demonstrate a basic architecture for this as well as just showing example code.
We could improve the solution by adding validation for the files before upload and calculating the upload speed. We’re also not handling errors well.
We could use redux with Thunks, Sagas or Epics for larger applications rather than a view state service
The components could also be split into smart and dumb components where the smart component gets the data and calls functions on the view state services and the dumb component, which use props, display the data.
You can also see a working example of the solution here. Apologies for the lack of styling.
Please let me know if you have any comments or suggestions about the article below, but please raise any code issues on the Github repo