DEV Community

Cover image for Uploading Image and Text from React to Django with JSON and Proper Encoding.
Frank Ezenwanne
Frank Ezenwanne

Posted on

Uploading Image and Text from React to Django with JSON and Proper Encoding.

Uploading only text as JSON from React to Django is quite straightforward. Django serializers easily convert JSON to python native data types. JSON is just like python dictionary in strings(quotes).

But then, how can we send images along with text?. Well, sending image files fixed in the react component state by e.target.files[0] did not work.

A little research brought up the idea of encoding the images as base64!!😈

Okay..okay, Don't let that scare you.

Two or three lines on the frontend, we are done. We don't even need to install anything. Same thing on the backend, 2 lines and we just need to pip install a little package.

We don't even need any header on the frontend, except you are sending an authorization token. We don't even need parsers in the API.

Highlights.

NB: This is not a tutorial on setting up Django with React. A good tutorial on that would be a YouTube series by Brad Traversy (Traversy Media), Full stack React and Django.
For pure Django, then Django Tutorials by Corey Schafer.

We are gonna be using a Post model as an example (Like a Blog Post).

THE BACKEND (DJANGO REST)

  • Create the Post Model
  • Adding MEDIA_URL and MEDIA_ROOT to settings.
  • Pip installing drf_extra_fields (the only installation)
  • Create the serializers
  • Create the API
  • Set up the url.

THE FRONTEND ( REACT )

  • Set up the PostCreate component with state and onChange attribute.

  • Adding the Image change handler, conversion of the image to base64 with FileReader.

  • Setting up axios and sending the JSON data with the base64 representation.

NOW LET'S GO INTO DETAILS

THE BACKEND

1. Create the Post model

We will start by creating the Post model

from django.db import models
from django_resized import ResizedImageField
from django.utils.text import slugify
from django.utils import timezone
from django.urls import reverse
from django.contrib.auth.models import User


class Post(models.Model):
    title = models.CharField(max_length=150)
    slug = models.SlugField(blank=True)
    file = models.ImageField(null=True,upload_to = 'post_pics',blank=True)
    date_posted = models.DateTimeField(default = timezone.now)
    content = models.TextField()

    def __str__(self):
        return f'Post : {self.title}'

    def save(self, force_insert=True ,*args , **kwargs):
         if not self.slug:
            super().save(*args,**kwargs)
            pk=str(self.pk)
            slug_field = slugify(self.title) + pk
            self.slug = slug_field
            return super().save(*args,**kwargs)
         return super().save(*args,**kwargs)

    def get_absolute_url(self):
        return reverse('post-detail',kwargs ={"slug":self.slug})

Enter fullscreen mode Exit fullscreen mode

The image field takes null=True to allow image upload to be optional.

2.) Adding MEDIA_URL and MEDIA_ROOT to settings.

Next, we'll add MEDIA_ROOT AND MEDIA_URL to Django settings.py to enable us to create a local storage location for the uploaded images.

MEDIA_ROOT = os.path.join(BASE_DIR,'media')
MEDIA_URL = '/media/'
Enter fullscreen mode Exit fullscreen mode

3.) Pip installing drf_extra_fields (the only installation)

This is the only installation we will be doing in this tutorial. We need to use the Base64ImageFieldin the package to accept the base64 data.

pip install drf_extra_fields
Enter fullscreen mode Exit fullscreen mode

4.) Create the Serializer class

from rest_framework import serializers
from .models import Post, Review

from drf_extra_fields.fields import Base64ImageField

class PostSerializer(serializers.ModelSerializer):
    file = Base64ImageField()

    class Meta:
        model=Post
        fields= ('title','file','content')
Enter fullscreen mode Exit fullscreen mode

Notice how the file field was set to be the Base64ImageField. The field will receive the base64 data and will allow for conversion back to an image.

5.) Create the API

from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from .models import Post
from .serializers import PostSerializer, 


class PostAPI(APIView):
    permission_classes = [IsAuthenticated]
    def post(self,request,*args,**kwargs):
        serializer = PostSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()
        response = {        "title":instance.title,"content":instance.content,"date":instance.date_posted.strftime("%a %H:%M %d/%m/%y"),"file":instance.file.url,
            "url":instance.get_absolute_url()
        }
        return Response(response)
Enter fullscreen mode Exit fullscreen mode

The api gets the JSON data from the frontend, passes into the serializer, which validates and saves the data with the base64 being converted back to an image. Finally it accesses the properties of the saved instance including the url of the saved image and sends it back. I am not sending back the image to the frontend, but rather, a link to the saved image on my local drive.

You might be thinking why not send back as base 64.. Well, that would mean I can't open up the storage location and view. Also, in the frontend, I'll have to convert again from base64. So I didn't bother. So I think it is better this way.

6.) Set up the url.

from django.urls import path
from .api import PostAPI


urlpatterns=[
    path('api/create',PostAPI.as_view()),]

Enter fullscreen mode Exit fullscreen mode

Here, we set up the URL necessary to link the react request to the api.

That's it for the Backend..

THE FRONTEND (REACT)

1. Setting up the PostCreate component with state and onChange attribute.

import React, {Component} from "react"
import axios from "axios"

class PostCreate extends Component{

    state = {
        title: "",
        content: "",
        file: ""
    }


    onchange=(e) =>{
        this.setState({[e.target.name] : e.target.value})
    }

    render(){

        const { title, content, file} = this.state

        return(
            <div className = 'create-form'>
                <h4 align="center" className = 'createpost-heading'>Start A New Topic</h4>
                <span className ="create-post-requirement">A Title is Enough To Start.. </span>
                <form onSubmit={this.onsubmit}>
                    <div className = 'form-field'>
                        <span className= "asterik-field">*</span>
                        <label className= "post-create-label-first" htmlFor = "id_title">Title</label><br/>
                        <input id = "id_title"
                        className = 'user-field'
                        type ='text'
                        name ='title'
                        placeholder=' Title of Post'
                        size = '110'
                        maxLength = '100'
                        value = {title} 
                        onChange={this.onchange}
                        />

                    </div><br/>


                    <div id="post-create-text" className = 'form-field'>
                        <label className= "post-create-label" htmlFor = "id_content">Write Something</label>
                        <textarea id = 'id_content'
                        className = 'content-write'
                        type ='text'
                        name ='content'
                        placeholder=' Write post content'
                        rows = '7'
                        cols = '25'
                        value = {content}
                        onChange={this.onchange}>
                        </textarea>
                    </div> <br/>

                    <div id="post-create-image" className = 'form-field'>
                        <label className= "post-create-label" htmlFor = "id_postimage">Upload A Game Pic</label>
                        <input id = "id_postimage" 
                        className = 'post-image-field' 
                        type ='file' 
                        accept = 'image/*'
                        name = 'file' 
                        />
                    </div><br/>

                    <button type = "submit" className = 'form-button'>Submit</button><br/>

                </form>
        </div>

        )
    }
}


export default PostCreate
Enter fullscreen mode Exit fullscreen mode

Here, we have created the component for post creation and put in the fields. We have also set the onChange handler for the title and content fields.

2.) Adding the Image change handler and conversion of the image to base64 with FileReader.

Now let's set up the handler for the image field. You'll see the base64 encoding by FileReader in action here.😈

imageChange = (e) =>{
        const file = e.target.files[0]
        const reader = new FileReader()
        reader.onload = () => {
            this.setState({file : reader.result})
        }

        if(file){
            reader.readAsDataURL(file)
        }

    }
Enter fullscreen mode Exit fullscreen mode

Now, what happens here is very simple. The first line gets the uploaded file under the file variable. The next line creates a FileReader object. Let's visit the last block before the reader.onload. The last block calls reader.readAsDataURL on the file and converts it to base64 format. The reader.onload runs an arrow function when reader.readAsDataURL is triggered to handle a file i.e just like an event Listener. The arrow function simply sets the state with the base64 file.

3.)Setting up axios and sending the JSON data with the base64 representation.

We are gonna be setting up the axios in the onSubmithandler function, so that the axios request is triggered on submission.

onsubmit = (e) =>{
        e.preventDefault();
        const {title,content,file} = this.state
        const token = localStorage.token
        let config={}

        if(token){

            config = {
                "headers": {"Authorization":`Token ${token}`
                  } 
            }
        }

        const body = {title,content,file}
        console.log(body)
        axios
        .post("api/create",body,config)

        .then(
            (res) => {
                console.log(res)
            }
        )
       .catch(
            (err)=>{
                console.log(err.response)
           }
       )
    }
Enter fullscreen mode Exit fullscreen mode

Token was used for authentication explaining the token setting in the header. Using JSON.stringify wasn't necessary on the body before sending. The operation is quite simple. After preventing default submission with e.preventDefault , the fields were extracted from the state, and token, from localStorage. The axios request comes in to finish the job by sending the data and handling success with .then, and failure with .catch

VERSIONS OF TOOLS

 Python == 3.7.2,
 Django == 3.2.13,
 drf-extra-fields == 3.4.0,
"react": "^18.0.0",
"react-dom": "^18.0.0",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"axios": "^0.27.1",
"react-router-dom": "^6.3.0",
"@babel/core": "^7.17.9",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"babel-loader": "^8.2.4",
"babel-plugin-transform-class-properties": "^6.24.1"
Enter fullscreen mode Exit fullscreen mode

And that's a wrap! I hope you enjoyed the article. I'd love to read/hear your comments. 😊

Oldest comments (2)

Collapse
 
dntm2802 profile image
Duarte Mortágua

Hey. What if I want to submit multiple images, for the same post?

Collapse
 
frankezenwanne profile image
Frank Ezenwanne

Hey Duarte, thanks for the comment. I am sorry for replying late. But I think there are two ways to look at it. Defined slots and Undefined slots. It is simpler when you wanna look at in terms of defined slots. Defined slots meaning, you might set a max of 4 images per post . Hence the serializer fields would have names like file1, file2...file4. which would be optional and all would be Base64ImageFields.