One place for hosting & domains

      Cómo crear una aplicación web moderna para gestionar la información de clientes con Django y React on Ubuntu 18.04


      El autor seleccionó Open Sourcing Mental Illness Ltd para recibir una donación como parte del programa Write for Donations.

      Introducción

      Las personas utilizan diferentes tipos de dispositivos para conectarse a internet y navegar por la Web. Debido a esto, las aplicaciones deben ser accesibles desde varios lugares. Para los sitios web tradicionales, tener una IU receptiva suele ser suficiente, pero las aplicaciones más complejas suelen requerir el uso de otras técnicas y arquitecturas. Entre ellas se contempla tener aplicaciones back-end y front-end REST independientes que puedan implementarse como aplicaciones web para el cliente, aplicaciones web progresivas (PWA) o aplicaciones móviles nativas.

      Algunas herramientas que puede utilizar al crear aplicaciones más complejas incluyen:

      • React, un marco de trabajo de JavaScript que permite a los desarrolladores crear frontends web y nativos para sus backend de API REST.
      • Django, una estructura web de Python gratuita y de código abierto que sigue el patrón de arquitectura del software de modelo vista controlador (MVC).
      • Django REST framework, un conjunto de herramientas potentes y flexibles para el desarrollo de REST APIs en Django.

      En este tutorial, usted creará una aplicación web moderna con un backend REST API independiente y un frontend utilizando React, Django y Django REST Framework. Al utilizar React con Django, podrá beneficiarse de los últimos avances en el desarrollo de JavaScript y front-end. En lugar de crear una aplicación de Django que utilice un motor de plantillas incorporado, usted utilizará React como biblioteca de IU, aprovechando su enfoque virtual de Modelo de objetos de documentos (DOM), enfoque declarativo y componentes que rápidamente reproduzcan cambios en los datos.

      La aplicación web que usted creará almacena registros sobre clientes en una base de datos, y puede utilizarlo como punto de partida para una aplicación CRM. Cuando haya terminado, podrá crear, leer, actualizar y borrar registros utilizando una interfaz React de estilo Bootstrap 4.

      Requisitos previos

      Para completar este tutorial, necesitará lo siguiente:

      Paso 1: Creación de un entorno virtual de Python e instalación de dependencias

      En este paso, crearemos un entorno virtual e instalaremos las dependencias necesarias para nuestra aplicación, entre ellas, Django, Django REST framework y django-cors-headers.

      Nuestra aplicación utilizará dos servidores de desarrollo distintos para Django y React. Se ejecutarán en diferentes puertos y funcionarán como dos dominios separados. Debido a esto, debemos permitir el intercambio de recursos de origen cruzado (CORS) para enviar solicitudes HTTP desde React a Django sin que el navegador pueda bloquearlos.

      Navegue a su directorio principal y cree un entorno virtual utilizando el módulo venv Python 3:

      • cd ~
      • python3 -m venv ./env

      Active el entorno virtual creado utilizando “source:

      A continuación, instale las dependencias del proyecto con pip. Entre ellas se incluyen:

      • Django: el marco de trabajo web para el proyecto.
      • Django REST framework: Una aplicación externa que desarrolla REST APIs con Django.
      • django-cors-headers: un paquete que habilita CORS.

      Instalar la estructura de Django:

      • pip install django djangorestframework django-cors-headers

      Con las dependencias del proyecto instaladas, puede crear el proyecto Django y el frontend de React.

      Paso 2: Creación del proyecto Django

      En este paso, generaremos el proyecto Django utilizando las siguientes comandos y utilidades:

      • **django-admin startproject project-name**:django-admin es una utilidad de línea de comandos que se utiliza para realizar tareas con Django. El comando startproject crea un nuevo proyecto Django.

      • **python manage.py startapp myapp**: manage.py es un script de utilidad que se añade automáticamente a cada proyecto de Django, y que ejecuta varias tareas administrativas, como crear nuevas aplicaciones, migrar la base de datos y servir de forma local el proyecto de Django. Su comando startapp crea una aplicación de Django dentro del proyecto Django. En Django, el término aplicación describe un paquete de Python que proporciona algunas características en un proyecto.

      Para comenzar, cree el proyecto Django con django-admin startproject. Hemos de nombrar a nuestro proyecto djangoreactproject

      • django-admin startproject djangoreactproject

      Antes de seguir, observemos la estructura del directorio de nuestro proyecto Django utilizando el comando tree.

      Nota: tree es un comando útil para visualizar estructuras de archivos y directorio desde la línea de comandos. Puede instalarlo con el comando que se indica a continuación:

      • sudo apt-get install tree

      Para utilizarlo, use el comando cd en el directorio que desee y escriba tree o proporcione la ruta al punto de partida con tree /home/sammy/sammys-project.

      Navegue a la carpeta djangoreactproject dentro del root de su proyecto y ejecute el comando tree:

      • cd ~/djangoreactproject
      • tree

      Verá lo siguiente:

      Output

      ├── djangoreactproject │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py

      La carpeta ~/djangoreactproject es la root del proyecto. Dentro de esta carpeta, hay varios archivos que serán importantes para su labor:

      • manage.py: el script de utilidad que realiza diversas tareas administrativas.
      • settings.py: el archivo de configuración principal para el proyecto de Django, donde puede modificar la configuración del proyecto. Estos ajustes incluyen variables como INSTALLED_APPS, una lista de strings que designan las aplicaciones habilitadas para su proyecto. La documentación de Django contiene más información sobre los ajustes disponibles.
      • urls.py: este archivo contiene una lista de patrones de URL y vistas relacionadas. Cada patrón traza una conexión entre una URL y la función que se debe solicitar para esa URL. Para más información sobre URL y vistas, consulte nuestro tutorial sobre Cómo crear vistas de Django.

      Nuestro primer paso en el trabajo con el proyecto será configurar los paquetes que instalamos en el paso anterior, incluidos Django REST framework y el paquete Django CORS, al añadirlos a settings.py. Abra el archivo con nano o su editor favorito:

      • nano ~/djangoreactproject/djangoreactproject/settings.py

      Navegue al ajuste INSTALLED_APPS y añada las aplicaciones rest_framework y corsheaders en la parte inferior de la lista:

      ~/djangoreactproject/djangoreactproject/settings.py

      ...
      INSTALLED_APPS = [
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
          'rest_framework',
          'corsheaders'
      ]
      

      A continuación, añada el middleware corsheaders.middleware.CorsMiddleware desde el paquete CORS instalado previamente al ajuste MIDDLEWARE. Este ajuste es una lista de middlewares, una clase de Python que contiene código que se procesa cada vez que su aplicación web gestiona una solicitud o respuesta:

      ~/djangoreactproject/djangoreactproject/settings.py

      ...
      
      MIDDLEWARE = [
      ...
      'django.contrib.messages.middleware.MessageMiddleware',
      'django.middleware.clickjacking.XFrameOptionsMiddleware',
      'corsheaders.middleware.CorsMiddleware'
      ]
      

      A continuación, puede habilitar CORS. El ajuste CORS_ORIGIN_ALLOW_ALL especifica si desea habilitar CORS para todos los dominios o no, y CORS_ORIGIN_WHITELIST es una tupla de Python que contiene URL permitidas. En nuestro caso, dado que el servidor de desarrollo de React se ejecutará en http://localhost:3000, añadiremos nuevos ajustes CORS_ORIGIN_ALLOW_ALL = False y CORS_ORIGIN_WHITELIST('localhost:3000',) a nuestro archivo settings.py. Añade estos ajustes en cualquier parte del archivo:

      ~/djangoreactproject/djangoreactproject/settings.py

      
      ...
      CORS_ORIGIN_ALLOW_ALL = False
      
      CORS_ORIGIN_WHITELIST = (
             'localhost:3000',
      )
      ...
      

      Puede encontrar más opciones de configuración en la documentación de django-cors-headers.

      Guarde el archivo y salga del editor cuando haya terminado.

      Aún en el directorio ~/djangoreactproject, cree una nueva aplicación de Django llamada customers:

      • python manage.py startapp customers

      Contendrá los modelos y las vistas para gestionar clientes. Los modelos definen los campos y los comportamientos de nuestros datos de aplicaciones, mientras que las vistas permiten a nuestras aplicaciones gestionar adecuadamente las solicitudes web y devolver las respuestas requeridas.

      A continuación, añada esta aplicación a la lista de aplicaciones instaladas en el archivo settings.py de su proyecto para que Django la reconozca como parte del proyecto. Abra settings.py de nuevo:

      • nano ~/djangoreactproject/djangoreactproject/settings.py

      Añada la aplicación customers:

      ~/djangoreactproject/djangoreactproject/settings.py

      ...
      INSTALLED_APPS = [
          ...
          'rest_framework',
          'corsheaders',
          'customers'
      ]
      ...
      

      A continuación, *migre *la base de datos e inicie el servidor de desarrollo local. Las migraciones son la manera en que Django propaga los cambios que usted realiza a sus modelos en su esquema de base de datos. Estos cambios pueden ser, por ejemplo, añadir un campo o eliminar un modelo. Para obtener más información sobre modelos y migraciones, consulte Cómo crear modelos de Django.

      Migre la base de datos:

      Inicie el servidor de desarrollo local:

      • python manage.py runserver

      El resultado debe ser similar a lo siguiente:

      Output

      Performing system checks... System check identified no issues (0 silenced). October 22, 2018 - 15:14:50 Django version 2.1.2, using settings 'djangoreactproject.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

      Su aplicación web se ejecuta desde http://127.0.0.1:8000. Si se dirige a esta dirección en su navegador web, debería ver la siguiente página:

      Página de demostración de Django

      En este momento, deje la aplicación en ejecución y abra una nueva terminal para seguir desarrollando el proyecto.

      Paso 3: creación del frontend de React

      En esta sección, vamos a crear la aplicación frontend de nuestro proyecto utilizando React.

      React tiene una herramienta oficial que le permite generar proyectos de React de forma rápida sin tener que configurar Webpack directamente. Webpack es un empaquetador de módulos que se utiliza para agrupar recursos web, como código de JavaScript, CSS e imágenes. Normalmente, para poder utilizar Webpack, debe establecer varias opciones de configuración, pero, gracias a la herramienta create-react-app, no tiene que lidiar con Webpack directamente hasta que decida que necesita más control. Para ejecutar create-react-app puede utilizar npx, una herramienta que ejecuta binarios de paquetes npm.

      En su segunda terminal, asegúrese de que estar en el directorio de su proyecto:

      Cree un proyecto de React llamado frontend utilizando create-react-app y npx

      • npx create-react-app frontend

      A continuación, navegue al interior de su aplicación de React e inicie el servidor de desarrollo:

      • cd ~/djangoreactproject/frontend
      • npm start

      Su aplicación se ejecutará desde http://localhost:3000/:

      Página de demostración de React

      Deje el servidor de desarrollo de React en ejecución y abra otra ventana de terminal para proceder.

      Para ver la estructura del directorio de todo el proyecto en este punto, navegue a la carpeta root y ejecute el comando tree de nuevo:

      • cd ~/djangoreactproject
      • tree

      Verá una estructura como esta:

      Output

      ├── customers │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── djangoreactproject │ ├── __init__.py │ ├── __pycache__ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── frontend │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── README.md │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ └── registerServiceWorker.js │ └── yarn.lock └── manage.py

      Nuestra aplicación utilizará Bootstrap 4 para dar forma a la interfaz de React, por lo que lo incluiremos en el archivo frontend/src/App.css que gestiona nuestros ajustes de CSS. Abra el archivo:

      • nano ~/djangoreactproject/frontend/src/App.css

      Añada la importación que se indica a continuación al comienzo del archivo. Puede eliminar el contenido existente del archivo, pero no es necesario hacerlo:

      ~/djangoreactproject/frontend/src/App.css

      @import  'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css';
      

      Aquí, @import es una instrucción de CSS que se utiliza para importar reglas de estilo de otras hojas de estilo.

      Ahora que hemos creado las aplicaciones back-end y front-end, vamos a crear el modelo de cliente y algunos datos de prueba.

      Paso 4: creación del modelo de cliente y datos iniciales

      Tras crear la aplicación de Django y el frontend de React, nuestro próximo paso será crear el modelo de cliente, que representa la tabla de base de datos que almacenará información sobre los clientes. No necesita nada de SQL, dado que el mapeo objeto-relacional (ORM) de Django se encargará de las operaciones de la base de datos al asignar las clases y variables de Python a tablas y columnas de SQL. De esta manera, el ORM de Django extrae las interacciones de SQL con la base de datos a través de una interfaz de Python.

      Active su entorno virtual de nuevo:

      • cd ~
      • source env/bin/activate

      Diríjase al directorio customers y abra models.py, un archivo de Python que contiene los modelos de su aplicación:

      • cd ~/djangoreactproject/customers/
      • nano models.py

      El archivo incluirá el siguiente contenido:

      ~/djangoreactproject/customers/models.py

      from django.db import models
      # Create your models here.
      

      La API del modelo de cliente ya está importada en el archivo gracias a la instrucción import from django.db import models. Ahora, añadirá la clase Customer, que extiende models.Model. Cada modelo de Django es una clase de Python que extiende django.db.models.Model.

      El modelo Customer tendrá estos campos de base de datos:

      • first_name: el nombre del cliente.
      • last_name: el apellido del cliente.
      • email: la dirección de correo electrónico del cliente.
      • phone: el número de teléfono del cliente.
      • address: la dirección del cliente.
      • description: la descripción del cliente.
      • createdAt: la fecha en que se añade el cliente.

      También añadiremos la función __str__(), que define la manera en que se mostrará el modelo. En nuestro caso, será con el nombre del cliente. Para obtener más información sobre la creación de clases y la definición de objetos, consulte Cómo crear clases y definir objetos en Python 3.

      Añada el código siguiente al archivo:

      ~/djangoreactproject/customers/models.py

      from django.db import models
      
      class Customer(models.Model):
          first_name = models.CharField("First name", max_length=255)
          last_name = models.CharField("Last name", max_length=255)
          email = models.EmailField()
          phone = models.CharField(max_length=20)
          address =  models.TextField(blank=True, null=True)
          description = models.TextField(blank=True, null=True)
          createdAt = models.DateTimeField("Created At", auto_now_add=True)
      
          def __str__(self):
              return self.first_name
      

      A continuación, migre la base de datos para crear las tablas de la base de datos. El comando makemigrations crea los archivos de migración en los que se añadirán los cambios al modelo, y migrate aplica los cambios en los archivos de migraciones a la base de datos.

      Vuelva a navegar a la carpeta root del proyecto:

      Ejecute lo siguiente para crear los archivos de migración:

      • python manage.py makemigrations

      Verá algo similar a esto:

      Output

      customers/migrations/0001_initial.py - Create model Customer

      Aplique estos cambios en la base de datos:

      Verá un resultado que indica que la migración se realizó correctamente:

      Output

      Operations to perform: Apply all migrations: admin, auth, contenttypes, customers, sessions Running migrations: Applying customers.0001_initial... OK

      A continuación, utilizará un archivo de migración de datos para crear datos iniciales de clientes. Un archivo de migración de datos es una migración que añade o altera datos en la base de datos. Cree un archivo de migración de datos vacío para la aplicación customers:

      • python manage.py makemigrations --empty --name customers customers

      Visualizará la siguiente confirmación con el nombre de su archivo de migración:

      Output

      Migrations for 'customers': customers/migrations/0002_customers.py

      Tenga en cuenta que el nombre de su archivo de migración es 0002_customers.py.

      A continuación, navegue al interior de la carpeta de migraciones de la aplicación customers:

      • cd ~/djangoreactproject/customers/migrations

      Abra el archivo de migración creado:

      Este es el contenido inicial del archivo:

      ~/djangoreactproject/customers/migrations/0002_customers.py

      from django.db import migrations
      
      class Migration(migrations.Migration):
          dependencies = [
              ('customers', '0001_initial'),
          ]
          operations = [
          ]        
      

      La instrucción import importa la API de migraciones, una API de Django para crear migraciones, desde django.db, un paquete incorporado que contiene clases para trabajar con bases de datos.

      La clase Migration es una clase de Python que describe las operaciones que se ejecutan al migrar bases de datos. Esta clase extiende migrations.Migration y tiene dos listas:

      • dependencies: contiene las migraciones dependientes.
      • operations: contiene las operaciones que se ejecutarán al aplicar la migración.

      A continuación, añada un método para crear datos de clientes de prueba. Añada el método que se indica a continuación antes de la definición de la clase Migration:

      ~/djangoreactproject/customers/migrations/0002_customers.py

      ...
      def create_data(apps, schema_editor):
          Customer = apps.get_model('customers', 'Customer')
          Customer(first_name="Customer 001", last_name="Customer 001", email="customer001@email.com", phone="00000000", address="Customer 000 Address", description= "Customer 001 description").save()
      
      ...
      

      En este método, estamos tomando la clase Customer de nuestra aplicación customers y creando un cliente de prueba para insertar en la base de datos.

      Para obtener la clase Customer, que permitirá la creación de nuevos clientes, usamos el método get_model() del objeto apps. El objeto apps representa el registro de aplicaciones instaladas y sus modelos de base de datos.

      El objeto apps se pasará del método RunPython() cuando lo usemos para ejecutar create_data(). Añada el método migrations.RunPython() a la lista operations vacía:

      ~/djangoreactproject/customers/migrations/0002_customers.py

      
      ...
          operations = [
              migrations.RunPython(create_data),
          ]  
      

      RunPython() es parte de la API de Migrations que le permite ejecutar código de Python personalizado en una migración. Nuestra lista operations especifica que este método se ejecutará al aplicar la migración.

      Este es el archivo completo:

      ~/djangoreactproject/customers/migrations/0002_customers.py

      from django.db import migrations
      
      def create_data(apps, schema_editor):
          Customer = apps.get_model('customers', 'Customer')
          Customer(first_name="Customer 001", last_name="Customer 001", email="customer001@email.com", phone="00000000", address="Customer 000 Address", description= "Customer 001 description").save()
      
      class Migration(migrations.Migration):
          dependencies = [
              ('customers', '0001_initial'),
          ]
          operations = [
              migrations.RunPython(create_data),
          ]        
      

      Para obtener más información sobre migraciones de datos, consulte la documentación sobre migraciones de datos en Django

      Para migrar su base de datos, primero, vuelva a navegar a la carpeta root de su proyecto:

      Migre su base de datos para crear los datos de prueba:

      Visualizará un resultado que confirma la migración:

      Output

      Operations to perform: Apply all migrations: admin, auth, contenttypes, customers, sessions Running migrations: Applying customers.0002_customers... OK

      Para obtener más detalles sobre este proceso, consulte Cómo crear modelos de Django.

      Con el modelo Customer y los datos de prueba creados, podemos pasar a la creación de la API REST.

      Paso 5: creación de la API REST

      En este paso, vamos a crear la API REST utilizando Django REST Framework. Crearemos varias vistas de API diferentes. Una vista de API es una función que gestiona una solicitud o llamada de API, mientras que un punto de final de API es una URL única que representa un punto de contacto con el sistema REST. Por ejemplo, cuando el usuario envía una solicitud de GET a un punto final de API, Django llama a la función o vista de API correspondiente para gestionar la solicitud y devolver los resultados posibles.

      También utilizaremos serializadores. Un serializador de Django REST Framework permite que instancias de modelos complejas y QuerySets se conviertan en formato JSON para el consumo de API. La clase de serialización también puede funcionar en dirección inversa, proporcionando mecanismos para el análisis y la deserialización de datos en QuerySets y modelos de Django.

      Nuestros puntos finales de API incluirán lo siguiente:

      • api/customers: este punto final se utiliza para crear clientes y devuelve conjuntos paginados de clientes.
      • api/clusters/<pk>: este punto final se utiliza para obtener, actualizar y eliminar clientes individuales por clave primaria o id.

      También crearemos URL en el archivo urls.py del proyecto para los puntos finales correspondientes (es decir, api/clusters y api/clusters/<pk>).

      Comencemos por crear la clase de serialización para nuestro modelo Customer.

      Incorporación de la clase de serialización

      Es necesario crear una clase de serialización para nuestro modelo Customer para transformar las instancias de cliente y QuerySets hacia y desde JSON. Para crear la clase de serialización, primero, cree un archivo serializer.py dentro de la aplicación customers:

      • cd ~/djangoreactproject/customers/
      • nano serializers.py

      Añada el código siguiente para importar el modelo Customer y la API de serializadores:

      ~/djangoreactproject/customers/serializers.py

      from rest_framework import serializers
      from .models import Customer
      

      A continuación, cree una clase de serialización que extienda serializers.ModelSerializer y especifique los campos que han de serializarse:

      ~/djangoreactproject/customers/serializers.py

      
      ...
      class CustomerSerializer(serializers.ModelSerializer):
      
          class Meta:
              model = Customer 
              fields = ('pk','first_name', 'last_name', 'email', 'phone','address','description')
      

      La clase Meta especifica qué campos y modelos se serializarán: pk, first_name, last_name, email, phone, address, description.

      Este es el contenido completo del archivo:

      ~/djangoreactproject/customers/serializers.py

      from rest_framework import serializers
      from .models import Customer
      
      class CustomerSerializer(serializers.ModelSerializer):
      
          class Meta:
              model = Customer 
              fields = ('pk','first_name', 'last_name', 'email', 'phone','address','description')
      

      Ahora que hemos creado nuestra clase de serialización, podemos añadir las vistas de API.

      Incorporación de vistas de API

      En esta sección, crearemos las vistas de API para nuestra aplicación, a las que llamará Django cuando el usuario visite el punto final correspondiente a la función de vista.

      Abra ~/djangoreactproject/customers/views.py:

      • nano ~/djangoreactproject/customers/views.py

      Elimine lo que haya allí y añada las siguientes importaciones:

      ~/djangoreactproject/customers/views.py

      from rest_framework.response import Response
      from rest_framework.decorators import api_view
      from rest_framework import status
      
      from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
      from .models import Customer 
      from .serializers import *
      

      Estamos importando el serializador que creamos, junto con el modelo Customer y las API de Django y Django REST Framework.

      A continuación, añada la vista para el procesamiento de POST y las solicitudes GET HTTP:

      ~/djangoreactproject/customers/views.py

      ...
      
      @api_view(['GET', 'POST'])
      def customers_list(request):
          """
       List  customers, or create a new customer.
       """
          if request.method == 'GET':
              data = []
              nextPage = 1
              previousPage = 1
              customers = Customer.objects.all()
              page = request.GET.get('page', 1)
              paginator = Paginator(customers, 10)
              try:
                  data = paginator.page(page)
              except PageNotAnInteger:
                  data = paginator.page(1)
              except EmptyPage:
                  data = paginator.page(paginator.num_pages)
      
              serializer = CustomerSerializer(data,context={'request': request} ,many=True)
              if data.has_next():
                  nextPage = data.next_page_number()
              if data.has_previous():
                  previousPage = data.previous_page_number()
      
              return Response({'data': serializer.data , 'count': paginator.count, 'numpages' : paginator.num_pages, 'nextlink': '/api/customers/?page=' + str(nextPage), 'prevlink': '/api/customers/?page=' + str(previousPage)})
      
          elif request.method == 'POST':
              serializer = CustomerSerializer(data=request.data)
              if serializer.is_valid():
                  serializer.save()
                  return Response(serializer.data, status=status.HTTP_201_CREATED)
              return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      

      Primero, usamos el decorador @api_view(['GET', 'POST']) para crear una vista de API que pueda aceptar solicitudes GET y POST. Un decorador es una función que toma otra función y la extiende de forma dinámica.

      En el cuerpo del método, usamos la variable request.method para verificar el método HTTP actual y ejecutar la lógica correspondiente dependiendo del tipo de solicitud:

      • Si se trata de una solicitud GET, el método pagina los datos utilizando Django Paginator y devuelve a la primera página de datos después de la serialización, el conteo de clientes disponibles, el número de páginas disponibles y los enlaces a las páginas anteriores y siguientes. Paginator es una clase integrada de Django que pagina una lista de datos en páginas y proporciona métodos para acceder a los elementos de cada una de ellas.
      • Si se trata de una solicitud POST, el método serializa los datos de clientes recibidos y, luego, llama al método save() del objeto serializador. A continuación, devuelve un objeto de Response, una instancia de HttpResponse, con el código de estado 201. Cada vista que crea se encarga de regresar un objeto HttpResponse. El método save() guarda los datos serializados en la base de datos.

      Para obtener más información sobre HttpResponse y las vistas, consulte esta discusión con respecto a la creación de funciones de vista.

      Ahora, añada la vista de API que se encargará del procesamiento de las solicitudes GET, PUT y DELETE para obtener, actualizar y eliminar clientes por pk (clave primaria):

      ~/djangoreactproject/customers/views.py

      
      ...
      @api_view(['GET', 'PUT', 'DELETE'])
      def customers_detail(request, pk):
       """
       Retrieve, update or delete a customer by id/pk.
       """
          try:
              customer = Customer.objects.get(pk=pk)
          except Customer.DoesNotExist:
              return Response(status=status.HTTP_404_NOT_FOUND)
      
          if request.method == 'GET':
              serializer = CustomerSerializer(customer,context={'request': request})
              return Response(serializer.data)
      
          elif request.method == 'PUT':
              serializer = CustomerSerializer(customer, data=request.data,context={'request': request})
              if serializer.is_valid():
                  serializer.save()
                  return Response(serializer.data)
              return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
          elif request.method == 'DELETE':
              customer.delete()
              return Response(status=status.HTTP_204_NO_CONTENT)
      

      El método está representado con @api_view(['GET', 'PUT', 'DELETE']) para indicar que se trata de una vista de API que puede aceptar las solicitudes GET, PUT y DELETE.

      La marca de revisión en el campo request.method verifica el método de solicitud y, dependiendo de su valor, llama a la lógica correcta:

      • Si se trata de una solicitud GET, se serializan los datos de clientes y se envían utilizando un objeto Response.
      • Si se trata de una solicitud PUT, el método crea un serializador para nuevos datos de cliente y, luego, llama al método save() del objeto serializador creado. Por último, envía un objeto Response con el cliente actualizado.
      • Si se trata de una solicitud DELETE, el método llama al método delete() del objeto de cliente para eliminarlo y, a continuación, devuelve un objeto Response sin datos.

      El archivo completo se ve de este modo:

      ~/djangoreactproject/customers/views.py

      from rest_framework.response import Response
      from rest_framework.decorators import api_view
      from rest_framework import status
      
      from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
      from .models import Customer 
      from .serializers import *
      
      
      @api_view(['GET', 'POST'])
      def customers_list(request):
          """
       List  customers, or create a new customer.
       """
          if request.method == 'GET':
              data = []
              nextPage = 1
              previousPage = 1
              customers = Customer.objects.all()
              page = request.GET.get('page', 1)
              paginator = Paginator(customers, 5)
              try:
                  data = paginator.page(page)
              except PageNotAnInteger:
                  data = paginator.page(1)
              except EmptyPage:
                  data = paginator.page(paginator.num_pages)
      
              serializer = CustomerSerializer(data,context={'request': request} ,many=True)
              if data.has_next():
                  nextPage = data.next_page_number()
              if data.has_previous():
                  previousPage = data.previous_page_number()
      
              return Response({'data': serializer.data , 'count': paginator.count, 'numpages' : paginator.num_pages, 'nextlink': '/api/customers/?page=' + str(nextPage), 'prevlink': '/api/customers/?page=' + str(previousPage)})
      
          elif request.method == 'POST':
              serializer = CustomerSerializer(data=request.data)
              if serializer.is_valid():
                  serializer.save()
                  return Response(serializer.data, status=status.HTTP_201_CREATED)
              return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
      @api_view(['GET', 'PUT', 'DELETE'])
      def customers_detail(request, pk):
          """
       Retrieve, update or delete a customer by id/pk.
       """
          try:
              customer = Customer.objects.get(pk=pk)
          except Customer.DoesNotExist:
              return Response(status=status.HTTP_404_NOT_FOUND)
      
          if request.method == 'GET':
              serializer = CustomerSerializer(customer,context={'request': request})
              return Response(serializer.data)
      
          elif request.method == 'PUT':
              serializer = CustomerSerializer(customer, data=request.data,context={'request': request})
              if serializer.is_valid():
                  serializer.save()
                  return Response(serializer.data)
              return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
          elif request.method == 'DELETE':
              customer.delete()
              return Response(status=status.HTTP_204_NO_CONTENT)
      

      Ahora, podemos pasar a crear nuestros puntos finales.

      Incorporación de puntos finales de API

      Ahora, crearemos los puntos finales de API: api/customers/ para consultar y crear clientes, y api/customers/ <pk> para obtener, actualizar o eliminar clientes individuales por su pk.

      Abra ~/djangoreactproject/djangoreactproject/urls.py:

      • nano ~/djangoreactproject/djangoreactproject/urls.py

      Deje lo que haya allí, pero añada la importación a las vistas customers en la parte superior del archivo:

      ~/djangoreactproject/djangoreactproject/urls.py

      from django.contrib import admin
      from django.urls import path
      from customers import views
      from django.conf.urls import url
      

      A continuación, añada las URL de api/customers/ y api/customers/<pk> a la lista urlpatterns que contiene las URL de la aplicación:

      ~/djangoreactproject/djangoreactproject/urls.py

      ...
      
      urlpatterns = [
          path('admin/', admin.site.urls),
          url(r'^api/customers/$', views.customers_list),
          url(r'^api/customers/(?P<pk>[0-9]+)$', views.customers_detail),
      ]
      

      Con nuestros puntos finales REST creados, veamos cómo podemos consumirlos.

      Paso 6: consumo de API REST con Axios.

      En este paso, instalaremos Axios, el cliente HTTP que utilizaremos para realizar llamadas de API. También crearemos una clase para consumir los puntos finales de API que hemos creado.

      Primero, desactive su entorno virtual:

      A continuación, navegue a su carpeta frontend:

      • cd ~/djangoreactproject/frontend

      Instale axios desde npm utilizando:

      La opción --save añade la dependencia axios al archivo package.json de su aplicación.

      A continuación, cree un archivo de JavaScript denominado CustomersService.js, que contendrá el código para llamar a las API REST. Realizaremos esto dentro de la carpeta src, donde residirá el código de la aplicación de nuestro proyecto:

      • cd src
      • nano CustomersService.js

      Añada el código siguiente, que contiene métodos para conectarse a la API REST de Django:

      ~/djangoreactproject/frontend/src/CustomersService.js

      import axios from 'axios';
      const API_URL = 'http://localhost:8000';
      
      export default class CustomersService{
      
          constructor(){}
      
      
          getCustomers() {
              const url = `${API_URL}/api/customers/`;
              return axios.get(url).then(response => response.data);
          }  
          getCustomersByURL(link){
              const url = `${API_URL}${link}`;
              return axios.get(url).then(response => response.data);
          }
          getCustomer(pk) {
              const url = `${API_URL}/api/customers/${pk}`;
              return axios.get(url).then(response => response.data);
          }
          deleteCustomer(customer){
              const url = `${API_URL}/api/customers/${customer.pk}`;
              return axios.delete(url);
          }
          createCustomer(customer){
              const url = `${API_URL}/api/customers/`;
              return axios.post(url,customer);
          }
          updateCustomer(customer){
              const url = `${API_URL}/api/customers/${customer.pk}`;
              return axios.put(url,customer);
          }
      }
      

      La clase CustomersService llamará a los siguientes métodos de Axios:

      • getCustomers(): obtiene la primera página de clientes.
      • getCustomersByURL(): obtiene clientes por URL. Esto permite obtener las siguientes páginas de clientes al pasar enlaces como /api/customers/?page=2.
      • get Customer(): obtiene un cliente por clave primaria.
      • createCustomer(): crea un cliente.
      • updateCustomer(): actualiza un cliente.
      • deleteCustomer(): elimina un cliente.

      Ahora, podemos mostrar los datos de nuestra API en nuestra IU de React al crear un componente CustomersList.

      Paso 7: visualización de datos de la API en la aplicación de React

      En este paso, crearemos el componente CustomersList de React. Un componente de React representa una parte de la IU; también le permite dividir la IU en piezas independientes y reutilizables.

      Comience por crear CustomersList.js en frontend/src:

      • nano ~/djangoreactproject/frontend/src/CustomersList.js

      Inicie la importación de React y Component para crear un componente de React:

      ~/djangoreactproject/frontend/src/CustomersList.js

      import  React, { Component } from  'react';
      

      A continuación, importe e instancie el módulo CustomersService que creó en el paso anterior, el cual proporciona métodos que interactúan con el backend de API REST:

      ~/djangoreactproject/frontend/src/CustomersList.js

      
      ...
      import  CustomersService  from  './CustomersService';
      
      const  customersService  =  new  CustomersService();
      

      A continuación, cree un componente CustomersList que extienda Component para llamar a la API REST. Un componente de React debería extender o crear una subclase de la clase Component. Para obtener más información acerca de las clases E6 y herencia, consulte nuestro tutorial sobre Comprensión de las clases de JavaScript.

      Añada el siguiente código para crear un componente de React que extienda react.Component:

      ~/djangoreactproject/frontend/src/CustomersList.js

      
      ...
      class  CustomersList  extends  Component {
      
          constructor(props) {
              super(props);
              this.state  = {
                  customers: [],
                  nextPageURL:  ''
              };
              this.nextPage  =  this.nextPage.bind(this);
              this.handleDelete  =  this.handleDelete.bind(this);
          }
      }
      export  default  CustomersList;
      

      Dentro del constructor, estamos inicializando el objeto state. Este almacena las variables de estado de nuestro componente utilizando una matriz customers vacía. Esta matriz almacenará clientes y una nextPageURL que contendrá la URL de la siguiente página que se obtendrá del back-end de la API. También procedemos a vincular los métodos nextPage() y handleDelete() a this para que sean accesibles desde el código HTML.

      A continuación, añada el método componentDidMount() y una llamada a getCustomers() dentro de la clase CustomersList, antes de la llave de cierre.

      El método componentDidMount() es un método de ciclo de vida del componente que se llama al crear el componente y se inserta en el DOM. getCustomers() llama al objeto Customers Service para obtener la primera página de datos y el enlace de la siguiente página del backend de Django:

      ~/djangoreactproject/frontend/src/CustomersList.js

      
      ...
      componentDidMount() {
          var  self  =  this;
          customersService.getCustomers().then(function (result) {
              self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
          });
      }
      

      Ahora, añada el método handleDelete(), que gestiona la eliminación de un cliente, debajo de componentDidMount():

      ~/djangoreactproject/frontend/src/CustomersList.js

      
      ...
      handleDelete(e,pk){
          var  self  =  this;
          customersService.deleteCustomer({pk :  pk}).then(()=>{
              var  newArr  =  self.state.customers.filter(function(obj) {
                  return  obj.pk  !==  pk;
              });
              self.setState({customers:  newArr})
          });
      }
      

      El método handleDelete() llama al método deleteCustomer() para eliminar un cliente utilizando su pk (clave primaria). Si la operación se realiza correctamente, la matriz customers se filtra por el cliente eliminado.

      A continuación, añada un método nextPage() para obtener los datos de la siguiente página y actualice el enlace de la página siguiente:

      ~/djangoreactproject/frontend/src/CustomersList.js

      
      ...
      nextPage(){
          var  self  =  this;
          customersService.getCustomersByURL(this.state.nextPageURL).then((result) => {
              self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
          });
      }
      

      El método nextPage() llama a un método getCustomersByURL(), que obtiene la URL de la página siguiente del objeto de estado, this.state.nextPageURL, y actualiza la matriz customers con los datos devueltos.

      Por último, añada el método render() del componente, que produce una tabla de clientes del estado de componente:

      ~/djangoreactproject/frontend/src/CustomersList.js

      
      ...
      render() {
      
          return (
          <div  className="customers--list">
              <table  className="table">
                  <thead  key="thead">
                  <tr>
                      <th>#</th>
                      <th>First Name</th>
                      <th>Last Name</th>
                      <th>Phone</th>
                      <th>Email</th>
                      <th>Address</th>
                      <th>Description</th>
                      <th>Actions</th>
                  </tr>
                  </thead>
                  <tbody>
                      {this.state.customers.map( c  =>
                      <tr  key={c.pk}>
                          <td>{c.pk}  </td>
                          <td>{c.first_name}</td>
                          <td>{c.last_name}</td>
                          <td>{c.phone}</td>
                          <td>{c.email}</td>
                          <td>{c.address}</td>
                          <td>{c.description}</td>
                          <td>
                          <button  onClick={(e)=>  this.handleDelete(e,c.pk) }> Delete</button>
                          <a  href={"/customer/" + c.pk}> Update</a>
                          </td>
                      </tr>)}
                  </tbody>
              </table>
              <button  className="btn btn-primary"  onClick=  {  this.nextPage  }>Next</button>
          </div>
          );
      }
      

      Este es el contenido completo del archivo:

      ~/djangoreactproject/frontend/src/CustomersList.js

      import  React, { Component } from  'react';
      import  CustomersService  from  './CustomersService';
      
      const  customersService  =  new  CustomersService();
      
      class  CustomersList  extends  Component {
      
      constructor(props) {
          super(props);
          this.state  = {
              customers: [],
              nextPageURL:  ''
          };
          this.nextPage  =  this.nextPage.bind(this);
          this.handleDelete  =  this.handleDelete.bind(this);
      }
      
      componentDidMount() {
          var  self  =  this;
          customersService.getCustomers().then(function (result) {
              console.log(result);
              self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
          });
      }
      handleDelete(e,pk){
          var  self  =  this;
          customersService.deleteCustomer({pk :  pk}).then(()=>{
              var  newArr  =  self.state.customers.filter(function(obj) {
                  return  obj.pk  !==  pk;
              });
      
              self.setState({customers:  newArr})
          });
      }
      
      nextPage(){
          var  self  =  this;
          console.log(this.state.nextPageURL);        
          customersService.getCustomersByURL(this.state.nextPageURL).then((result) => {
              self.setState({ customers:  result.data, nextPageURL:  result.nextlink})
          });
      }
      render() {
      
          return (
              <div  className="customers--list">
                  <table  className="table">
                  <thead  key="thead">
                  <tr>
                      <th>#</th>
                      <th>First Name</th>
                      <th>Last Name</th>
                      <th>Phone</th>
                      <th>Email</th>
                      <th>Address</th>
                      <th>Description</th>
                      <th>Actions</th>
                  </tr>
                  </thead>
                  <tbody>
                  {this.state.customers.map( c  =>
                      <tr  key={c.pk}>
                      <td>{c.pk}  </td>
                      <td>{c.first_name}</td>
                      <td>{c.last_name}</td>
                      <td>{c.phone}</td>
                      <td>{c.email}</td>
                      <td>{c.address}</td>
                      <td>{c.description}</td>
                      <td>
                      <button  onClick={(e)=>  this.handleDelete(e,c.pk) }> Delete</button>
                      <a  href={"/customer/" + c.pk}> Update</a>
                      </td>
                  </tr>)}
                  </tbody>
                  </table>
                  <button  className="btn btn-primary"  onClick=  {  this.nextPage  }>Next</button>
              </div>
              );
        }
      }
      export  default  CustomersList;
      

      Ahora que hemos creado el componente CustomersList para visualizar la lista de clientes, podemos añadir el componente que gestiona la creación y la actualización de clientes.

      Paso 8: incorporación del componente de React de creación y actualización de clientes

      En este paso, crearemos el componente CustomerCreateUpdate, que se encargará de crear y actualizar clientes. Lo hará al proporcionar una forma que los usuarios puedan utilizar para ingresar datos sobre un cliente nuevo o actualizar una entrada existente.

      En frontend/src, cree un archivo CustomerCreateUpdate.js:

      • nano ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      Añada el siguiente código para crear un componente de React, importando React y Component:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      import  React, { Component } from  'react';
      

      A continuación, también podemos importar e instanciar la clase CustomersService que creamos en el paso anterior, que proporciona métodos que interactúan con el backend de API REST:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      ...
      import  CustomersService  from  './CustomersService';
      
      const  customersService  =  new  CustomersService();
      

      Luego, cree un componente CustomerCreateUpdate que extienda Component para crear y actualizar clientes:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      
      ...
      class  CustomerCreateUpdate  extends  Component {
      
          constructor(props) {
              super(props);
          }
      
      }
      export default CustomerCreateUpdate;
      

      Dentro de la definición de clase, añada el método render() del componente, que produce un formulario HTML que toma información sobre el cliente:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      
      ...
      render() {
              return (
                <form onSubmit={this.handleSubmit}>
                <div className="form-group">
                  <label>
                    First Name:</label>
                    <input className="form-control" type="text" ref='firstName' />
      
                  <label>
                    Last Name:</label>
                    <input className="form-control" type="text" ref='lastName'/>
      
                  <label>
                    Phone:</label>
                    <input className="form-control" type="text" ref='phone' />
      
                  <label>
                    Email:</label>
                    <input className="form-control" type="text" ref='email' />
      
                  <label>
                    Address:</label>
                    <input className="form-control" type="text" ref='address' />
      
                  <label>
                    Description:</label>
                    <textarea className="form-control" ref='description' ></textarea>
      
      
                  <input className="btn btn-primary" type="submit" value="Submit" />
                  </div>
                </form>
              );
        }
      

      Para cada elemento de entrada del formulario, el método añade una propiedad ref para acceder al valor del elemento del formulario y establecerlo.

      A continuación, por encima del método render(), defina un método handleSubmit(event) de modo que tenga la funcionalidad correcta cuando un usuario haga clic en el botón de enviar:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      
      ...
      handleSubmit(event) {
          const { match: { params } } =  this.props;
          if(params  &&  params.pk){
              this.handleUpdate(params.pk);
          }
          else
          {
              this.handleCreate();
          }
          event.preventDefault();
      }
      
      ...
      

      El método handleSubmit(event) gestiona el envío del formulario y, dependiendo de la ruta, llama al método handleUpdate(pk) para actualizar el cliente con la pk correcta, o el método handleCreate() para crear un nuevo cliente. Procederemos a definir estos métodos en breve.

      De nuevo en el constructor del componente, vincule el método handleSubmit() recientemente añadido a this para poder acceder a él en su formulario:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      ...
      class CustomerCreateUpdate extends Component {
      
      constructor(props) {
          super(props);
          this.handleSubmit = this.handleSubmit.bind(this);
      }
      ...
      

      A continuación, defina el método handleCreate() para crear un cliente a partir de los datos del formulario. Encima del método handleSubmit(event), añada el siguiente código:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      
      ...
      handleCreate(){
          customersService.createCustomer(
              {
              "first_name":  this.refs.firstName.value,
              "last_name":  this.refs.lastName.value,
              "email":  this.refs.email.value,
              "phone":  this.refs.phone.value,
              "address":  this.refs.address.value,
              "description":  this.refs.description.value
              }).then((result)=>{
                      alert("Customer created!");
              }).catch(()=>{
                      alert('There was an error! Please re-check your form.');
              });
      }
      
      ...
      

      El método handleCreate() se utilizará para crear un cliente a partir de los datos ingresados. Llama al método CustomersService.createCustomer() correspondiente que provoca que la API real llame al backend para crear un cliente.

      A continuación, por debajo del método handleCreate(), defina el método handleUpdate(pk) para implementar actualizaciones:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      
      ...
      handleUpdate(pk){
      customersService.updateCustomer(
          {
          "pk":  pk,
          "first_name":  this.refs.firstName.value,
          "last_name":  this.refs.lastName.value,
          "email":  this.refs.email.value,
          "phone":  this.refs.phone.value,
          "address":  this.refs.address.value,
          "description":  this.refs.description.value
          }
          ).then((result)=>{
      
              alert("Customer updated!");
          }).catch(()=>{
              alert('There was an error! Please re-check your form.');
          });
      }
      

      El método updateCustomer() actualizará un cliente por pk, utilizando la nueva información del formulario de información de clientes. Llama al método customersService.updateCustomer().

      A continuación, añada un método componentDidMount(). Si el usuario visita una ruta customer/:pk, queremos que el formulario se complete con información relacionada con el cliente utilizando la clave primaria de la URL. Para ello, podemos añadir el método getCustomer(pk) una vez que el componente se monte en el evento de ciclo de vida de componentDidMount(). Añada el siguiente código por debajo del constructor del componente para añadir este método:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      
      ...
      componentDidMount(){
          const { match: { params } } =  this.props;
          if(params  &&  params.pk)
          {
              customersService.getCustomer(params.pk).then((c)=>{
                  this.refs.firstName.value  =  c.first_name;
                  this.refs.lastName.value  =  c.last_name;
                  this.refs.email.value  =  c.email;
                  this.refs.phone.value  =  c.phone;
                  this.refs.address.value  =  c.address;
                  this.refs.description.value  =  c.description;
              })
          }
      }
      

      Este es el contenido completo del archivo:

      ~/djangoreactproject/frontend/src/CustomerCreateUpdate.js

      import React, { Component } from 'react';
      import CustomersService from './CustomersService';
      
      const customersService = new CustomersService();
      
      class CustomerCreateUpdate extends Component {
          constructor(props) {
              super(props);
      
              this.handleSubmit = this.handleSubmit.bind(this);
            }
      
            componentDidMount(){
              const { match: { params } } = this.props;
              if(params && params.pk)
              {
                customersService.getCustomer(params.pk).then((c)=>{
                  this.refs.firstName.value = c.first_name;
                  this.refs.lastName.value = c.last_name;
                  this.refs.email.value = c.email;
                  this.refs.phone.value = c.phone;
                  this.refs.address.value = c.address;
                  this.refs.description.value = c.description;
                })
              }
            }
      
            handleCreate(){
              customersService.createCustomer(
                {
                  "first_name": this.refs.firstName.value,
                  "last_name": this.refs.lastName.value,
                  "email": this.refs.email.value,
                  "phone": this.refs.phone.value,
                  "address": this.refs.address.value,
                  "description": this.refs.description.value
              }          
              ).then((result)=>{
                alert("Customer created!");
              }).catch(()=>{
                alert('There was an error! Please re-check your form.');
              });
            }
            handleUpdate(pk){
              customersService.updateCustomer(
                {
                  "pk": pk,
                  "first_name": this.refs.firstName.value,
                  "last_name": this.refs.lastName.value,
                  "email": this.refs.email.value,
                  "phone": this.refs.phone.value,
                  "address": this.refs.address.value,
                  "description": this.refs.description.value
              }          
              ).then((result)=>{
                console.log(result);
                alert("Customer updated!");
              }).catch(()=>{
                alert('There was an error! Please re-check your form.');
              });
            }
            handleSubmit(event) {
              const { match: { params } } = this.props;
      
              if(params && params.pk){
                this.handleUpdate(params.pk);
              }
              else
              {
                this.handleCreate();
              }
      
              event.preventDefault();
            }
      
            render() {
              return (
                <form onSubmit={this.handleSubmit}>
                <div className="form-group">
                  <label>
                    First Name:</label>
                    <input className="form-control" type="text" ref='firstName' />
      
                  <label>
                    Last Name:</label>
                    <input className="form-control" type="text" ref='lastName'/>
      
                  <label>
                    Phone:</label>
                    <input className="form-control" type="text" ref='phone' />
      
                  <label>
                    Email:</label>
                    <input className="form-control" type="text" ref='email' />
      
                  <label>
                    Address:</label>
                    <input className="form-control" type="text" ref='address' />
      
                  <label>
                    Description:</label>
                    <textarea className="form-control" ref='description' ></textarea>
      
      
                  <input className="btn btn-primary" type="submit" value="Submit" />
                  </div>
                </form>
              );
            }  
      }
      
      export default CustomerCreateUpdate;
      

      Con el componente CustomerCreateUpdate creado, podemos actualizar el componente principal App para añadir enlaces a los diferentes componentes que hemos creado.

      Paso 9: actualización del componente principal App

      En esta sección, actualizaremos el componente App de nuestra aplicación para crear enlaces a los componentes que hemos creado en los pasos anteriores.

      Desde la carpeta frontend, ejecute el siguiente comando para instalar React Router, que le permite añadir enrutamiento y navegación entre varios componentes de React:

      • cd ~/djangoreactproject/frontend
      • npm install --save react-router-dom

      A continuación, abra ~/djangoreactproject/frontend/src/App.js:

      • nano ~/djangoreactproject/frontend/src/App.js

      Elimine todo lo que haya allí y añada el siguiente código para importar las clases necesarias para incorporar enrutamiento. Estas incluyen BrowserRouter, que crea un componente Router, y Route, que crea un componente de ruta:

      ~/djangoreactproject/frontend/src/App.js

      import  React, { Component } from  'react';
      import { BrowserRouter } from  'react-router-dom'
      import { Route, Link } from  'react-router-dom'
      import  CustomersList  from  './CustomersList'
      import  CustomerCreateUpdate  from  './CustomerCreateUpdate'
      import  './App.css';
      

      BrowserRoutermantiene la IU sincronizada con la URL utilizando la ](https://developer.mozilla.org/en-US/docs/Web/API/History_API)API de historial HTML5[.

      A continuación, cree un diseño base que proporcione el componente básico que envolverá el componente BrowserRouter:

      ~/djangoreactproject/frontend/src/App.js

      ...
      
      const  BaseLayout  = () => (
      <div  className="container-fluid">
          <nav  className="navbar navbar-expand-lg navbar-light bg-light">
              <a  className="navbar-brand"  href="https://www.digitalocean.com/#">Django React Demo</a>
              <button  className="navbar-toggler"  type="button"  data-toggle="collapse"  data-target="#navbarNavAltMarkup"  aria-controls="navbarNavAltMarkup"  aria-expanded="false"  aria-label="Toggle navigation">
              <span  className="navbar-toggler-icon"></span>
          </button>
          <div  className="collapse navbar-collapse"  id="navbarNavAltMarkup">
              <div  className="navbar-nav">
                  <a  className="nav-item nav-link"  href="/">CUSTOMERS</a>
                  <a  className="nav-item nav-link"  href="http://www.digitalocean.com/customer">CREATE CUSTOMER</a>
              </div>
          </div>
          </nav>
          <div  className="content">
              <Route  path="/"  exact  component={CustomersList}  />
              <Route  path="/customer/:pk"  component={CustomerCreateUpdate}  />
              <Route  path="/customer/"  exact  component={CustomerCreateUpdate}  />
          </div>
      </div>
      )
      

      Utilizamos el componente Route para definir las rutas de nuestra aplicación; el componente que el enrutador debe cargar cuando se encuentra una coincidencia. Cada ruta requiere un path que especifique la ruta que se debe seguir y un component que indique el componente que se debe cargar. La propiedad exact le indica al enrutador que siga la trayectoria exacta.

      Por último, cree el componente App, el componente root o de nivel superior de nuestra aplicación de React:

      ~/djangoreactproject/frontend/src/App.js

      ...
      
      class  App  extends  Component {
      
      render() {
          return (
          <BrowserRouter>
              <BaseLayout/>
          </BrowserRouter>
          );
      }
      }
      export  default  App;
      

      Hemos envuelto el componente BaseLayout con el componente BrowserRouter, dado que nuestra aplicación está diseñada para ejecutarse en un navegador.

      El archivo completo se ve de este modo:

      ~/djangoreactproject/frontend/src/App.js

      import React, { Component } from 'react';
      import { BrowserRouter } from 'react-router-dom'
      import { Route, Link } from 'react-router-dom'
      
      import  CustomersList from './CustomersList'
      import  CustomerCreateUpdate  from './CustomerCreateUpdate'
      import './App.css';
      
      const BaseLayout = () => (
        <div className="container-fluid">
      <nav className="navbar navbar-expand-lg navbar-light bg-light">
        <a className="navbar-brand" href="https://www.digitalocean.com/#">Django React Demo</a>
        <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
          <span className="navbar-toggler-icon"></span>
        </button>
        <div className="collapse navbar-collapse" id="navbarNavAltMarkup">
          <div className="navbar-nav">
            <a className="nav-item nav-link" href="/">CUSTOMERS</a>
            <a className="nav-item nav-link" href="http://www.digitalocean.com/customer">CREATE CUSTOMER</a>
      
          </div>
        </div>
      </nav>  
      
          <div className="content">
            <Route path="/" exact component={CustomersList} />
            <Route path="/customer/:pk"  component={CustomerCreateUpdate} />
            <Route path="/customer/" exact component={CustomerCreateUpdate} />
      
          </div>
      
        </div>
      )
      
      class App extends Component {
        render() {
          return (
            <BrowserRouter>
              <BaseLayout/>
            </BrowserRouter>
          );
        }
      }
      
      export default App;
      

      Ahora que añadimos enrutamiento a nuestra aplicación, estamos listos para probarla. Navegue a http://localhost:3000. Debería ver la primera página de la aplicación:

      Página de inicio de la aplicación

      Con la implementación de esta aplicación, ahora, cuenta con la base para una aplicación de CRM.

      Conclusión

      En este tutorial, usted creó una aplicación de prueba utilizando Django y React. Utilizó Django REST framework para crear la API REST, Axios para consumir la API y Bootstrap 4 para dar estilo a su CSS. Puede encontrar el código fuente de este proyecto en este repositorio de GitHub.

      En la configuración de este tutorial, se utilizaron aplicaciones de front-end y back-end. Para obtener un enfoque diferente de la integración de React con Django, consulte este tutorial y este otro tutorial.

      Para obtener más información sobre la creación de aplicaciones con Django, puede seguir la serie de desarrollo de Django. También puede consultar la documentación oficial de Django.



      Source link

      How To Embed a React Application in WordPress on Ubuntu 18.04


      The author selected the Electronic Frontier Foundation to receive a donation as part of the Write for DOnations program.

      Introduction

      WordPress is a popular content management system that, according to W3Techs (Web Technology Surveys), powers over 33% of websites on the Internet. One reason it is so popular is that it is easy to set up with clear, straight-forward documentation. In addition, there are a great deal of community resources supporting WordPress developers. WordPress can solve many use-cases with an inexpensive or even free out-of-the-box solution. Finally, WordPress comes with a well-defined plugin system, which allows developers to write custom code to add their own functionality. This plugin system is well-documented, works well, and as you will see later in this tutorial, is easy to use.

      Developers who want to deliver the richest, most interactive experiences can use JavaScript, supported by frameworks such as React. React is a JavaScript library that is designed to make it easy for developers to create dynamic, interactive UIs that go above and beyond a typical static page or form. Created by Facebook, and thus well maintained for security, stability, and ease of use, React is popular because it is has good documentation and a well-established, community-driven ecosystem of documentation and plugins.

      This tutorial will walk you through best practices for embedding a React application in a WordPress site. For its example, it will use a common use case: creating a widget intended to be embedded on multiple pages and sometimes multiple times on a page. On the server side, it will be implemented as a WordPress shortcode. A shortcode is like an HTML tag, but it uses square brackets ([…]) instead of angle brackets (<…>). Instead of rendering an HTML element directly, it invokes a PHP function, which in turn renders HTML, interpolated with data from the database.

      By the end of this tutorial, you will have created your own shortcode, inserted it into a page in WP Admin, and published that page. On that page, you will be able to see your React widget displayed by the browser.

      Prerequisites

      In order to follow this tutorial, you must have:

      • An Ubuntu 18.04 server set up with the Initial Server Setup with Ubuntu 18.04 tutorial to configure a firewall for your server along with a new user who has root privileges.
      • A fully registered domain name. This tutorial will use your_domain as an example throughout. You can purchase a domain name on Namecheap, get one for free on Freenom, or use the domain registrar of your choice.
      • Both of the following DNS records set up for your server. You can follow this introduction to DigitalOcean DNS for details on how to add them.

        • An A record with your_domain pointing to your server’s public IP address.
        • An A record with www.your_domain pointing to your server’s public IP address.
      • Installations of Apache, MySQL, and PHP on your server. You can get this by following How To Install Linux, Apache, MySQL, PHP (LAMP) stack on Ubuntu 18.04.

      • Secured Apache with Let’s Encrypt by following How To Secure Apache with Let’s Encrypt on Ubuntu 18.04 to generate a free SSL certificate.

      • A WordPress installation, which you can get by following How To Install WordPress with LAMP on Ubuntu 18.04 and its prerequisites.

      • Installation of Node.js by following the “Installing Using a PPA” option in How To Install Node.js on Ubuntu 18.04. This tutorial will be using version 11.15.0, so when using curl to download the installation script, replace 10.x with 11.x to follow along with the procedure in this tutorial.

      Step 1 — Updating and Configuring Filesystem Permissions

      When logged in as the non-root user created in the Initial Server Setup with Ubuntu 18.04 prerequisite, you will not have access to view or edit any files in the WordPress directory. This is a problem, as you will be adding and modifying files later to create your WordPress plugin and your React application. To fix this problem, in this step you will update your WordPress configuration so that you have access to edit your WordPress files.

      Run the following command, substituting sammy for the name of your non-root user and /var/www/wordpress for the path to your WordPress directory (which is the Apache document root folder you created in the prerequisite):

      • sudo chown -R sammy:www-data /var/www/wordpress

      Let’s break down this command:

      • sudo — This allows you to execute this command as root, since you are modifying files sammy does not have access to.
      • chown — This command changes file ownership.
      • -R — This flag changes the ownership recursively, including all subfolders and files.
      • sammy:www-data — This sets the owner as your non-root user (sammy) and keeps the group as www-data so that Apache can still access the files in order to serve them.
      • /var/www/wordpress — This specifies the path to your WordPress directory. This is the directory on which the ownership will change.

      To verify that this command was successful, list out the contents of the WordPress directory:

      • ls -la /var/www/wordpress

      You will see a listing of the contents of the directory:

      Output

      total 216 drwxr-x--- 5 sammy www-data 4096 Apr 13 15:42 . drwxr-xr-x 4 root root 4096 Apr 13 15:39 .. -rw-r----- 1 sammy www-data 235 Apr 13 15:54 .htaccess -rw-r----- 1 sammy www-data 420 Nov 30 2017 index.php -rw-r----- 1 sammy www-data 19935 Jan 1 20:37 license.txt -rw-r----- 1 sammy www-data 7425 Jan 9 02:56 readme.html -rw-r----- 1 sammy www-data 6919 Jan 12 06:41 wp-activate.php drwxr-x--- 9 sammy www-data 4096 Mar 13 00:18 wp-admin -rw-r----- 1 sammy www-data 369 Nov 30 2017 wp-blog-header.php -rw-r----- 1 sammy www-data 2283 Jan 21 01:34 wp-comments-post.php -rw-r----- 1 sammy www-data 2898 Jan 8 04:30 wp-config-sample.php -rw-r----- 1 sammy www-data 3214 Apr 13 15:42 wp-config.php drwxr-x--- 6 sammy www-data 4096 Apr 13 15:54 wp-content -rw-r----- 1 sammy www-data 3847 Jan 9 08:37 wp-cron.php drwxr-x--- 19 sammy www-data 12288 Mar 13 00:18 wp-includes -rw-r----- 1 sammy www-data 2502 Jan 16 05:29 wp-links-opml.php -rw-r----- 1 sammy www-data 3306 Nov 30 2017 wp-load.php -rw-r----- 1 sammy www-data 38883 Jan 12 06:41 wp-login.php -rw-r----- 1 sammy www-data 8403 Nov 30 2017 wp-mail.php -rw-r----- 1 sammy www-data 17947 Jan 30 11:01 wp-settings.php -rw-r----- 1 sammy www-data 31085 Jan 16 16:51 wp-signup.php -rw-r----- 1 sammy www-data 4764 Nov 30 2017 wp-trackback.php -rw-r----- 1 sammy www-data 3068 Aug 17 2018 xmlrpc.php

      These files are the ones included in the WordPress core in the file named latest.tar.gz that you downloaded from wordpress.org in the prerequisite How To Install WordPress with LAMP on Ubuntu 18.04. If the permissions appear as they do in the preceding output, this means that your files and directories have been updated correctly.

      In this step, you updated your WordPress installation to give yourself access to edit its files. In the next step, you will use that access to create files that will compose a WordPress plugin.

      Step 2 — Creating a Basic WordPress Plugin

      Now that you have access to modify files in the WordPress directory, you will create a basic WordPress plugin and add it to the installation. This will allow React to interact with WordPress later in the tutorial.

      A WordPress plugin can be as simple as:

      1. A directory inside wp-content/plugins.
      2. A file inside that directory with the same name and a .php file extension.
      3. A special comment at the top of that file that provides WordPress with important plugin metadata.

      To make a plugin for the React code you will write later, start by creating a directory for the WordPress plugin. For simplicity, this tutorial will name the plugin react-wordpress. Run the following command, replacing wordpress with your Apache document root:

      • mkdir /var/www/wordpress/wp-content/plugins/react-wordpress

      Then, navigate to the newly-created directory. Subsequent commands will be executed from here.

      • cd /var/www/wordpress/wp-content/plugins/react-wordpress

      Let’s create the plugin file now. This tutorial will use nano, invoked with the command nano, as the command line text editor for all files. You are also free to use any other text editor of your choice, such as Pico, Vim, or Emacs.

      Open up react-wordpress.php for editing:

      Add the following lines to your file to create the start of the plugin:

      /var/www/wordpress/wp-content/plugins/react-wordpress/react-wordpress.php

      <?php
      /**
       * @wordpress-plugin
       * Plugin Name:       Embedding React In WordPress
       */
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      

      The commented section at the top provides metadata for the plugin, and the line that checks for the ABSPATH constant prevents a bad actor from accessing this script directly by its URL. ABSPATH is the absolute path to your WordPress root directory, so if ABSPATH is defined, you can be sure the file was loaded through the WordPress environment.

      Note: Many fields are available for a plugin metadata comment, but only Plugin Name is required. See the Header Requirements page in the WordPress documentation for more details.

      Next, open up a web browser and navigate to the Plugins page of your domain (https://your_domain/wp-admin/plugins.php). You will see your plugin listed along with WordPress’s default plugins:

      WP Admin Plugins Page

      Click Activate to enable your plugin.

      Once you have activated your plugin, the row containing your plugin will be highlighted in blue, with a blue border on the left, and instead of a link below it that says Activate, there will be one that says Deactivate:

      WP Admin Plugins Page After Plugin Activation

      Next, you will establish the structure of your plugin.

      Go back to your terminal to open react-wordpress.php:

      Then update it to add the following highlighted lines, which define useful constants:

      /var/www/wordpress/wp-content/plugins/react-wordpress/react-wordpress.php

      <?php
      /**
       * @wordpress-plugin
       * Plugin Name:       Embedding React In WordPress
       */
      
      defined( 'ABSPATH' ) or die( 'Direct script access diallowed.' );
      
      define( 'ERW_WIDGET_PATH', plugin_dir_path( __FILE__ ) . '/widget' );
      define( 'ERW_ASSET_MANIFEST', ERW_WIDGET_PATH . '/build/asset-manifest.json' );
      define( 'ERW_INCLUDES', plugin_dir_path( __FILE__ ) . '/includes' );
      

      In the newly added lines, you defined three constants:

      1. ERW_WIDGET_PATH — This will be the path to the React application.
      2. ERW_ASSET_MANIFEST — This is the path to the React asset manifest, a file that contains the list of JavaScript and CSS files that need to be included on the page for your application to work.
      3. ERW_INCLUDES — This subdirectory will contain all of the PHP files.

      Note that each define() refers to plugin_dir_path( __FILE__ ). That stands for the directory path to that file.

      After adding the constant definitions, save the file and exit the editor.

      Note: It is important to namespace your constants. In this case we are using the namespace ERW_, which stands for Embedding React in WordPress. Prefixing variables with this namespace ensures they are unique so that they don’t conflict with constants defined in other plugins.

      To create the includes/ folder, which will contain the other PHP files, start at the top level of the plugin directory, /var/www/your_domain/wp-content/plugins/react-wordpress. Then, create the folder:

      Now that you’ve scaffolded the PHP-related files and folders needed to make a WordPress plugin, you will create the initial files and folders for React.

      Step 3 — Initializing the React Application

      In this step, you will use Create React App to initialize your React application.

      This tutorial was tested using Create React App version 3.0.1. Version 3.0.0 made breaking changes to the structure of asset-manifest.json, so this earlier version is not compatible with this tutorial without modifications. To ensure you are using the version expected here, run this command to install Create React App:

      • sudo npm install --global create-react-app@3.0.1

      This command will install version 3.0.1 of Create React App. The --global flag will install it system-wide. Installing system-wide ensures that when you run create-react-app (or npx create-react-app) without any path specified, you will use the version that you just installed.

      After installing Create React App, use it to create the React application. This tutorial will name the app widget:

      • sudo create-react-app widget

      This command uses npx, which is a binary that ships with NPM. It is designed to make it easy to use CLI tools and other executables that are hosted on NPM. It will install those tools if they are not found locally.

      The create-react-app command will generate a project folder and all of the necessary files for a basic React app. This includes an index.html file, starting JavaScript, CSS, and test files, and a package.json for defining your project and dependencies. It pre-includes dependencies and scripts that let you build your application for production without needing to install and configure any additional build tools.

      Once you have set up the widget app, the output in the terminal will look something like this:

      Output

      … Success! Created widget at /var/www/wordpress/wp-content/plugins/react-wordpress/widget Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd widget npm start Happy hacking!

      Next, navigate to the newly created directory:

      You will now be able to build your application using the default build command, npm run build. This build command looks at the file package.json under the key scripts for a script named build:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/package.json

      {
        "name": "widget",
        "version": "0.1.0",
        "private": true,
        "dependencies": {
          "react": "^16.9.0",
          "react-dom": "^16.9.0",
          "react-scripts": "3.1.1"
        },
        "scripts": {
          "start": "react-scripts start",
          "build": "react-scripts build",
          "test": "react-scripts test",
          "eject": "react-scripts eject"
        },
        "eslintConfig": {
          "extends": "react-app"
        },
        "browserslist": {
          "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
          ],
          "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
          ]
        }
      }
      

      This calls the react-scripts.js executable provided by the react-scripts node module, which is one of the core components provided by create-react-app. This in turn calls the build script, which uses webpack to compile your project files into static asset files your browser understands. It does this by:

      • Resolving dependencies.
      • Compiling SASS files into CSS and JSX or TypeScript into JavaScript.
      • Transforming ES6 syntax into ES5 syntax with better cross-browser compatibility.

      Now that you know a bit about build, run the command in your terminal:

      Once the command completes, you will receive output similar to the following:

      Output

      > widget@0.1.0 build /var/www/wordpress/wp-content/plugins/react-wordpress/widget > react-scripts build Creating an optimized production build… Compiled successfully. File sizes after gzip: 36.83 KB (+43 B) build/static/js/2.6efc73d3.chunk.js 762 B (+44 B) build/static/js/runtime~main.a8a9905a.js 710 B (+38 B) build/static/js/main.2d1d08c1.chunk.js 539 B (+44 B) build/static/css/main.30ddb8d4.chunk.css The project was built assuming it is hosted at the server root. You can control this with the homepage field in your package.json. For example, add this to build it for GitHub Pages: "homepage" : "http://myname.github.io/myapp", The build folder is ready to be deployed. You may serve it with a static server: npm install -g serve serve -s build Find out more about deployment here: https://bit.ly/CRA-deploy

      Your project is now built, but before moving to the next step, it is a best practice to ensure that your application only loads if it is present.

      React uses an HTML element in the DOM inside of which it renders the application. This is called the target element. By default, this element has the ID root. To ensure that this root node is the app you are creating, alter src/index.js to check the ID of the target for the namespaced erw-root. To do this, first open src/index.js:

      Modify and add the highlighted lines:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/src/index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import App from './App';
      import * as serviceWorker from './serviceWorker';
      
      const target = document.getElementById('erw-root');
      if (target) { ReactDOM.render(<App />, target); }
      
      serviceWorker.unregister();
      

      Finally, save and quit the file when you are done editing.

      In this file, you made two important changes to the default index.js file:

      1. You changed the target element from <div id="root"></div> to <div id="erw-root"></div> so it is namespaced for your application.
      2. You enclosed the call to ReactDOM.render() in an if (…) statement so that the app is only loaded if it is present.

      Note: If you expect the widget to be present on every page, you may also wish to add a line of error handling, which prints a message to the console if an element with ID erw-root is not found. However, this tutorial will omit this step. A line like this would produce a console error on every page that does not have the element, including ones in which you are not planning to include the element. These multiple JavaScript console errors can risk lowering the search engine rankings for your site.

      After changing any JavaScript or CSS file in your src/ directory, it is important to recompile your app so that your changes are incorporated. To rebuild your app, run:

      Now your build/ directory contains a working React application in the form of JavaScript and CSS files. The next step involves setting up some PHP files that will enqueue your JavaScript and CSS in the page.

      Step 4 — Enqueueing the JavaScript and CSS Files

      In this step, you will use WordPress actions and filters to:

      1. Output the script-enqueueing code at the appropriate time in the WordPress page load cycle.
      2. Enqueue your JavaScript and CSS files in a way that least impacts page load speed.

      WordPress uses actions and filters as its primary hooks. Actions make it possible to execute code at a specified time in the page load cycle, and filters modify specific behavior by changing the return value of functions you do not otherwise own.

      To use these hooks, you will create a PHP file that will contain the code that parses the asset manifest. This is the same file you will use later to enqueue all of the assets so the scripts are written into the <head> tag.

      Before creating the file, use the following command to navigate out of the directory containing your React app and into the top-level react-wordpress plugin directory:

      • cd /var/www/wordpress/wp-content/plugins/react-wordpress

      Create the enqueue.php file inside the includes/ folder:

      • nano includes/enqueue.php

      Start by placing the opening <?php tag at the top of the file. Also add the line that checks for ABSPATH, which as discussed before is a best practice in every PHP file:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/enqueue.php

      <?php
      // This file enqueues scripts and styles
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      

      Save and quit this file.

      Then, update react-wordpress.php to require enqueue.php from the project. First, open up the file for editing:

      Add the following highlighted line:

      /var/www/wordpress/wp-content/plugins/react-wordpress/react-wordpress.php

      <?php
      /**
       * @wordpress-plugin
       * Plugin Name:       Embedding React In WordPress
       */
      
      defined( 'ABSPATH' ) or die( 'Direct script access diallowed.' );
      
      define( 'ERW_WIDGET_PATH', plugin_dir_path( __FILE__ ) . '/widget' );
      define( 'ERW_ASSET_MANIFEST', ERW_WIDGET_PATH . '/build/asset-manifest.json' );
      define( 'ERW_INCLUDES', plugin_dir_path( __FILE__ ) . '/includes' );
      
      require_once( ERW_INCLUDES . '/enqueue.php' );
      

      It is a common pattern in WordPress plugins to require other PHP files from the includes/ directory in order to split important tasks into chunks. The require_once() function parses the contents of the file passed as an argument as though that file’s PHP code were written right there inline. Unlike the similar command include, require will raise an exception if the file you are trying to require cannot be found. Using require_once() (as opposed to just require()) ensures that enqueue.php will not be parsed multiple times if the directive require_once( ERW_INCLUDES . '/enqueue.php' ); is given multiple times.

      Save and exit the file.

      Now reopen includes/enqueue.php:

      • nano includes/enqueue.php

      Then, add the following highlighted code:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/enqueue.php

      <?php
      // This file enqueues scripts and styles
      
      defined( 'ABSPATH' ) or die( 'Direct script access diallowed.' );
      
      add_action( 'init', function() {
      
        add_filter( 'script_loader_tag', function( $tag, $handle ) {
          if ( ! preg_match( '/^erw-/', $handle ) ) { return $tag; }
          return str_replace( ' src', ' async defer src', $tag );
        }, 10, 2 );
      
        add_action( 'wp_enqueue_scripts', function() {
      
        });
      });
      

      Adding a function to the init action means that this code will be run during the init phase of the load process, which is after your theme and other plugins have loaded.

      Setting the async and defer attributes on the <script> tags using the script_loader_tag filter tells the browser to load the scripts asynchronously instead of blocking DOM construction and page rendering.

      The wp_enqueue_scripts action then enqueues front-end items. See this page for more details.

      Be sure to write the file and exit.

      You have now told WordPress to write script and stylesheet tags to the page. In this next step, you will parse a file called the asset manifest. This will give you the paths to all of the files that you’ll need to enqueue.

      Step 5 — Parsing the Asset Manifest

      In this step, you will parse the asset manifest generated by the React build into a list of JavaScript and CSS files.

      When you build the application, the React build script will build your project into multiple JavaScript and CSS files. The files quantity and names will vary from one build to the next, as each one includes a hash of the file’s contents. The asset manifest provides the name of each file generated in the last build along with the path to that file. By parsing it programatically, you are guaranteed that script and stylesheet tags you write to the page will always point to the right files, even when the names change.

      First, examine the asset-manifest.json with the cat command:

      • cat widget/build/asset-manifest.json

      It will look something like this:

      Output

      { "files": { "main.css": "/static/css/main.2cce8147.chunk.css", "main.js": "/static/js/main.a284ff71.chunk.js", "main.js.map": "/static/js/main.a284ff71.chunk.js.map", "runtime~main.js": "/static/js/runtime~main.fa565546.js", "runtime~main.js.map": "/static/js/runtime~main.fa565546.js.map", "static/js/2.9ca06fd6.chunk.js": "/static/js/2.9ca06fd6.chunk.js", "static/js/2.9ca06fd6.chunk.js.map": "/static/js/2.9ca06fd6.chunk.js.map", "index.html": "/index.html", "precache-manifest.e40c3c7a647ca45e36eb20f8e1a654ee.js": "/precache-manifest.e40c3c7a647ca45e36eb20f8e1a654ee.js", "service-worker.js": "/service-worker.js", "static/css/main.2cce8147.chunk.css.map": "/static/css/main.2cce8147.chunk.css.map", "static/media/logo.svg": "/static/media/logo.5d5d9eef.svg" } }

      To parse it, your code will look for object keys that end with .js and .css.

      Open up your enqueue.php file:

      • nano includes/enqueue.php

      Add the highlighted snippet:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/enqueue.php

      <?php
      // This file enqueues scripts and styles
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      
      add_action( 'init', function() {
      
        add_filter( 'script_loader_tag', function( $tag, $handle ) {
          if ( ! preg_match( '/^erw-/', $handle ) ) { return $tag; }
          return str_replace( ' src', ' async defer src', $tag );
        }, 10, 2 );
      
        add_action( 'wp_enqueue_scripts', function() {
      
          $asset_manifest = json_decode( file_get_contents( ERW_ASSET_MANIFEST ), true )['files'];
      
          if ( isset( $asset_manifest[ 'main.css' ] ) ) {
            wp_enqueue_style( 'erw', get_site_url() . $asset_manifest[ 'main.css' ] );
          }
      
          wp_enqueue_script( 'erw-runtime', get_site_url() . $asset_manifest[ 'runtime~main.js' ], array(), null, true );
      
          wp_enqueue_script( 'erw-main', get_site_url() . $asset_manifest[ 'main.js' ], array('erw-runtime'), null, true );
      
          foreach ( $asset_manifest as $key => $value ) {
            if ( preg_match( '@static/js/(.*).chunk.js@', $key, $matches ) ) {
              if ( $matches && is_array( $matches ) && count( $matches ) === 2 ) {
                $name = "erw-" . preg_replace( '/[^A-Za-z0-9_]/', '-', $matches[1] );
                wp_enqueue_script( $name, get_site_url() . $value, array( 'erw-main' ), null, true );
              }
            }
      
            if ( preg_match( '@static/css/(.*).chunk.css@', $key, $matches ) ) {
              if ( $matches && is_array( $matches ) && count( $matches ) == 2 ) {
                $name = "erw-" . preg_replace( '/[^A-Za-z0-9_]/', '-', $matches[1] );
                wp_enqueue_style( $name, get_site_url() . $value, array( 'erw' ), null );
              }
            }
          }
      
        });
      });
      

      When you are done, write and quit the file.

      The highlighted code does the following:

      1. Reads the asset manifest file and parses it as a JSON file. It accesses the content stored at the key 'files' and stores it to the $asset_manifest variable.
      2. Enqueues the main CSS file if it exists.
      3. Enqueues the React runtime first, then the main JavaScript file, setting the runtime as a dependency to ensure it is loaded in the page first.
      4. Parses the asset manifest file list for any JavaScript files named static/js/<hash>.chunk.js and enqueues them in the page after the main file.
      5. Parses the asset manifest file list for any CSS files named static/css/<hash>.chunk.css and enqueues them in the page after the main CSS file.

      Note: Using wp_enqueue_script() and wp_enqueue_style will cause <script> and <link> tags for the enqueued files to appear in every page. The last argument true tells WordPress to place the file below the page content footer instead of at the bottom of the <head> element. This is important so that loading the JavaScript files doesn’t slow down the rest of the page.

      In this step, you isolated the filepaths of the scripts and styles used by your app. In the next step, you will ensure that those filepaths point to your React app’s build directory and that none of your source files are accessible from the browser.

      Step 6 — Serving and Securing Static Files

      At this point, you have told WordPress which JavaScript and CSS files to load and where to find them. However, if you visit https://your_domain in the browser and look at the JavaScript console, you will see HTTP 404 errors. (Check out this article for more info on how to use the JavaScript console.)

      404 Errors in the JavaScript Console

      This is because the URL route to the file (e.g., /static/js/main.2d1d08c1.chunk.js) does not match the actual path to the file (e.g., /wp-content/plugins/react-wordpress/widget/build/static/js/main.2d1d08c1.chunk.js).

      In this step, you will correct this issue by telling React where the build directory is located. You will also add an Apache rewrite rule to the .htaccess file to protect your source files from being viewed in the browser.

      To give React the correct path to your app, open package.json inside of your React application’s directory:

      • sudo nano widget/package.json

      Then, add the highlighted homepage line:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/package.json

      {
        "name": "widget",
        "version": "0.1.0",
        "private": true,
        "homepage": "/wp-content/plugins/react-wordpress/widget/build",
        "dependencies": {
          "react": "^16.9.0",
          "react-dom": "^16.9.0",
          "react-scripts": "3.1.1"
        },
        "scripts": {
          "start": "react-scripts start",
          "build": "react-scripts build",
          "test": "react-scripts test",
          "eject": "react-scripts eject"
        },
        "eslintConfig": {
          "extends": "react-app"
        },
        "browserslist": {
          "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
          ],
          "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
          ]
        }
      }
      

      Write and quit the file. Then, rebuild your React application. Move to the top level of widget/:

      Then run the build command:

      After the build command completes, inspect the asset manifest by outputting its contents to the terminal:

      • cat build/asset-manifest.json

      You will see that the file paths have all changed:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/build/asset-manifest.json

      {
        "files": {
          "main.css": "/wp-content/plugins/react-wordpress/widget/build/static/css/main.2cce8147.chunk.css",
          "main.js": "/wp-content/plugins/react-wordpress/widget/build/static/js/main.a28d856a.chunk.js",
          "main.js.map": "/wp-content/plugins/react-wordpress/widget/build/static/js/main.a28d856a.chunk.js.map",
          "runtime~main.js": "/wp-content/plugins/react-wordpress/widget/build/static/js/runtime~main.2df87c4b.js",
          "runtime~main.js.map": "/wp-content/plugins/react-wordpress/widget/build/static/js/runtime~main.2df87c4b.js.map",
          "static/js/2.9ca06fd6.chunk.js": "/wp-content/plugins/react-wordpress/widget/build/static/js/2.9ca06fd6.chunk.js",
          "static/js/2.9ca06fd6.chunk.js.map": "/wp-content/plugins/react-wordpress/widget/build/static/js/2.9ca06fd6.chunk.js.map",
          "index.html": "/wp-content/plugins/react-wordpress/widget/build/index.html",
          "precache-manifest.233e0a9875cf4d2df27d6280d12b780d.js": "/wp-content/plugins/react-wordpress/widget/build/precache-manifest.233e0a9875cf4d2df27d6280d12b780d.js",
          "service-worker.js": "/wp-content/plugins/react-wordpress/widget/build/service-worker.js",
          "static/css/main.2cce8147.chunk.css.map": "/wp-content/plugins/react-wordpress/widget/build/static/css/main.2cce8147.chunk.css.map",
          "static/media/logo.svg": "/wp-content/plugins/react-wordpress/widget/build/static/media/logo.5d5d9eef.svg"
        }
      }
      

      This tells your app where to find the correct files, but also presents a problem: It exposes the path to your app’s src directory, and somebody who is familiar with create-react-app could visit https://your_domain/wp-content/plugins/react-wordpress/widget/src/index.js and start exploring the source files for your app. Try it yourself!

      To protect the paths you do not want users to access, add an Apache rewrite rule to your WordPress’s .htaccess file.

      • nano /var/www/wordpress/.htaccess

      Add the four highlighted lines:

      /var/www/wordpress/.htaccess

      <IfModule mod_rewrite.c>
      RewriteRule ^wp-content/plugins/react-wordpress/widget/(build|public)/(.*) - [L]
      RewriteRule ^wp-content/plugins/react-wordpress/widget/* totally-bogus-erw.php [L]
      </IfModule>
      
      # BEGIN WordPress
      <IfModule mod_rewrite.c>
      RewriteEngine On
      RewriteBase /
      RewriteRule ^index.php$ - [L]
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteRule . /index.php [L]
      </IfModule>
      
      # END WordPress
      

      This tells Apache to allow browser requests to anything at wp-content/plugins/react-wordpress/widget/build/ or wp-content/react-wordpress/widget/public/. Anything else will redirect to totally-bogus-erw.php. Unless you have a file named totally-bogus-erw.php at your top level, this request will be handled by WordPress, which will render a 404 error.

      There are WordPress plugins, such as Stream, that will monitor request activity and log 404s. In the logs, the request will show the IP address and the page requested when the user received the 404. Watching for totally-bogus-erw.php will tell you if a specific IP address is trying to crawl your React app’s src directory.

      Be sure to write and quit the file.

      Now that you have established the routing necessary to load your JavaScript and CSS files onto the page, it is time to use a shortcode to add HTML elements to the page that the JavaScript will interact with to render your app.

      Step 7 — Creating a Shortcode

      Shortcodes make it possible to insert complex HTML blocks interpolated with server-side data, with very simple in-page syntax. In this step, you will create and register a WordPress shortcode and use that to embed your application in the page.

      Navigate to the top level of your plugin:

      • cd /var/www/wordpress/wp-content/plugins/react-wordpress/

      Create a new PHP file that will contain the shortcode:

      • touch includes/shortcode.php

      Then, edit your main PHP file so that includes/shortcode.php is required when your plugin loads. First open react-wordpress.php:

      Then add the following highlighted line:

      /var/www/wordpress/wp-content/plugins/react-wordpress/react-wordpress.php

      <?php
      /**
       * @wordpress-plugin
       * Plugin Name:       Embedding React In WordPress
       */
      
      defined( 'ABSPATH' ) or die( 'Direct script access diallowed.' );
      
      define( 'ERW_WIDGET_PATH', plugin_dir_path( __FILE__ ) . '/widget' );
      define( 'ERW_ASSET_MANIFEST', ERW_WIDGET_PATH . '/build/asset-manifest.json' );
      define( 'ERW_INCLUDES', plugin_dir_path( __FILE__ ) . '/includes' );
      
      require_once( ERW_INCLUDES . '/enqueue.php' );
      require_once( ERW_INCLUDES . '/shortcode.php' );
      

      Write and quit the file.

      Now, open the newly created shortcode file:

      • nano includes/shortcode.php

      Add the following code:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/shortcode.php

      <?php
      // This file enqueues a shortcode.
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      
      add_shortcode( 'erw_widget', function( $atts ) {
        $default_atts = array();
        $args = shortcode_atts( $default_atts, $atts );
      
        return "<div id='erw-root'></div>";
      });
      

      This code contains mostly boilerplate. It registers a shortcode named erw_widget that, when invoked, prints <div id="erw-root"></div>, the React app’s root element, to the page.

      Save and quit shortcode.php.

      To see the React app in action, you will need to create a new WordPress page and add the shortcode to it.

      Navigate to https://your_domain/wp-admin in a web browser. At the very top of the page, you’ll see a black bar that has the WordPress logo on the left, followed by a house icon, the name of your site, a comment bubble icon and number, and another link that says + New. Hover over the + New button and a menu will drop down. Click the menu item that says Page.

      Create a Page

      When the screen loads, your cursor will be focused in the text box that says Add title. Click there and start typing to give the new page a relevant title. This tutorial will use My React App:

      Giving the Page a Title

      Assuming you are using the WordPress Gutenberg editor, you will see a line of text near the top of the page, below the title, that reads Start writing or type / to choose a block. When you hover over that text, three symbols will appear on the right. Choose the nearest one that resembles [/] to add a shortcode block:

      Adding a Shortcode Block

      Type the shortcode [erw_widget] into the newly-added text area. Then, click the blue Publish… button in the upper right corner of the window, then press Publish to confirm.

      Type in Your Shortcode and Publish

      You will see a green bar confirming that the page has been published. Click the View Page link:

      Click Link to View Page

      On the screen, you will see your app:

      Working React App

      Now that you have a basic React app rendering in the page, you can customize that app with options provided server-side by an admin.

      Step 8 — Injecting Server-Generated Settings

      In this step, you will inject settings into the application using both server-generated data and user-provided data. This will enable you to display dynamic data in your application and to use the widget multiple times in a page.

      First, open the index.js file:

      • sudo nano widget/src/index.js

      Then, delete the import App from './App'; line and update the contents of index.js with the following highlighted lines:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/src/index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import * as serviceWorker from './serviceWorker';
      
      const App = () => (
        <div className="App">
          <span className="App__Message">Hello,<br />World!</span>
        </div>
      );
      
      const target = document.getElementById('erw-root');
      if (target) { ReactDOM.render(<App />, target); }
      
      serviceWorker.unregister();
      

      This modifies your React application so that instead of returning the default Create React App screen, it returns an element that reads Hello, World!.

      Save and quit the file. Then open index.css for editing:

      • nano widget/src/index.css

      Replace the contents of index.css with the following code:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/src/index.css

      .App {
        width: 100px;
        height: 100px;
        border: 1px solid;
        display: inline-block;
        margin-right: 20px;
        position: relative;
      }
      
      .App .App__Message {
        font-size: 15px;
        line-height: 15px;
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        text-align: center;
        width: 100%;
      }
      

      The styles for .App will render a 100-pixel square, with a solid border, and the styles for .App__Message will render text that is centered inside the square, both vertically and horizontally.

      Write and quit the file, then rebuild the application:

      • cd widget
      • sudo npm run build

      Once the build is successful, refresh https://your_domain/index.php/my-react-app/ in your browser. You will now see the box that you styled with CSS, along with the text Hello, World!:

      Simplified React Application

      Next, you will add custom settings, consisting of a user-provided border color and size. You will also pass the display name of the current user from the server.

      Updating the Shortcode to Accept Arguments

      To pass a user-provided argument, you must first give the user a way to pass an argument. Back in the terminal, navigate back to the top level of your plugin:

      Next, open your shortcode.php file for editing:

      • nano includes/shortcode.php

      Update your shortcode file to contain the following highlighted lines:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/shortcode.php

      <?php
      // This file enqueues your shortcode.
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      
      add_shortcode( 'erw_widget', function( $atts ) {
        $default_atts = array( 'color' => 'black' );
        $args = shortcode_atts( $default_atts, $atts );
      
        return "<div class='erw-root'></div>";
      });
      

      Write and quit the file. Notice how the code adds 'color' => 'black' to the $default_atts array. The array key color instructs WordPress to expect that the color attribute might be passed to the [erw_widget] shortcode. The array value, black, sets the default value. All shortcode attributes are passed to the shortcode function as strings, so if you do not want to set a default value, you could use the empty string ('') instead. The last line changes to use a class instead of an ID because it is expected that there will be more than one of the element in the page.

      Now, go back to your browser and click the Edit button beneath your Hello, World! box. Update the WordPress page in your browser to add a second instance of the shortcode, and add a color attribute to both instances. This tutorial will use [erw_widget color="#cf6f1a"] and [erw_widget color="#11757e"]:

      Add a Second Widget

      Click the blue Update button to save.

      Note: The second widget will not display yet. You need to update the React app to expect multiple instances identified by a class instead of a single instance identified by an ID.

      Next, open index.js for editing:

      • sudo nano widget/src/index.js

      Update it with the following:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/src/index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import * as serviceWorker from './serviceWorker';
      
      const App = () => (
        <div className="App">
          <span className="App__Message">Hello,<br />World!</span>
        </div>
      );
      
      const targets = document.querySelectorAll('.erw-root');
      Array.prototype.forEach.call(targets, target => ReactDOM.render(<App />, target));
      
      serviceWorker.unregister();
      

      Write and quit the file. The updated lines will invoke the React app on each instance with the class erw-root. So if the shortcode is used twice, two squares will appear in the page.

      Finally, open index.css for editing:

      • sudo nano widget/src/index.css

      Update the file to contain the following highlighted line:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/src/index.css

      .erw-root { display: inline-block; }
      
      .App {
        width: 100px;
        height: 100px;
        border: 1px solid;
        display: inline-block;
        margin-right: 20px;
        position: relative;
      }
      
      .App .App__Message {
        font-size: 15px;
        line-height: 15px;
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        text-align: center;
        width: 100%;
      }
      

      With this added line, multiple adjacent widgets will appear side-by-side instead of one above the other.

      Save and quit the file.

      Now, re-compile your React app:

      • cd widget
      • sudo npm run build

      Now, if you refresh the page in your browser, you will see both widgets:

      Two Widgets

      Notice that the widgets still do not display the border color. This will be addressed in a future section.

      Uniquely Identifying Each Widget Instance

      In order to uniquely identify each widget, it is necessary to pass an ID from the server. This can be done through the data-id attribute of the root element. This is important, since each widget on the page may have different settings.

      To do this, return back to your top level plugin directory and open shortcode.php for editing:

      • cd ..
      • nano includes/shortcode.php

      Update it to have the following highlighted lines:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/shortcode.php

      <?php
      // This file enqueues your shortcode.
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      
      add_shortcode( 'erw_widget', function( $atts ) {
        $default_atts = array( 'color' => 'black' );
        $args = shortcode_atts( $default_atts, $atts );
        $uniqid = uniqid('id');
      
        return "<div class='erw-root' data-id='{$uniqid}'></div>";
      });
      

      The first new line generates a unique ID with the prefix id. The updated line attaches the ID to the React root using the data-id attribute. This will make the ID accessible in React.

      Save the file, but do not yet exit from it.

      Write Settings to the JavaScript window Object

      In the shortcode file, you will write the settings to the page in a window-global JavaScript object. Using the window object ensures it can be accessed from within React.

      With shortcode.php still open, update it so it contains the following:

      /var/www/wordpress/wp-content/plugins/react-wordpress/includes/shortcode.php

      <?php
      // This file enqueues your shortcode.
      
      defined( 'ABSPATH' ) or die( 'Direct script access disallowed.' );
      
      add_shortcode( 'erw_widget', function( $atts ) {
        $default_atts = array( 'color' => 'black' );
        $args = shortcode_atts( $default_atts, $atts );
        $uniqid = uniqid('id');
      
        global $current_user;
        $display_name = $current_user ? $current_user->display_name : 'World';
      
        ob_start(); ?>
        <script>
        window.erwSettings = window.erwSettings || {};
        window.erwSettings["<?= $uniqid ?>"] = {
          'color': '<?= $args["color"] ?>',
          'name': '<?= $display_name ?>',
        }
        </script>
        <div class="erw-root" data-id="<?= $uniqid ?>"></div>
      
        <?php
        return ob_get_clean();
      });
      

      These updates write a <script> block before each element that initializes the window-global settings object and populates it with the data provided in WP Admin.

      Note: The syntax <?= is shorthand for <?php echo

      Save and quit the file.

      Now, inspect the WordPress page in your web browser. This will show you the HTML for your page. If you CTRL+F and search for window.erwSettings, you will see the settings being written to the HTML of your page as the following:

      …
        window.erwSettings = window.erwSettings || {};
        window.erwSettings["id5d5f1958aa5ae"] = {
          'color': '#cf6f1a',
          'name': 'sammy',
        }
      …
      

      Retrieve Settings From React

      In the React app, you will retrieve the settings based on the ID and pass the border color value to the App component as a property (prop). This lets the App component use the value without needing to know where it came from.

      Open index.js for editing:

      • sudo nano widget/src/index.js

      Update it so it contains the following highlighted lines:

      /var/www/wordpress/wp-content/plugins/react-wordpress/widget/src/index.js

      import React from 'react';
      import ReactDOM from 'react-dom';
      import './index.css';
      import * as serviceWorker from './serviceWorker';
      
      const App = ({ settings }) => (
        <div className="App" style={{borderColor: settings.color}}>
          <span className="App__Message">Hello,<br />{settings.name}!</span>
        </div>
      );
      
      const targets = document.querySelectorAll('.erw-root');
      Array.prototype.forEach.call(targets, target => {
        const id = target.dataset.id;
        const settings = window.erwSettingstag:www.digitalocean.com,2005:/community/tutorials/how-to-embed-a-react-application-in-wordpress-on-ubuntu-18-04;
        ReactDOM.render(<App settings={settings} />, target)
      });
      
      serviceWorker.unregister();
      

      Save the file and exit from your text editor.

      Your React app will now use the unique ID from the window-global window.erwSettings object to retrieve settings and pass them to the App component. To put this into effect, re-compile your application:

      • cd widget
      • sudo npm run build

      After completing this last step, refresh the WordPress page in your browser. You will see the user-provided border color and the server-provided display name appear in the widgets:

      Widgets with Settings Applied

      Conclusion

      In this tutorial, you created your own WordPress plugin with a React application inside of it. You then built a shortcode as a bridge to make your application embeddable within the WP Admin page builder, and in the end, you customized your widget on the page.

      Now, you can expand on your React application with the confidence that your delivery mechanism is already in place. This foundation in WordPress ensures that you can focus on the client-side experience, and as your application expands and grows, you can easily add more production-oriented tools and techniques that will work with any WordPress installation.

      For further reading on what you can do with your solid React foundation, try exploring one of these tutorials:



      Source link

      How To Set Up a Ruby on Rails Project with a React Frontend


      The author selected the Electronic Frontier Foundation to receive a donation as part of the Write for DOnations program.

      Introduction

      Ruby on Rails is a popular server-side web application framework, with over 42,000 stars on GitHub at the time of writing this tutorial. It powers a lot of the popular applications that exist on the web today, like GitHub, Basecamp, SoundCloud, Airbnb, and Twitch. With its emphasis on programmer experience and the passionate community that has built up around it, Ruby on Rails will give you the tools you need to build and maintain your modern web application.

      React is a JavaScript library used to create front-end user interfaces. Backed by Facebook, it is one of the most popular front-end libraries used on the web today. React offers features like a virtual Document Object Model (DOM), component architecture, and state management, which make the process of front-end development more organized and efficient.

      With the frontend of the web moving toward frameworks that are separate from the server-side code, combining the elegance of Rails with the efficiency of React will let you build powerful and modern applications informed by current trends. By using React to render components from within a Rails view instead of the Rails template engine, your application will benefit from the latest advancements in JavaScript and front-end development while still leveraging the expressiveness of Ruby on Rails.

      In this tutorial, you will create a Ruby on Rails application that stores your favorite recipes then displays them with a React frontend. When you are finished, you will be able to create, view, and delete recipes using a React interface styled with Bootstrap:

      Completed Recipe App

      If you would like to take a look at the code for this application, check out the companion repository for this tutorial on the DigitalOcean Community GitHub.

      Prerequisites

      To follow this tutorial, you need to have the following:

      • Node.js and npm installed on your development machine. This tutorial uses Node.js version 10.16.0 and npm version 6.9.0. Node.js is a JavaScript run-time environment that allows you to run your code outside of the browser. It comes with a pre-installed Package Manager called npm, which lets you install and update packages. To install these on macOS or Ubuntu 18.04, follow the steps in How to Install Node.js and Create a Local Development Environment on macOS or the “Installing Using a PPA” section of How To Install Node.js on Ubuntu 18.04.

      • The Yarn package manager installed on your development machine, which will allow you to download the React framework. This tutorial was tested on version 1.16.0; to install this dependency, follow the official Yarn installation guide.

      • Installation of the Ruby on Rails framework. To get this, follow our guide on How to Install Ruby on Rails with rbenv on Ubuntu 18.04, or How To Install Ruby on Rails with rbenv on CentOS 7. If you would like to develop this application on macOS, please see this tutorial on How To Install Ruby on Rails with rbenv on macOS. This tutorial was tested on version 2.6.3 of Ruby and version 5.2.3 of Rails, so make sure to specify these versions during the installation process.

      • Installation of PostgreSQL, as shown in Steps 1 and 2 of our tutorial How To Use PostgreSQL with Your Ruby on Rails Application on Ubuntu 18.04 or How To Use PostgreSQL with Your Ruby on Rails Application on macOS. To follow this tutorial, use PostgreSQL version 10. If you are looking to develop this application on a different distribution of Linux or on another OS, see the official PostgreSQL downloads page. For more information on how to use PostgreSQL, see our How To Install and Use PostgreSQL tutorials.

      Step 1 — Creating a New Rails Application

      In this step, you will build your recipe application on the Rails application framework. First, you’ll create a new Rails application, which will be set up to work with React out of the box with little configuration.

      Rails provides a number of scripts called generators that help in creating everything that’s necessary to build a modern web application. To see a full list of these commands and what they do, run the following command in your Terminal window:

      This will yield a comprehensive list of options, which will allow you to set the parameters of your application. One of the commands listed is the new command, which creates a new Rails application.

      Now, you will create a new Rails application using the new generator. Run the following command in your Terminal window:

      • rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee

      The preceding command creates a new Rails application in a directory named rails_react_recipe, installs the required Ruby and JavaScript dependencies, and configures Webpack. Let’s walk through the flags that are associated with this new generator command:

      • The -d flag specifies the preferred database engine, which in this case is PostgreSQL.
      • The -T flag instructs Rails to skip the generation of test files, since you won’t be writing tests for the purposes of this tutorial. This command is also suggested if you want to use a Ruby testing tool different from the one Rails provides.
      • The --webpack instructs Rails to preconfigure for JavaScript with the webpack bundler, in this case specifically for a React application.
      • The --skip-coffee asks Rails not to set up CoffeeScript, which is not needed for this tutorial.

      Once the command is done running, move into the rails_react_recipe directory, which is the root directory of your app:

      Next, list out the contents of the directory:

      This root directory has a number of auto-generated files and folders that make up the structure of a Rails application, including a package.json file containing the dependencies for a React application.

      Now that you have successfully created a new Rails application, you are ready to hook it up to a database in the next step.

      Step 2 — Setting Up the Database

      Before you run your new Rails application, you have to first connect it to a database. In this step, you'll connect the newly created Rails application to a PostgreSQL database, so recipe data can be stored and fetched when needed.

      The database.yml file found in config/database.yml contains database details like database name for different development environments. Rails specifies a database name for the different development environments by appending an underscore (_) followed by the environment name to your app’s name. You can always change any environment database name to whatever you prefer.

      Note: At this point, you can alter config/database.yml to set up which PostgreSQL role you would like Rails to use to create your database. If you followed the Prerequisite How To Use PostgreSQL with Your Ruby on Rails Application and created a role that is secured by a password, you can follow the instructions in Step 4 for macOS or Ubuntu 18.04.

      As earlier stated, Rails offers a lot of commands to make developing web applications easy. This includes commands to work with databases, such as create, drop, and reset. To create a database for your application, run the following command in your Terminal window:

      This command creates a development and test database, yielding the following output:

      Output

      Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

      Now that the application is connected to a database, start the application by running the following command in you Terminal window:

      • rails s --binding=127.0.0.1

      The s or server command fires up Puma, which is a web server distributed with Rails by default, and --binding=127.0.0.1 binds the server to your localhost.

      Once you run this command, your command prompt will disappear, and you will see the following output:

      Output

      => Booting Puma => Rails 5.2.3 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 Use Ctrl-C to stop

      To see your application, open a browser window and navigate to http://localhost:3000. You will see the Rails default welcome page:

      Rails welcome page

      This means that you have properly set up your Rails application.

      To stop the web server at anytime, press CTRL+C in the Terminal window where the server is running. Go ahead and do this now; you will get a goodbye message from Puma:

      Output

      ^C- Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting

      Your prompt will then reappear.

      You have successfully set up a database for your food recipe application. In the next step, you will install all the extra JavaScript dependencies you need to put together your React frontend.

      Step 3 — Installing Frontend Dependencies

      In this step, you will install the JavaScript dependencies needed on the frontend of your food recipe application. They include:

      Run the following command in your Terminal window to install these packages with the Yarn package manager:

      • yarn add react-router-dom bootstrap jquery popper.js

      This command uses Yarn to install the specified packages and adds them to the package.json file. To verify this, take a look at the package.json file located in the root directory of the project:

      You'll see the installed packages listed under the dependencies key:

      ~/rails_react_recipe/package.json

      {
        "name": "rails_react_recipe",
        "private": true,
        "dependencies": {
          "@babel/preset-react": "^7.0.0",
          "@rails/webpacker": "^4.0.7",
          "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
          "bootstrap": "^4.3.1",
          "jquery": "^3.4.1",
          "popper.js": "^1.15.0",
          "prop-types": "^15.7.2",
          "react": "^16.8.6",
          "react-dom": "^16.8.6",
          "react-router-dom": "^5.0.1"
        },
        "devDependencies": {
          "webpack-dev-server": "^3.7.2"
        }
      }
      

      You have installed a few front-end dependencies for your application. Next, you’ll set up a homepage for your food recipe application.

      Step 4 — Setting Up the Homepage

      With all the required dependencies installed, in this step you will create a homepage for the application. The homepage will serve as the landing page when users first visit the application.

      Rails follows the Model-View-Controller architectural pattern for applications. In the MVC pattern, a controller's purpose is to receive specific requests and pass them along to the appropriate model or view. Right now the application displays the Rails welcome page when the root URL is loaded in the browser. To change this, you will create a controller and view for the homepage and match it to a route.

      Rails provides a controller generator for creating a controller. The controller generator receives a controller name, along with a matching action. For more on this, check out the official Rails documentation.

      This tutorial will call the controller Homepage. Run the following command in your Terminal window to create a Homepage controller with an index action.

      • rails g controller Homepage index

      Note:
      On Linux, if you run into the error FATAL: Listen error: unable to monitor directories for changes., this is due to a system limit on the number of files your machine can monitor for changes. Run the following command to fix it:

      • echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

      This will permanently increase the amount of directories that you can monitor with Listen to 524288. You can change this again by running the same command and replacing 524288 with your desired number.

      Running this command generates the following files:

      • A homepage_controller.rb file for receiving all homepage-related requests. This file contains the index action you specified in the command.
      • A homepage.js file for adding any JavaScript behavior related to the Homepage controller.
      • A homepage.scss file for adding styles related to the Homepage controller.
      • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
      • An index.html.erb file which is the view page for rendering anything related to the homepage.

      Apart from these new pages created by running the Rails command, Rails also updates your routes file which is located at config/routes.rb. It adds a get route for your homepage which you will modify as your root route.

      A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb in your favorite editor:

      Inside this file, replace get 'homepage/index' with root 'homepage#index' so that the file looks like the following:

      ~/rails_react_recipe/config/routes.rb

      Rails.application.routes.draw do
        root 'homepage#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      This modification instructs Rails to map requests to the root of the application to the index action of the Homepage controller, which in turn renders whatever is in the index.html.erb file located at app/views/homepage/index.html.erb on to the browser.

      To verify that this is working, start your application:

      • rails s --binding=127.0.0.1

      Opening the application in the browser, you will see a new landing page for your application:

      Application Homepage

      Once you have verified that your application is working, press CTRL+C to stop the server.

      Next, delete the contents of the ~/rails_react_recipe/app/views/homepage/index.html.erb file. By doing this, you will ensure that the contents of index.html.erb do not interfere with the React rendering of your frontend.

      Now that you have set up your homepage for your application, you can move to the next section, where you will configure the frontend of your application to use React.

      Step 5 — Configuring React as Your Rails Frontend

      In this step, you will configure Rails to use React on the frontend of the application, instead of its template engine. This will allow you to take advantage of React rendering to create a more visually appealing homepage.

      Rails, with the help of the Webpacker gem, bundles all your JavaScript code into packs. These can be found in the packs directory at app/javascript/packs. You can link these packs in Rails views using the javascript_pack_tag helper, and you can link stylesheets imported into the packs using the stylesheet_pack_tag helper. To create an entry point to your React environment, you will add one of these packs to your application layout.

      First, rename the ~/rails_react_recipe/app/javascript/packs/hello_react.jsx file to ~/rails_react_recipe/app/javascript/packs/Index.jsx.

      • mv ~/rails_react_recipe/app/javascript/packs/hello_react.jsx ~/rails_react_recipe/app/javascript/packs/Index.jsx

      After renaming the file, open application.html.erb, the application layout file:

      • nano ~/rails_react_recipe/app/views/layouts/application.html.erb

      Add the following highlighted lines of code at the end of the head tag in the application layout file:

      ~/rails_react_recipe/app/views/layouts/application.html.erb

      <!DOCTYPE html>
      <html>
        <head>
          <title>RailsReactRecipe</title>
          <%= csrf_meta_tags %>
          <%= csp_meta_tag %>
      
          <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
          <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <%= javascript_pack_tag 'Index' %>
        </head>
      
        <body>
          <%= yield %>
        </body>
      </html>
      

      Adding the JavaScript pack to your application’s header makes all your JavaScript code available and executes the code in your Index.jsx file on the page whenever you run the app. Along with the JavaScript pack, you also added a meta viewport tag to control the dimensions and scaling of pages on your application.

      Save and exit the file.

      Now that your entry file is loaded onto the page, create a React component for your homepage. Start by creating a components directory in the app/javascript directory:

      • mkdir ~/rails_react_recipe/app/javascript/components

      The components directory will house the component for the homepage, along with other React components in the application. The homepage will contain some text and a call to action button to view all recipes.

      In your editor, create a Home.jsx file in the components directory:

      • nano ~/rails_react_recipe/app/javascript/components/Home.jsx

      Add the following code to the file:

      ~/rails_react_recipe/app/javascript/components/Home.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      export default () => (
        <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
          <div className="jumbotron jumbotron-fluid bg-transparent">
            <div className="container secondary-color">
              <h1 className="display-4">Food Recipes</h1>
              <p className="lead">
                A curated list of recipes for the best homemade meal and delicacies.
              </p>
              <hr className="my-4" />
              <Link
                to="/recipes"
                className="btn btn-lg custom-button"
                role="button"
              >
                View Recipes
              </Link>
            </div>
          </div>
        </div>
      );
      

      In this code, you imported React and also the Link component from React Router. The Link component creates a hyperlink to navigate from one page to another. You then created and exported a functional component containing some Markup language for your homepage, styled with Bootstrap classes.

      With your Home component in place, you will now set up routing using React Router. Create a routes directory in the app/javascript directory:

      • mkdir ~/rails_react_recipe/app/javascript/routes

      The routes directory will contain a few routes with their corresponding components. Whenever any specified route is loaded, it will render its corresponding component to the browser.

      In the routes directory, create an Index.jsx file:

      • nano ~/rails_react_recipe/app/javascript/routes/Index.jsx

      Add the following code to it:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
          </Switch>
        </Router>
      );
      

      In this Index.jsx route file, you imported a couple of modules: the React module that allows us to use React, and the BrowserRouter, Route, and Switch modules from React Router, which together help us navigate from one route to another. Lastly, you imported your Home component, which will be rendered whenever a request matches the root (/) route. Whenever you want to add more pages to your application, all you need to do is declare a route in this file and match it to the component you want to render for that page.

      Save and exit the file.

      You have now successfully set up routing using React Router. For React to be aware of the available routes and use them, the routes have to be available at the entry point to the application. To achieve this, you will render your routes in a component that React will render in your entry file.

      Create an App.jsx file in the app/javascript/components directory:

      • nano ~/rails_react_recipe/app/javascript/components/App.jsx

      Add the following code into the App.jsx file:

      ~/rails_react_recipe/app/javascript/components/App.jsx

      import React from "react";
      import Routes from "../routes/Index";
      
      export default props => <>{Routes}</>;
      

      In the App.jsx file, you imported React and the route files you just created. You then exported a component that renders the routes within fragments. This component will be rendered at the entry point of the aplication, thereby making the routes available whenever the application is loaded.

      Now that you have your App.jsx set up, it's time to render it in your entry file. Open the entry Index.jsx file:

      • nano ~/rails_react_recipe/app/javascript/packs/Index.jsx

      Replace the code there with the following code:

      ~/rails_react_recipe/app/javascript/packs/Index.jsx

      import React from "react";
      import { render } from "react-dom";
      import 'bootstrap/dist/css/bootstrap.min.css';
      import $ from 'jquery';
      import Popper from 'popper.js';
      import 'bootstrap/dist/js/bootstrap.bundle.min';
      import App from "../components/App";
      
      document.addEventListener("DOMContentLoaded", () => {
        render(
          <App />,
          document.body.appendChild(document.createElement("div"))
        );
      });
      

      In this code snippet, you imported React, the render method from ReactDOM, Bootstrap, jQuery, Popper.js, and your App component. Using ReactDOM's render method, you rendered your App component in a div element, which was appended to the body of the page. Whenever the application is loaded, React will render the content of the App component inside the div element on the page.

      Save and exit the file.

      Finally, add some CSS styles to your homepage.

      Open up your application.css in your ~/rails_react_recipe/app/assets/stylesheets directory:

      • nano ~/rails_react_recipe/app/assets/stylesheets/application.css

      Next, replace the contents of the application.css file with the follow code:

      ~/rails_react_recipe/app/assets/stylesheets/application.css

      .bg_primary-color {
        background-color: #FFFFFF;
      }
      .primary-color {
        background-color: #FFFFFF;
      }
      .bg_secondary-color {
        background-color: #293241;
      }
      .secondary-color {
        color: #293241;
      }
      .custom-button.btn {
        background-color: #293241;
        color: #FFF;
        border: none;
      }
      .custom-button.btn:hover {
        color: #FFF !important;
        border: none;
      }
      .hero {
        width: 100vw;
        height: 50vh;
      }
      .hero img {
        object-fit: cover;
        object-position: top;
        height: 100%;
        width: 100%;
      }
      .overlay {
        height: 100%;
        width: 100%;
        opacity: 0.4;
      }
      

      This creates the framework for a hero image, or a large web banner on the front page of your website, that you will add later. Additionally, this styles the button that the user will use to enter the application.

      With your CSS styles in place, save and exit the file. Next, restart the web server for your application, then reload the application in your browser. You will see a brand new homepage:

      Homepage Style

      In this step, you configured your application so that it uses React as its frontend. In the next section, you will create models and controllers that will allow you to create, read, update, and delete recipes.

      Step 6 — Creating the Recipe Controller and Model

      Now that you have set up a React frontend for your application, in this step you'll create a Recipe model and controller. The recipe model will represent the database table that will hold information about the user's recipes while the controller will receive and handle requests to create, read, update, or delete recipes. When a user requests a recipe, the recipe controller receives this request and passes it to the recipe model, which retrieves the requested data from the database. The model then returns the recipe data as a response to the controller. Finally, this information is displayed in the browser.

      Start by creating a Recipe model by using the generate model subcommand provided by Rails and by specifying the name of the model along with its columns and data types. Run the following command in your Terminal window to create a Recipe model:

      • rails generate model Recipe name:string ingredients:text instruction:text image:string

      The preceding command instructs Rails to create a Recipe model together with a name column of type string, an ingredients and instruction column of type text, and an image column of type string. This tutorial has named the model Recipe, because by convention models in Rails use a singular name while their corresponding database tables use a plural name.

      Running the generate model command creates two files:

      • A recipe.rb file that holds all the model related logic.
      • A 20190407161357_create_recipes.rb file (the number at the beginning of the file may differ depending on the date when you run the command). This is a migration file that contains the instruction for creating the database structure.

      Next, edit the recipe model file to ensure that only valid data is saved to the database. You can achieve this by adding some database validation to your model. Open your recipe model located at app/models/recipe.rb:

      • nano ~/rails_react_recipe/app/models/recipe.rb

      Add the following highlighted lines of code to the file:

      class Recipe < ApplicationRecord
        validates :name, presence: true
        validates :ingredients, presence: true
        validates :instruction, presence: true
      end
      

      In this code, you added model validation which checks for the presence of a name, ingredients, and instruction field. Without the presence of these three fields, a recipe is invalid and won’t be saved to the database.

      Save and quit the file.

      For Rails to create the recipes table in your database, you have to run a migration, which in Rails is a way to make changes to your database programmatically. To make sure that the migration works with the database you set up, it is necessary to make changes to the 20190407161357_create_recipes.rb file.

      Open this file in your editor:

      • nano ~/rails_react_recipe/db/migrate/20190407161357_create_recipes.rb

      Add the following highlighted lines, so that the file looks like this:

      db/migrate/20190407161357_create_recipes.rb

      class CreateRecipes < ActiveRecord::Migration[5.2]
        def change
          create_table :recipes do |t|
            t.string :name, null: false
            t.text :ingredients, null: false
            t.text :instruction, null: false
            t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
            t.timestamps
          end
        end
      end
      

      This migration file contains a Ruby class with a change method, and a command to create a table called recipes along with the columns and their data types. You also updated 20190407161357_create_recipes.rb with a NOT NULL constraint on the name, ingredients, and instruction columns by adding null: false, ensuring that these columns have a value before changing the database. Finally, you added a default image URL for your image column; this could be another URL if you wanted to use a different image.

      With these changes, save and exit the file. You’re now ready to run your migration and actually create your table. In your Terminal window, run the following command:

      Here you used the database migrate command, which executes the instructions in your migration file. Once the command runs successfully, you will receive an output similar to the following:

      Output

      == 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

      With your recipe model in place, create your recipes controller and add the logic for creating, reading, and deleting recipes. In your Terminal window, run the following command:

      • rails generate controller api/v1/Recipes index create show destroy -j=false -y=false --skip-template-engine --no-helper

      In this command, you created a Recipes controller in an api/v1 directory with an index, create, show, and destroy action. The index action will handle fetching all your recipes, the create action will be responsible for creating new recipes, the show action will fetch a single recipe, and the destroy action will hold the logic for deleting a recipe.

      You also passed some flags to make the controller more lightweight, including:

      • -j=false which instructs Rails to skip generating associated JavaScript files.
      • -y=false which instructs Rails to skip generating associated stylesheet files.
      • --skip-template-engine, which instructs Rails to skip generating Rails view files, since React is handling your front-end needs.
      • --no-helper, which instructs Rails to skip generating a helper file for your controller.

      Running the command also updated your routes file with a route for each action in the Recipes controller. To use these routes, make changes to your config/routes.rb file.

      Open up the routes file in your text editor:

      • nano ~/rails_react_recipe/config/routes.rb

      Once it is open, update it to look like the following code, altering or adding the highlighted lines:

      ~/rails_react_recipe/config/routes.rb

      Rails.application.routes.draw do
        namespace :api do
          namespace :v1 do
            get 'recipes/index'
            post 'recipes/create'
            get '/show/:id', to: 'recipes#show'
            delete '/destroy/:id', to: 'recipes#destroy'
          end
        end
        root 'homepage#index'
        get '/*path' => 'homepage#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      In this route file, you modified the HTTP verb of the create and destroy routes so that it can post and delete data. You also modified the routes for the show and destroy action by adding an :id parameter into the route. :id will hold the identification number of the recipe you want to read or delete.

      You also added a catch all route with get '/*path' that will direct any other request that doesn’t match the existing routes to the index action of the homepage controller. This way, the routing on the frontend will handle requests that are not related to creating, reading, or deleting recipes.

      Save and exit the file.

      To see a list of routes available in your application, run the following command in your Terminal window:

      Running this command displays a list of URI patterns, verbs, and matching controllers or actions for your project.

      Next, add the logic for getting all recipes at once. Rails uses the ActiveRecord library to handle database-related tasks like this. ActiveRecord connects classes to relational database tables and provides a rich API for working with them.

      To get all recipes, you'll use ActiveRecord to query the recipes table and fetch all the recipes that exist in the database.

      Open the recipes_controller.rb file with the following command:

      • nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      Add the following highlighted lines of code to the recipes controller:

      ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      class Api::V1::RecipesController < ApplicationController
        def index
          recipe = Recipe.all.order(created_at: :desc)
          render json: recipe
        end
      
        def create
        end
      
        def show
        end
      
        def destroy
        end
      end
      

      In your index action, using the all method provided by ActiveRecord, you get all the recipes in your database. Using the order method, you order them in descending order by their created date. This way, you have the newest recipes first. Lastly, you send your list of recipes as a JSON response with render.

      Next, add the logic for creating new recipes. As with fetching all recipes, you'll rely on ActiveRecord to validate and save the provided recipe details. Update your recipe controller with the following highlighted lines of code:

      ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      class Api::V1::RecipesController < ApplicationController
        def index
          recipe = Recipe.all.order(created_at: :desc)
          render json: recipe
        end
      
        def create
          recipe = Recipe.create!(recipe_params)
          if recipe
            render json: recipe
          else
            render json: recipe.errors
          end
        end
      
        def show
        end
      
        def destroy
        end
      
        private
      
        def recipe_params
          params.permit(:name, :image, :ingredients, :instruction)
        end
      end
      

      In the create action, you use ActiveRecord’s create method to create a new recipe. The create method has the ability to assign all controller parameters provided into the model at once. This makes it easy to create records, but also opens the possibility of malicious use. This can be prevented by using a feature provided by Rails known as strong parameters. This way, parameters can’t be assigned unless they’ve been whitelisted. In your code, you passed a recipe_params parameter to the create method. The recipe_params is a private method where you whitelisted your controller parameters to prevent wrong or malicious content from getting into your database. In this case, you are permitting a name, image, ingredients, and instruction parameter for valid use of the create method.

      Your recipe controller can now read and create recipes. All that’s left is the logic for reading and deleting a single recipe. Update your recipes controller with the following code:

      ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      class Api::V1::RecipesController < ApplicationController
        def index
          recipe = Recipe.all.order(created_at: :desc)
          render json: recipe
        end
      
        def create
          recipe = Recipe.create!(recipe_params)
          if recipe
            render json: recipe
          else
            render json: recipe.errors
          end
        end
      
        def show
          if recipe
            render json: recipe
          else
            render json: recipe.errors
          end
        end
      
        def destroy
          recipe&.destroy
          render json: { message: 'Recipe deleted!' }
        end
      
        private
      
        def recipe_params
          params.permit(:name, :image, :ingredients, :instruction)
        end
      
        def recipe
          @recipe ||= Recipe.find(params[:id])
        end
      end
      

      In the new lines of code, you created a private recipe method. The recipe method uses ActiveRecord’s find method to find a recipe whose idmatches the id provided in the params and assigns it to an instance variable @recipe. In the show action, you checked if a recipe is returned by the recipe method and sent it as a JSON response, or sent an error if it was not.

      In the destroy action, you did something similar using Ruby’s safe navigation operator &., which avoids nil errors when calling a method. This let's you delete a recipe only if it exists, then send a message as a response.

      Now that you have finished making these changes to recipes_controller.rb, save the file and exit your text editor.

      In this step, you created a model and controller for your recipes. You’ve written all the logic needed to work with recipes on the backend. In the next section, you'll create components to view your recipes.

      Step 7 — Viewing Recipes

      In this section, you will create components for viewing recipes. First you’ll create a page where you can view all existing recipes, and then another to view individual recipes.

      You’ll start off by creating a page to view all recipes. However, before you can do this, you need recipes to work with, since your database is currently empty. Rails affords us the opportunity to create seed data for your application.

      Open up the seed file seeds.rb to edit:

      • nano ~/rails_react_recipe/db/seeds.rb

      Replace the contents of this seed file with the following code:

      ~/rails_react_recipe/db/seeds.rb

      9.times do |i|
        Recipe.create(
          name: "Recipe #{i + 1}",
          ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
          instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
        )
      end
      

      In this code, you are using a loop to instruct Rails to create nine recipes with a name, ingredients, and instruction. Save and exit the file.

      To seed the database with this data, run the following command in your Terminal window:

      Running this command adds nine recipes to your database. Now you can fetch them and render them on the frontend.

      The component to view all recipes will make a HTTP request to the index action in the RecipesController to get a list of all recipes. These recipes will then be displayed in cards on the page.

      Create a Recipes.jsx file in the app/javascript/components directory:

      • nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      Once the file is open, import the React and Link modules into it by adding the following lines:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      

      Next, create a Recipes class that extends the React.Component class. Add the following highlighted code to create a React component that extends React.Component:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipes extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            recipes: []
          };
        }
      
      }
      export default Recipes;
      

      Inside the constructor, we are initializing a state object that holds the state of your recipes, which on initialization is an empty array ([]).

      Next, add a componentDidMount method in the Recipe class. The componentDidMount method is a React lifecycle method that is called immediately after a component is mounted. In this lifecycle method, you will make a call to fetch all your recipes. To do this, add the following lines:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipes extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            recipes: []
          };
        }
      
        componentDidMount() {
            const url = "/api/v1/recipes/index";
            fetch(url)
              .then(response => {
                if (response.ok) {
                  return response.json();
                }
                throw new Error("Network response was not ok.");
              })
              .then(response => this.setState({ recipes: response }))
              .catch(() => this.props.history.push("/"));
        }
      
      }
      export default Recipes;
      

      In your componentDidMount method, you made an HTTP call to fetch all recipes using the Fetch API. If the response is successful, the application saves the array of recipes to the recipe state. If there’s an error, it will redirect the user to the homepage.

      Finally, add a render method in the Recipe class. The render method holds the React elements that will be evaluated and displayed on the browser page when a component is rendered. In this case, the render method will render cards of recipes from the component state. Add the following highlighted lines to Recipes.jsx:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipes extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            recipes: []
          };
        }
      
        componentDidMount() {
          const url = "/api/v1/recipes/index";
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipes: response }))
            .catch(() => this.props.history.push("/"));
        }
        render() {
          const { recipes } = this.state;
          const allRecipes = recipes.map((recipe, index) => (
            <div key={index} className="col-md-6 col-lg-4">
              <div className="card mb-4">
                <img
                  src={recipe.image}
                  className="card-img-top"
                  alt={`${recipe.name} image`}
                />
                <div className="card-body">
                  <h5 className="card-title">{recipe.name}</h5>
                  <Link to={`/recipe/${recipe.id}`} className="btn custom-button">
                    View Recipe
                  </Link>
                </div>
              </div>
            </div>
          ));
          const noRecipe = (
            <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
              <h4>
                No recipes yet. Why not <Link to="/new_recipe">create one</Link>
              </h4>
            </div>
          );
      
          return (
            <>
              <section className="jumbotron jumbotron-fluid text-center">
                <div className="container py-5">
                  <h1 className="display-4">Recipes for every occasion</h1>
                  <p className="lead text-muted">
                    We’ve pulled together our most popular recipes, our latest
                    additions, and our editor’s picks, so there’s sure to be something
                    tempting for you to try.
                  </p>
                </div>
              </section>
              <div className="py-5">
                <main className="container">
                  <div className="text-right mb-3">
                    <Link to="/recipe" className="btn custom-button">
                      Create New Recipe
                    </Link>
                  </div>
                  <div className="row">
                    {recipes.length > 0 ? allRecipes : noRecipe}
                  </div>
                  <Link to="/" className="btn btn-link">
                    Home
                  </Link>
                </main>
              </div>
            </>
          );
        }
      }
      export default Recipes;
      

      Save and exit Recipes.jsx.

      Now that you have created a component to display all the recipes, the next step is to create a route for it. Open the front-end route file located at app/javascript/routes/Index.jsx:

      • nano app/javascript/routes/Index.jsx

      Add the following highlighted lines to the file:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      import Recipes from "../components/Recipes";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/recipes" exact component={Recipes} />
          </Switch>
        </Router>
      );
      

      Save and exit the file.

      At this point, it's a good idea to verify that your code is working correctly. As you did before, use the following command to start your server:

      • rails s --binding=127.0.0.1

      Go ahead and open the app in your browser. By clicking the View Recipe button on the homepage, you will see a display with your seed recipes:

      Recipes Page

      Use CTRL+C in your Terminal window to stop the server and get your prompt back.

      Now that you can view all the recipes that exist in your application, it's time to create a second component to view individual recipes. Create a Recipe.jsx file in the app/javascript/components directory:

      • nano app/javascript/components/Recipe.jsx

      As with the Recipes component, import the React and Link modules by adding the following lines:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      

      Next create a Recipe class that extends React.Component class by adding the highlighted lines of code:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      }
      
      export default Recipe;
      

      Like with your Recipes component, in the constructor, you initialized a state object that holds the state of a recipe. You also bound an addHtmlEntities method to this so it can be accessible within the component. The addHtmlEntities method will be used to replace character entities with HTML entities in the component.

      In order to find a particular recipe, your application needs the id of the recipe. This means your Recipe component expects an id param. You can access this via the props passed into the component.

      Next, add a componentDidMount method where you will access the id param from the match key of the props object. Once you get the id, you will then make an HTTP request to fetch the recipe. Add the following highlighted lines to your file:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
      
          const url = `/api/v1/show/${id}`;
      
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
      }
      
      export default Recipe;
      

      In the componentDidMount method, using object destructuring, you get the id param from the props object, then using the Fetch API, you make a HTTP request to fetch the recipe that owns the id and save it to the component state using the setState method. If the recipe does not exist, the app redirects the user to the recipes page.

      Now add the addHtmlEntities method, which takes a string and replaces all escaped opening and closing brackets with their HTML entities. This will help us convert whatever escaped character was saved in your recipe instruction:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
      
          const url = `/api/v1/show/${id}`;
      
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      }
      
      export default Recipe;
      

      Finally, add a render method that gets the recipe from the state and renders it on the page. To do this, add the following highlighted lines:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
      
          const url = `/api/v1/show/${id}`;
      
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      
        render() {
          const { recipe } = this.state;
          let ingredientList = "No ingredients available";
      
          if (recipe.ingredients.length > 0) {
            ingredientList = recipe.ingredients
              .split(",")
              .map((ingredient, index) => (
                <li key={index} className="list-group-item">
                  {ingredient}
                </li>
              ));
          }
          const recipeInstruction = this.addHtmlEntities(recipe.instruction);
      
          return (
            <div className="">
              <div className="hero position-relative d-flex align-items-center justify-content-center">
                <img
                  src={recipe.image}
                  alt={`${recipe.name} image`}
                  className="img-fluid position-absolute"
                />
                <div className="overlay bg-dark position-absolute" />
                <h1 className="display-4 position-relative text-white">
                  {recipe.name}
                </h1>
              </div>
              <div className="container py-5">
                <div className="row">
                  <div className="col-sm-12 col-lg-3">
                    <ul className="list-group">
                      <h5 className="mb-2">Ingredients</h5>
                      {ingredientList}
                    </ul>
                  </div>
                  <div className="col-sm-12 col-lg-7">
                    <h5 className="mb-2">Preparation Instructions</h5>
                    <div
                      dangerouslySetInnerHTML={{
                        __html: `${recipeInstruction}`
                      }}
                    />
                  </div>
                  <div className="col-sm-12 col-lg-2">
                    <button type="button" className="btn btn-danger">
                      Delete Recipe
                    </button>
                  </div>
                </div>
                <Link to="/recipes" className="btn btn-link">
                  Back to recipes
                </Link>
              </div>
            </div>
          );
        }
      
      }
      
      export default Recipe;
      

      In this render method, you split your comma separated ingredients into an array and mapped over it, creating a list of ingredients. If there are no ingredients, the app displays a message that says No ingredients available. It also displays the recipe image as a hero image, adds a delete recipe button next to the recipe instruction, and adds a button that links back to the recipes page.

      Save and exit the file.

      To view the Recipe component on a page, add it to your routes file. Open your route file to edit:

      • nano app/javascript/routes/Index.jsx

      Now, add the following highlighted lines to the file:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      import Recipes from "../components/Recipes";
      import Recipe from "../components/Recipe";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/recipes" exact component={Recipes} />
            <Route path="/recipe/:id" exact component={Recipe} />
          </Switch>
        </Router>
      );
      

      In this route file, you imported your Recipe component and added a route for it. Its route has an :id param that will be replaced by the id of the recipe you want to view.

      Use the rails s command to start your server again, then visit http://localhost:3000 in your browser. Click the View Recipes button to navigate to the recipes page. On the recipes page, view any recipe by clicking its View Recipe button. You will be greeted with a page populated with the data from your database:

      Single Recipe Page

      In this section, you added nine recipes to your database and created components to view these recipes, both individually and as a collection. In the next section, you will add a component to create recipes.

      Step 8 — Creating Recipes

      The next step to having a usable food recipe application is the ability to create new recipes. In this step, you will create a component for creating recipes. This component will contain a form for collecting the required recipe details from the user and will make a request to the create action in the Recipe controller to save the recipe data.

      Create a NewRecipe.jsx file in the app/javascript/components directory:

      • nano app/javascript/components/NewRecipe.jsx

      In the new file, import the React and Link modules you have used so far in other components:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      

      Next create a NewRecipe class that extends React.Component class. Add the following highlighted code to create a React component that extends react.Component:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      }
      
      export default NewRecipe;
      

      In the NewRecipe component’s constructor, you initialized your state object with empty name, ingredients, and instruction fields. These are the fields you need to create a valid recipe. You also have three methods; onChange, onSubmit, and stripHtmlEntities, which you bound to this. These methods will handle updating the state, form submissions, and converting special characters (like <) into their escaped/encoded values (like &lt;), respectively.

      Next, create the stripHtmlEntities method itself by adding the highlighted lines to the NewRecipe component:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      
        stripHtmlEntities(str) {
          return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
        }
      
      }
      
      export default NewRecipe;
      

      In the stripHtmlEntities method, you’re replacing the < and > characters with their escaped value. This way you’re not storing raw HTML in your database.

      Next add the onChange and onSubmit methods to the NewRecipe component to handle editing and submission of the form:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      
        stripHtmlEntities(str) {
          return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
        }
      
        onChange(event) {
          this.setState({ [event.target.name]: event.target.value });
        }
      
        onSubmit(event) {
          event.preventDefault();
          const url = "/api/v1/recipes/create";
          const { name, ingredients, instruction } = this.state;
      
          if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
            return;
      
          const body = {
            name,
            ingredients,
            instruction: instruction.replace(/n/g, "<br> <br>")
          };
      
          const token = document.querySelector('meta[name="csrf-token"]').content;
          fetch(url, {
            method: "POST",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            },
            body: JSON.stringify(body)
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.props.history.push(`/recipe/${response.id}`))
            .catch(error => console.log(error.message));
        }
      
      }
      
      export default NewRecipe;
      

      In the onChange method, you used the ES6 computed property names to set the value of every user input to its corresponding key in your state. In the onSubmit method, you checked that none of the required inputs are empty. You then build an object that contains the parameters required by the recipe controller to create a new recipe. Using regular expression, you replace every new line character in the instruction with a break tag, so you can retain the text format entered by the user.

      To protect against Cross-Site Request Forgery (CSRF) attacks, Rails attaches a CSRF security token to the HTML document. This token is required whenever a non-GET request is made. With the token constant in the preceding code, your application verifies the token on the server and throws an exception if the security token doesn't match what is expected. In the onSubmit method, the application retrieves the CSRF token embedded in your HTML document by Rails and makes a HTTP request with a JSON string. If the recipe is successfully created, the application redirects the user to the recipe page where they can view their newly created recipe.

      Lastly, add a render method that renders a form for the user to enter the details for the recipe the user wishes to create:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      
        stripHtmlEntities(str) {
          return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
        }
      
        onChange(event) {
          this.setState({ [event.target.name]: event.target.value });
        }
      
        onSubmit(event) {
          event.preventDefault();
          const url = "/api/v1/recipes/create";
          const { name, ingredients, instruction } = this.state;
      
          if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
            return;
      
          const body = {
            name,
            ingredients,
            instruction: instruction.replace(/n/g, "<br> <br>")
          };
      
          const token = document.querySelector('meta[name="csrf-token"]').content;
          fetch(url, {
            method: "POST",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            },
            body: JSON.stringify(body)
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.props.history.push(`/recipe/${response.id}`))
            .catch(error => console.log(error.message));
        }
      
        render() {
          return (
            <div className="container mt-5">
              <div className="row">
                <div className="col-sm-12 col-lg-6 offset-lg-3">
                  <h1 className="font-weight-normal mb-5">
                    Add a new recipe to our awesome recipe collection.
                  </h1>
                  <form onSubmit={this.onSubmit}>
                    <div className="form-group">
                      <label htmlFor="recipeName">Recipe name</label>
                      <input
                        type="text"
                        name="name"
                        id="recipeName"
                        className="form-control"
                        required
                        onChange={this.onChange}
                      />
                    </div>
                    <div className="form-group">
                      <label htmlFor="recipeIngredients">Ingredients</label>
                      <input
                        type="text"
                        name="ingredients"
                        id="recipeIngredients"
                        className="form-control"
                        required
                        onChange={this.onChange}
                      />
                      <small id="ingredientsHelp" className="form-text text-muted">
                        Separate each ingredient with a comma.
                      </small>
                    </div>
                    <label htmlFor="instruction">Preparation Instructions</label>
                    <textarea
                      className="form-control"
                      id="instruction"
                      name="instruction"
                      rows="5"
                      required
                      onChange={this.onChange}
                    />
                    <button type="submit" className="btn custom-button mt-3">
                      Create Recipe
                    </button>
                    <Link to="/recipes" className="btn btn-link mt-3">
                      Back to recipes
                    </Link>
                  </form>
                </div>
              </div>
            </div>
          );
        }
      
      }
      
      export default NewRecipe;
      

      In the render method, you have a form that contains three input fields; one for the recipeName, recipeIngredients, and instruction. Each input field has an onChange event handler that calls the onChange method. Also, there's an onSubmit event handler on the submit button that calls the onSubmit method which then submits the form data.

      Save and exit the file.

      To access this component in the browser, update your route file with its route:

      • nano app/javascript/routes/Index.jsx

      Update your route file to include these highlighted lines:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      import Recipes from "../components/Recipes";
      import Recipe from "../components/Recipe";
      import NewRecipe from "../components/NewRecipe";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/recipes" exact component={Recipes} />
            <Route path="/recipe/:id" exact component={Recipe} />
            <Route path="/recipe" exact component={NewRecipe} />
          </Switch>
        </Router>
      );
      

      With the route in place, save and exit your file. Restart your development server and visit http://localhost:3000 in your browser. Navigate to the recipes page and click the Create New Recipe button. You will find a page with a form to add recipes to your database:

      Create Recipe Page

      Enter the required recipe details and click the Create Recipe button; you will see the newly created recipe on the page.

      In this step, you brought your food recipe application to life by adding the ability to create recipes. In the next step, you’ll add the functionality to delete recipes.

      Step 9 — Deleting Recipes

      In this section, you will modify your Recipe component to be able to delete recipes.

      When you click the delete button on the recipe page, the application will send a request to delete a recipe from the database. To do this, open up your Recipe.jsx file:

      • nano app/javascript/components/Recipe.jsx

      In the constructor of the Recipe component, bind this to the deleteRecipe method:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
          this.deleteRecipe = this.deleteRecipe.bind(this);
        }
      ...
      

      Now add a deleteRecipe method to the Recipe component:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
          this.deleteRecipe = this.deleteRecipe.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/show/${id}`;
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      
        deleteRecipe() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/destroy/${id}`;
          const token = document.querySelector('meta[name="csrf-token"]').content;
      
          fetch(url, {
            method: "DELETE",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            }
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(() => this.props.history.push("/recipes"))
            .catch(error => console.log(error.message));
        }
      
        render() {
          const { recipe } = this.state;
          let ingredientList = "No ingredients available";
      ... 
      

      In the deleteRecipe method, you get the id of the recipe to be deleted, then build your url and grab the CSRF token. Next, you make a DELETE request to the Recipes controller to delete the recipe. If the recipe is successfully deleted, the application redirects the user to the recipes page.

      To run the code in the deleteRecipe method whenever the delete button is clicked, pass it as the click event handler to the button. Add an onClick event to the delete button in the render method:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      ...
      return (
        <div className="">
          <div className="hero position-relative d-flex align-items-center justify-content-center">
            <img
              src={recipe.image}
              alt={`${recipe.name} image`}
              className="img-fluid position-absolute"
            />
            <div className="overlay bg-dark position-absolute" />
            <h1 className="display-4 position-relative text-white">
              {recipe.name}
            </h1>
          </div>
          <div className="container py-5">
            <div className="row">
              <div className="col-sm-12 col-lg-3">
                <ul className="list-group">
                  <h5 className="mb-2">Ingredients</h5>
                  {ingredientList}
                </ul>
              </div>
              <div className="col-sm-12 col-lg-7">
                <h5 className="mb-2">Preparation Instructions</h5>
                <div
                  dangerouslySetInnerHTML={{
                    __html: `${recipeInstruction}`
                  }}
                />
              </div>
              <div className="col-sm-12 col-lg-2">
                <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
                  Delete Recipe
                </button>
              </div>
            </div>
            <Link to="/recipes" className="btn btn-link">
              Back to recipes
            </Link>
          </div>
        </div>
      );
      ...
      

      At this point in the tutorial, your complete Recipe.jsx file will look like this:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
          this.deleteRecipe = this.deleteRecipe.bind(this);
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/show/${id}`;
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        deleteRecipe() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/destroy/${id}`;
          const token = document.querySelector('meta[name="csrf-token"]').content;
          fetch(url, {
            method: "DELETE",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            }
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(() => this.props.history.push("/recipes"))
            .catch(error => console.log(error.message));
        }
      
        render() {
          const { recipe } = this.state;
          let ingredientList = "No ingredients available";
          if (recipe.ingredients.length > 0) {
            ingredientList = recipe.ingredients
              .split(",")
              .map((ingredient, index) => (
                <li key={index} className="list-group-item">
                  {ingredient}
                </li>
              ));
          }
      
          const recipeInstruction = this.addHtmlEntities(recipe.instruction);
      
          return (
            <div className="">
              <div className="hero position-relative d-flex align-items-center justify-content-center">
                <img
                  src={recipe.image}
                  alt={`${recipe.name} image`}
                  className="img-fluid position-absolute"
                />
                <div className="overlay bg-dark position-absolute" />
                <h1 className="display-4 position-relative text-white">
                  {recipe.name}
                </h1>
              </div>
              <div className="container py-5">
                <div className="row">
                  <div className="col-sm-12 col-lg-3">
                    <ul className="list-group">
                      <h5 className="mb-2">Ingredients</h5>
                      {ingredientList}
                    </ul>
                  </div>
                  <div className="col-sm-12 col-lg-7">
                    <h5 className="mb-2">Preparation Instructions</h5>
                    <div
                      dangerouslySetInnerHTML={{
                        __html: `${recipeInstruction}`
                      }}
                    />
                  </div>
                  <div className="col-sm-12 col-lg-2">
                    <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
                      Delete Recipe
                    </button>
                  </div>
                </div>
                <Link to="/recipes" className="btn btn-link">
                  Back to recipes
                </Link>
              </div>
            </div>
          );
        }
      }
      
      export default Recipe;
      

      Save and exit the file.

      Restart the application server and navigate to the homepage. Click the View Recipes button to view all existing recipes, view any individual recipe, and click the Delete Recipe button on the page to delete the article. You will be redirected to the recipes page, and the deleted recipe will no longer exists.

      With the delete button working, you now have a fully functional recipe application!

      Conclusion

      In this tutorial, you created a food recipe application with Ruby on Rails and a React frontend, using PostgreSQL as your database and Bootstrap for styling. If you'd like to run through more Ruby on Rails content, take a look at our Securing Communications in a Three-tier Rails Application Using SSH Tunnels tutorial, or head to our How To Code in Ruby series to refresh your Ruby skills. To dive deeper into React, try out our How To Display Data from the DigitalOcean API with React article.



      Source link