Create a File Uploader with React and WordPress REST API (Media)

So I’m working on a small project and I ran into a task involving a form with a file input that has to update an image attached to a custom post type. It got pretty interesting because it took a bit of digging to get it to work. While there was not a lot of examples of it online, and when there were some, they’re mostly using jQuery. So I decided it’s worthy of a little tutorial, and can come in handy in the future.

We’re using React, Axios and the Media endpoint in WordPress’ REST API. First of, let’s see what we’re going to build:

We’re not going in details on how the modal or the loading gif works. We’re really focusing on the file input and the events that happen when the “Update” button is clicked.

Read to get started? Let’s begin.

First, let’s import our modules.

import React, { useState, useRef } from "react";
import axios from 'axios';

Let’s create a component called “ItemModal”, and add some HTML.

function ItemModal( { itemid } ){
    return (
        <div>
           {(()=>{
              if(img){
                return (<img src={img} alt='image' />)
              }
            })()}
           <input id="imgUpload" type="file" ref={imgUpload} onChange={previewImage}/>
           <button onClick={updateItem}>Update</button>
        </div>
    )
}

Above is a simple component that shows an image coming from our local state, an input of type “file” with a “ref” (will explain later), and a button that updates our record.

Let’s add a couple of hooks.

const imgUpload = useRef(null);
const [img, setImg] = useState('');

We need both hooks to make our elements work. We use “useRef”, to create a reference to our file input in the DOM. Also an “img” state, so we can store the “URL” of the image we’re working with.

The Update Item method

The file input works almost out of the box. Upon clicking, the OS file explorer will popup and navigate through your local files so you can select. But actually getting that file to the WordPress is what this Update method will entail.

function updateItem(){
      if(imgUpload.current.files.length > 0){
        var formData = new FormData();
        let file = imgUpload.current.files[0];
        formData.append( 'file', file );
        formData.append( 'title', file.name );
        formData.append( 'post', itemid ); //coming from props
        let headers = {};
        headers['Content-Disposition'] = 'form-data; filename=\''+file.name+'\'';
        headers['X-WP-Nonce'] = 'your nonce here...';
        axios.post('/wp-json/wp/v2/media/?featured='+itemid,formData, headers ).then(function(resp){
          getItems(); //callback to parent's this.getItems(),
        })
      }
    }

So basically, when we click our “Update” button, it triggers the above code. We’re checking if our DOM ref “imgUpload” has a file. The we create a FormData object so we can stuff it with our file details. 3 things in particular: 1) the file itself, 2) the title and 3) the post id – if you’re attaching it to a particular post / page or custom post type.

One thing to note is the custom headers that we are using. We need “Content-Disposition” – this is to tell WordPress’ API about our request, as well as “X-WP-Nonce“. Since this is only going to work when we’re authenticated, and the nonce is how WordPress handles this scenario.

You also notice in the media endpoint, we’re passing “?featured=id“. This completely optional – and only add this if you want it to be the “featured image“. You also need to add this to your functions.php file to get it to work:

add_filter( 'rest_prepare_attachment', 'attach_media_to_post',10,3);
function attach_media_to_post($response, $post, $request) {
    if($request->get_method()!='POST'){
        return $response;
    }
    $parameters = $request->get_params();
    if(isset($parameters['featured'])){
        set_post_thumbnail($parameters['featured'],$post->ID);
    }
    return $response;
}

Preview Image

Now the code above should work as is. But one thing you’ll see is when you select an image, our “img” tag remains the same as the old image – until you update it in the backend.

I want the user to see the file they selected before they even click “Update”. Well if you caught it early, our input has an “onChange” event bound to “previewImage”. That’s what its about.

function previewImage() {
      var oFReader = new FileReader();
      oFReader.readAsDataURL(imgUpload.current.files[0]);
      oFReader.onload = function (oFREvent) {
        setImg(oFREvent.target.result);
      };
    };

We’re using the same DOM ref “imgUpload”, and passing it into a “FileReader” object. And since our img tag is using our “img” state for it’s source, we simply call “setImg” to update that state.

Our selected image will show whenever we change it.

Improvements

This is a pretty barebones form / file upload component. There’s so much more we can do to improve it. For one, we need to restrict file types and file sizes. Maybe a nice error messaging when they select an invalid file.

Also, in my case, I want to delete the original image (since I don’t want it taking space in my server). So I simply add another AJAX call inside our update function (inside the successful axios.post).

Something like:

function deleteOrigImg(){
  if(origImg){
	let url = window.ONW.wp_api+'/media/'+origImg+'?force=1';
	axios.delete('/wp-json/wp/v2/media/',header).catch((err)=>{
	  handleError(context,err);
	})
  }
}

One last thing, you also would want to see the error logs when working with WordPress REST API. Now this can be cumbersome especially when working with the out of the box endpoints.

This plugin REST API Log provides an easy to use utility, so you can see what’s going on with your request / response. You’ll thank me (or the plugin author) later.

5 Comments

  1. Great tuts thanks.
    I have a question regarding multiple media upload.
    I try to loop through my images array and append inside but only the first image is uploaded to WordPress media library.
    I spend a day for that but could not achieve that. Any idea ?!
    Thanks
    Cchumi

    Reply
  2. Hey Micheal thank you so much for this! This didnt work for me initially, but all I had to do was wrap the header object in an object literal : header -> {headers}. So -> axios.post(‘/wp-json/wp/v2/media/?featured=’+itemid,formData, headers ) becomes axios.post(‘/wp-json/wp/v2/media/?featured=’+itemid,formData, {headers} ) this works!

    Reply

Leave a Reply to Cchumi Cancel reply