Laravel & React Native File Upload

In this article, we will see how we can do file upload from react native mobile app to a laravel API, we will also cover validations, error response, and some other cool stuff 🤓.

Laravel & React Native File Upload

File upload can be a bit tricky, especially when dealing with mobile applications, so i will try to detail this as much as i can.

Let's jump into this 🪂

Building The API

Create Form Request

They are useful to deal with request validation outside our controller logic which will keep it cleaner.

php artisan make:request FileUploadRequest
<?php 

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Http\Response;

class FileUploadRequest extends FormRequest {
  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
 public function authorize() { return true; }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
 public function rules() {
    return ['file' => 'required|mimes:csv,txt,xlx,xls,pdf|max:10240'];
  }

 public function messages() {
    return [
      'file.required' => 'A file is required',
      'file.mimes' => 'Accepted file types: csv,txt,xlx,xls,pdf',
      'file.max' => 'The document must not be greater than 10 megabytes',
    ];
  }

 protected function failedValidation(Validator $validator) {
    $response = new Response(
        [
          "success" => false, "message" => "File upload failed.",
          'errors' => $validator->errors()
        ],
        422);
    throw new ValidationException($validator, $response);
  }
}

Here we can see a couple of stuffs:

  • First, the authorize() method as the name suggests, it's used to check whether the user has the right to perform an action or not, simply ACL.

    Setting it to TRUE will give the right to allow all users to perform that action.

  • We have also the rules() method, where we can put our validation logic, and we can use any validation rules from docs.

  • For custom error messages we use the messages() method, you can remove this method and just use Laravel defaults.

  • In order to return errors as a JSON object to the client, we did use failedValidation() method, in here you can deal with request validation failures.

Create Controller

php artisan make:controller "Api\UploadController"

<?php

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Http\Requests\FileUploadRequest;

class UploadController extends Controller {
 
 public function upload(FileUploadRequest $request) {
    try {
	  
      $fileName = time().'_'.$request->file->getClientOriginalName();
      $request->file('file')->storeAs('uploads', $fileName, 'public');

      return response()->json(
          [
            "success" => true,
            "message" => "File has been uploaded.",
          ],
		200);

    } catch (\Throwable $th) {
      return response()->json(
          [
            "success" => false, 
			"message" => "File upload failed.",
            'errors' => ['file' = > "Oops! something went wrong."]
          ],
		400);
    }
}

We can note certain points:

  • FileUploadRequest instead of Request class.

  • The time() method to avoid overwriting any existing file.

  • The storeAs method will save the file into the default disk.

  • try/catch can be useful if any error pops out like, for example, the disk has no space in it, or if when dealing with s3 or any remote disk. In all cases, we need always a plan B 😎.

📝 A side note: The storeAs method returns a path to the file if you wanna store that in a database.

$path = $request->file('file')->storeAs('uploads', $fileName, 'public');

Running The Project

Now we are done with the backend, I wanna highlight something very important 🚨.

In development in order to consume the API from a mobile application, you have to know that you can not use localhost cause you will get an Axios error, for a simple reason, mobile phones have their own local DNS, so when you type localhost it will connect to you the localhost of the device itself, not your pc localhost.

Anyway, enough of this word 😜

To solve that, the phone and PC need to be connected to the same WIFI network.

We need also our PC's local IP address, we can use these commands to get it.

  • For Linux

hostname -I | awk '{print $1}'
  • For Windows (Hope it works 😅)

ipconfig | find "ipv4" /i

Now we can run our project likewise.

php artisan serve --host=Your local api 

Similarly, you can use 0.0.0.0 to bind all IPv4 interfaces.

php artisan serve --host=0.0.0.0

Building the mobile App

Prerequisites

Check this guide in order to set up your environment docs.

Initiating The Project

npx react-native init reactNativeFileUpload

installing Dependencies

We need mainly react-native-document-picker package to perform file picking from the phone.

Optionally react-native-dotenv just to set some environment variables.

npm i axios react-native-dotenv react-native-document-picker

If you will use the react-native-dotenv package you need to update your babel.config.js.

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
	['module:react-native-dotenv']
  ],
};

After that, we can start coding the app.

import {API_URL} from '@env';                                             
import axios from 'axios';
import React, {useCallback, useState} from 'react';
import {Button, Platform, SafeAreaView, StatusBar, StyleSheet, Text, View,} from 'react-native';
import DocumentPicker, {types} from 'react-native-document-picker';

const App = () => {
  const [success, setSuccess] = useState(false);
  const [errors, setErrors] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const handleDocumentSelection = useCallback(async () => {
    try {
      const file = await DocumentPicker.pick({
        presentationStyle: 'fullScreen',
        type: [types.pdf],
        allowMultiSelection: false,
      });
      if (file && file[0]) {
        var data = new FormData();
        data.append('file', {
          uri:
            Platform.OS === 'android'
              ? file[0].uri
              : file[0].uri.replace('file://', ''),
          name: file[0]?.name,
          type: file[0]?.type,
        });
        var config = {
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'multipart/form-data',
          },
        };
        setSuccess(null);
        setErrors(null);
        setIsLoading(true);
        axios
          .post(`${API_URL}/upload`, data, config)
          .then(response => {
            setSuccess(response.data);
          })
          .catch(({response}) => {
            setErrors(response.data?.errors?.file);
          })
          .finally(() => setIsLoading(false));
      }
    } catch (err) {
      console.warn(err);
    }
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle={'dark-content'} />

      <Button
        disabled={isLoading}
        title={!isLoading ? 'Select 📑' : 'Loading ...'}
        onPress={handleDocumentSelection}
      />

      {success && (
        <View>
          <Text style={styles.success}>{success?.message}</Text>
        </View>
      )}

      {errors && (
        <View>
          {errors.map((err, index) => (
            <Text key={index} style={styles.error}>
              {err}
            </Text>
          ))}
        </View>
      )}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  success: {
    color: 'green',
  },
  error: {
    color: 'red',
  },
});

export default App;

The mobile app is simple we did use do DocumentPicker.pick to choose a file from the phone.

The tricky part here is that you have to use FormData to upload the file with that need to set some HTTP headers.

headers: {
    'Accept': 'application/json',
   'Content-Type': 'multipart/form-data',
}
  • API_URL is from an .env file.

Running The Project

I will use a real device just by plugging it into the computer using a USB and setting it to debug mode.

First, we need to start the development server.

npm start

Now, we can start the application by typing this command.

npm run android

Well, hope this was instructive to you 🥷.

If you have any feedback or just wanna say hi, you can find me on LinkedIn or Twitter.

I wanna thank those who did send me their awesome feedback, it's really heart touching ❤️❤️❤️.

You can find the complete code for the backend in the following Github repository and this Github for the React Native app.

You like it,it will be nice if you share it