Using Laravel and React to bootstrap a notes taking app

Laravel is one of the most powerful frameworks out there. Since it’s built on PHP, it can easily be hosted almost anywhere and many developers already have enough knowledge to maintain or extend the functionality. I will use it for setting up a new project, using Windows, Docker and WSL2.

As a main resource, we will use the bootcamp example which is backed by the Laravel developers themselves, with the React/Inertia option.

This article is a walk-though for the steps there, with git implementation and fixing whichever issues we might encounter in the above described configuration. This article will not contain detailed coding instructions, that would be redundant. We will put emphasis on the important aspects of the project and also enhance the functionality here and there. However, you will find the code in this article’s github repository.

Assumptions and recommendations

The current article is written with the following assumptions in mind:

  • We are using Laravel v10.13.1, Vite, Tailwind;
  • The name of the project is notes;
  • WSL is installed on your Windows machine;
  • Docker and a Debian / Ubuntu instance are already set on your machine.

It is recommended that you setup the project inside the filesystem of your WSL Debian instance, especially if the project runs very slowly on your computer.

For ease of usage, one can add an alias to bash for the sail command. It can be something as simple as:

alias sail='/vendor/bin/sail'

or the laravel recommended way:

alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

You might also find yourself in the position to need some composer commands run on your debian instance (e.g. reinstall the dependencies), so it is recommended to have it already set up.

More info:

Setup the project

We will connect to WSL, run the install command and then initialize the git repository and create the first commit:

  • wsl
  • curl -s https://laravel.build/notes | bash
  • cd notes
  • git init
  • git add .
  • git commit -m "First commit"

Start the project locally with sail

Sail is Laravel’s own implementation of docker. Behind the curtains it is using docker-compose, so we can use the same command line parameters. To start the project we can use:

  • sail up -d

Useful resources:

Setup breeze and inertia/react

Once the first commit is ready, we will move to a new branch, so that we will be able to clearly see the changes we are doing.

  • git checkout -b initial-implementation
  • sail composer require laravel/breeze --dev
  • sail php artisan breeze:install react

vite hot reload issue

In the configuration we use, one might encounter difficulties with hot reload. To fix that, add the server section to the content of ./vite.config.js to:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        laravel({
            input: 'resources/js/app.jsx',
            refresh: true,
        }),
        react(),
    ],
    server: {
        hmr: {
            host: 'localhost',
        },
        watch: {
            usePolling: true
        }
    },
});

https://github.com/vitejs/vite/discussions/9155

Implement the notes functionality

We will now focus on how to create the minimal functionality for the notes, closely following the instruction from laravel bootcamp.

Create the notes model

The Note model is one central part of the application, therefore it will receive special attention in this article. The other changes will be available in article’s the github repository.

To create the initial notes files, use the following command:

  • sail php artisan make:model -mrc Note

The above will give us an empty controller, a model and a new migration. Once the migration file is created, we need to update the content of the note to store a message and the creator of the note. The new code for up method in the notes migration is:

public function up(): void
{
    Schema::create('notes', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('message');
        $table->timestamps();
    });
}

Use the following command to run the newly created migration:

  • sail php artisan migrate

Create some fake data

We will use the factories and seeds provided by laravel to create some fake data in our dev env, to have a proper visual representation of what we build and also make sure that everything runs correctly.

  • sail php artisan make:factory NoteFactory
  • sail php artisan make:seeder NoteSeeder
  • sail php artisan make:seeder UserSeeder

The first thing we will do is update the content of the NoteFactory:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Note>
 */
class NoteFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            // Given that this factory should run after the UsersFactory, we
            // already have 10 users created
            'user_id' => fake()->numberBetween(1, 10),
            'message' => fake()->sentence(5),
            'created_at' => now(),
            'updated_at' => now(),
        ];
    }
}

Then we will update the UserSeeder. The intention is to create some random users, taking into consideration that the main one we need is the user with the id 1, which will be the creator for all the notes.

Given that user_id is a foreign key in notes, we will encounter an issue when trying to truncate the table. Schema::disableForeignKeyConstraints() will help for this issue.

<?php

// UsersSeeder.php

namespace Database\Seeders;

use App\Models\User;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Schema;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Schema::disableForeignKeyConstraints();
        User::truncate();
        Schema::enableForeignKeyConstraints();


        User::factory()->create([
            'name' => 'Notes Master',
            'email' => 'test@example.com',
        ]);

        User::factory(9)->create();

    }
}

The code for the NoteSeeder will be fairly straightforward, only specifying the id for the user who will create the note, which will be different from what the factory specifies:

<?php

// NoteSeeder

namespace Database\Seeders;

use App\Models\Note;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class NoteSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Note::truncate();

        // Create 10 notes for the main user
        Note::factory(10)->create([
            'user_id' => 1
        ]);
    }
}

As far as files changes go, lastly we need to tell laravel which seeders should be run, by editing the DatabaseSeeder.php file:

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            UserSeeder::class,
            NoteSeeder::class
        ]);
    }
}

To create the fake data, we can use one of the following, the latter being that which runs the seeder after the migrations:

  • sail php artisan db:seed
  • sail php artisan migrate --seed

More information:

Create a policy for notes

From the show, update and delete functionality which follows in our project, we will take a closer look at policies. They are Laravel’s feature used to handle authorization.

  • sail php artisan make:policy NotePolicy --model=Note

The content of the file should be:

<?php

namespace App\Policies;

use App\Models\Note;
use App\Models\User;
use Illuminate\Auth\Access\Response;

class NotePolicy
{
    /**
     * Determine whether the user can view any models.
     */
    public function viewAny(User $user): bool
    {
        //
    }

    /**
     * Determine whether the user can view the model.
     */
    public function view(User $user, Note $note): bool
    {
        //
    }

    /**
     * Determine whether the user can create models.
     */
    public function create(User $user): bool
    {
        //
    }

    /**
     * Determine whether the user can update the model.
     */
    public function update(User $user, Note $note): bool
    {
        return $note->user()->is($user);
    }

    /**
     * Determine whether the user can delete the model.
     */
    public function delete(User $user, Note $note): bool
    {
        return $this->update($user, $chirp);
    }

    /**
     * Determine whether the user can restore the model.
     */
    public function restore(User $user, Note $note): bool
    {
        //
    }

    /**
     * Determine whether the user can permanently delete the model.
     */
    public function forceDelete(User $user, Note $note): bool
    {
        //
    }
}

More information:

Project customization

Up until now we have used the beaten path shown by Laravel’s bootcamp example. We did most of the things indicated there, except for the notifcation and events and the deploy section, which got us to a fairly good result.

From this point on we would like to customize the looks and functionality to make the application fit our purpose better.

Remove the Dashboard and Welcome pages

We are only interested in showing the application, so Welcome.jsx and Dashboard.jsx are not needed. We will remove them and the files which are related, but the most important change will be done to the routes file ./routes/web.php, where we will replace:

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

… simply with:

Route::get('/', function () {
    return redirect('notes');
});

We must also remember to update the example unit test in ./tests/Feature/ExampleTest.php and replace the 200 error code in the assertion with the http redirect code, which is 302.

Better cancel action when editing

When editing, “Form submission canceled because the form is not connected” will be shown in the console when the cancel button is pressed. This happens because e.preventDefault() is not called when cancel is clicked.

A quick and easy fix would be to update the cancel button functionality in Note.jsx:

<button
    className="mt-4"
    onClick={(e) => {
        e.preventDefault();
        setEditing(false);
        reset();
        clearErrors();
    }}
>
    Cancel
</button>

However, the nicer approach is to move this function outside the component.

Better look and feel

To improve the look and feel of the application, but also some small functionality changes, we will do the following:

  • Make the logout route use GET instead of POST;
  • Use the whole width of the screen for the notes;
  • Remove the header from the profile page;
  • Remove the dropdown from the main menu;
  • Add a search bar;

Useful links:

Publish the code on Github

Git and Github are some of the most popular solutions among programmers. To use them in our project, we will create a new repository on github.com and then add it as a remote to our workdir, which already uses git for versioning.

  • git checkout master
  • git remote add origin git@github.com:cristidraghici/laravel-notes.git
  • git branch -M master
  • git push -u origin master

More info:

Conclusion

Laravel remains one of the most complete solutions for a framework of which I have used so far. It has everything you need and beyond, so to quickly develop apps in PHP it’s surely the way to go. Getting familiar with the structure and how to do things right will take some effort in the beginning, but after everything will fall into place naturally.

So Laravel alone is still a good solution for building APIs fast and in a language that is familiar to many developers. Inertia, on the other hand, is not something that I am keen on using. It is a very-very nice solution and it enables the use of frontend libraries to build the app and seamlessly integrate it with the backend. But as far as frontend goes, it feels a bit unnatural to make things this way. The combination with Tailwind is one which is a very-very good choice, though. I am still a beginner with it, but I think that using tailwind helps reducing complexity within the project.

All in all, in seems like a good solution to easily bootstrap applications and get to market fast. If they turn out well, then the following steps could be a microservice architecture or even just separating the react frontend from the laravel backend.