Laravel + React Forum

Posting Threads

This is the first lesson in a series on how to create a forum using Laravel and React. If you haven't read the introduction where we plan out the forum you should check it out first!

To get started with a forum, the first thing we need is the ability to create accounts, login, and create threads. Thankfully, Laravel comes with the Artisan make:auth command, which we'll be taking advantage of. Since this is covered pretty thoroughly other places, like in the Laravel Authentication documentation, I won't go over it again. Just know that I simply ran the php artisan make:auth command and didn't change any of the related files.

Creating New Threads

The first goal in making a forum will be allowing users to publish threads. With the default authentication system we get a /home route and page. We'll make use of this to show users important information, like threads they've created and notifications they've received. For right now though, we'll add a sidebar with a form to create a new thread. The template is pretty simple, so replace what's in the auto-generated template with our new home page.

@extends('layouts.app')

@section('content')
<div class="container">
  <div class="row">
    <div class="col-md-5 col-md-offset-7">
      <div id="create-thread"></div>
    </div>
  </div>
</div>
@endsection

That div with an id of create-thread is where our React form will be going. Using React for something simple like a form may seem like a little bit of overkill, but it will allow us to add extra functionality as we move forward and starting out we can make use of it to submit an Ajax request instead of forcing the user to load a whole new page. As we add more components to this page, you'll see just how useful React is for all of these interactive pieces.

Before we move on to writing the actual front-end code, lets quickly go over the back-end code that will be powering the creation of new threads.

The Back-End Functionality

To actually create threads, we'll need a database migration. For right now, we need nothing more than an author_id column to reference who created the thread, as well as a title and content column. In the future we'll be adding more columns, but this is good enough to get us started. You can check out the full migration on GitHub. If you're following along, make sure to create that migration (I recommend just running php artisan make:migration create_threads_table --create=threads and editing it to match my file) and refresh your database.

Since threads will be an actual model that we'll be interacting with, we'll need a model as well. Starting out simple, the only thing I've put into the model is a $fillable property so that we avoid mass-assignment exceptions and an Eloquent relationship for accessing the threads author (and of course I added the matching relationship in the User class as well, you can view that diff here).

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Thread extends Model
{
    protected $fillable = ['author_id', 'title', 'content'];
    public function author()
    {
        return $this->belongsTo('App\User');
    }
}

And finally, none of this would be very useful if we didn't also have a controller to handle incoming requests. You can create a blank controller with php artisan make:controller ThreadsController and then add these few functions.

This one is pretty straightforward and will simply show a single thread.

public function show(Thread $thread)
{
    return view('threads.show', compact('thread'));
}

This is where we'll actually be storing new threads. First we validate that the title and content data values are present. Then we actually create the thread with the values and also set the author_id to be the id of the currently authenticated user.

public function store(Request $request)
{
    $request->validate([
        'title' => 'required',
        'content' => 'required',
    ]);

    $thread = Thread::create([
        'author_id' => \Auth::user()->id,
        'title' => $request->title,
        'content' => $request->content
    ]);

    return response($thread, 201);
}

Now that you've seen the two useful functions, go back up to the top of the controller and add a constructor. This is simply to ensure only authenticated users can make a POST request to create a thread. Since we'll be adding more functions in the future, I decided it was best to just require authentication on everything except simply viewing a thread.

public function __construct()
{
    $this->middleware('auth')->except('show');
}

Finally, we just need some routes for creating and viewing threads and calling these controller functions. Throw these two routes into your route/web.php file to handle that.

Route::get('/threads/{thread}', 'ThreadsController@show');
Route::post('/threads', 'ThreadsController@store');

Creating Threads With React

Now that the back-end is all setup and ready to go, lets create that form component!

First, go into your resources/assets/js folder and open up app.js. In there you'll see a line that pulls in the Example component. Go ahead and change that so it pulls in a more descriptive file.

require('./components/CreateThread');

Go into that components folder and rename the Example.js file to be CreateThread.js. This will be our form component.

By default, you should see that the file imports React and ReactDOM. Since we're going to making Ajax requests from this component, we'll also require Axios (which comes configured by default in every Laravel project).

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';

First we'll update what comes in the file by default and then add in our own functions to get the functionality we need. Make sure to update the class name definition from Example to CreateThread and do the same for the ReactDOM.render at the end. We need to check if the page has the create-thread id element that we will use anywhere we want this component to load. Since a page will only ever have one of these forms, this simple check will work just fine.

export default class CreateThread extends Component {
  // Omitted for brevity right now.
}

if (document.getElementById('create-thread')) {
  ReactDOM.render(<CreateThread />, document.getElementById('create-thread'));
}

Now that the basic setup stuff is taken care of, we can work on rewriting the render method. We'll have the component always output itself as a self-contained panel. The majority of this code is just Bootstrap divs and styling. The important things to pay attention to are the onChange attributes on the input and textarea elements, and the onClick attribute on the button.

render() {
  return (
    <div className="panel panel-default">
      <div className="panel-heading">Create a New Thread</div>

      <div className="panel-body">
        <form>
          <div className="form-group">
            <label htmlFor="new-thread-title">Title</label>
            <input id="new-thread-title" className="form-control" onChange={this.changeTitle}></input>
          </div>
          <div className="form-group">
            <label htmlFor="new-thread-content">Title</label>
            <textarea id="new-thread-content" className="form-control" onChange={this.changeContent} rows="8"></textarea>
          </div>
        </form>
      </div>

      <div className="panel-footer">
        <button className="btn btn-primary" onClick={this.postThread}>
          Publish
        </button>
      </div>
    </div>
  );
}

Now that we have the render function all taken care of, we need to create those changeTitle(), changeContent(), and postThread() functions.

The title and content functions are easiest, since they'll just be updating the components state. The event parameter is automatically passed to the function by React because of how the onChange event attribute is handled. With the event object we simply select the target which will be either the input or textarea element and then select the value from that.

changeTitle(event) {
  this.setState({title: event.target.value});
}

changeContent(event) {
  this.setState({content: event.target.value});
}

The postThread() function is a bit more involved, but we'll start out with the most basic Axios call we can get away with for right now. We'll come back to this function in the future when we add some new features and options for posting threads.

postThread() {
  axios.post('/threads', {
    title: this.state.title,
    content: this.state.content
  }).then(response => {
    this.setState({title: '', content: ''});
    document.getElementById('new-thread-title').value = '';
    document.getElementById('new-thread-content').value = '';
  });
}

If you've ever worked with Axios before then this function will be pretty straight-forward. Even if you haven't used it before, I'm sure you're all smart enough to figure it out. We're simply using Axios to make a POST request to the /threads path on our site which we set up earlier to accept POST requests and send them to the ThreadsController. In that POST request we're passing the title and content data which is stored in the components state (put there by the changeTitle and changeContent functions). Axios works using promises so that after a request has been completed and a response is received, the then function gets called. Inside of that we reset the components state to have empty values and clear out the title and content inputs.

The final bit of this controller that we need is a constructor. We need to setup the initial state of the component, and also bind this to each of the functions we created. I go over why we need to doing this binding in my post on getting started with React.

constructor(props) {
  super(props);
  this.state = {
    title: '',
    content: '',
  };

  this.postThread = this.postThread.bind(this);
  this.changeTitle = this.changeTitle.bind(this);
  this.changeContent = this.changeContent.bind(this);
}

If you want to see the whole file, view it on GitHub.

That's it! We now have a component that can create threads on our forum using React and Axios. Visit your local version of the project and go to the /home path to see the form. If you try submitting it you might not see much happen, but if you check out your browser's developer tools you should be able to see a POST request going to /threads. After submitting a thread, try visiting /threads/1 and you should be able to see the first thread you created.

This lack of feedback isn't very user friendly, so in the next lesson we'll work on creating some sort of alert component to tell users that their thread was successfully created and give them a link to view it.