Downloading and Saving binary files using React Native with Expo
— JavaScript, Software Development, Testing — 3 min read
A key part of the app I’m building (JiffyCV) is to generate a PDF of the document you’ve been creating, but when it came to implementing the saving of the PDF there wasn’t much documentation so I’m going write some in the hope this helps others looking to add this functionality to their app.
In order to download and save the PDF in my app I’ve had to implement the following flow:
- Make the request to the server with axios
- Turn the response’s data in to a base64 string using Buffer
- Get a path for the file to be saved under
- Write the file to the path in order to have it available to share
- Initiate the device’s sharing functionality to share the file
- The user then has the option to save the file to the file system or share it elsewhere
Libraries used
There are a number of libraries that are needed to implement the flow described above, most are universally needed but axios
is more of a personal preference.
The libraries are:
- axios — For making the requests to server
- jest-mock-axios — For mocking out the server response during testing
- buffer — For turning the response from server into base64 string
- expo-file-system — For saving the file to app’s doc store before we share it
- expo-sharing — For allowing the user to share / save the file to device
Getting the file from the server
My app is using a serverless function to create a PDF using puppeteer and then returning the PDF object.
The response from the serverless function is base64 encoded but I’m using the arraybuffer
responseType of axios
which means I need to use buffer
to convert these into a base64 string I can use to create the file locally.
This was the only approach with axios
that I found worked, however if you’ve got a better way to do this then please leave a comment with your solution.
Saving the file to the app’s document store
Once we have the base64 string for the PDF we want to save we need to create the path we’re going to save the file under and save it to the app’s document directory.
You can get the path to save the file under by first getting the app’s document directory using the documentDirectory
property from expo-file-system
. You then need to append the filename you’ll save the file under.
The document directory will have the /
suffixed to it so there’s no need include this in your file name and you’ll need to make sure your filename is URI encoded as if it’s not you may end up with half a filename being shown to the user.
After getting the path to save the file under you can call writeAsStringAsync
from expo-file-system
passing it the filename, the base64 representation of the file, and as you’re using base64 you’ll need to pass it an options object with the encoding
property set to EncodingType.Base64
.
Allowing the user to share / save the file
This part is relatively simple as you just need to pass the path to the saved file in the app’s document store to the shareAsync
function of expo-sharing
. This will then bring up the dialog to allow the user to save the file to their device or share it via the apps they have installed on their phone.
Testing the flow
In order to test the calls to the server with axios
you can use the jest-mock-axios
library which allows for asserting that an endpoint was called as well as allowing for response values to be asserted against.
The jest-mock-axios
way of stubbing server responses isn’t the most elegant solution as it requires you to define the stubbed response after calling the axios code, but once you have the hang of the flow it works well.
When using jest-mock-axios
it’s very important to remember to call mockAxios.reset()
after each test, as if you don’t the response stubbing won’t work as expected.
Once the call to the server and its response are mocked then you’ll need to mock the expo-file-system
and expo-sharing
modules in order to control how the documentDirectory
, writeAsStringAsync
and shareAsync
calls behave.
You can use the standard jest.mock
approach for this as shown below. As long as the module mocks return an object with properties for the functions you are calling then it’ll work. If you want to control the way each function behaves or assert the function was called, then using a named mock and use mockImplementation
can change the behaviour.
Summary
Using a combination of expo-file-system
and expo-sharing
allows for an easy to implement test solution and the ability for the user to decide what they want to do with the file after is a really nice user experience, as they may not always wish to save it but instead send it via another app.