CircleCI codecov Maintainability Gem Version

ServerComponent

ServerComponent is a painless framework for React apps.

This gem is designed to be used with **npm package version of server_component.

Why?

SPAs do not always require much-highly scalable technology stack. There need alternatives for those who want to keep less code with retaining expressiveness of SPA. ServerComponent is a smart one of them.

The following image depicts how it reduces the complexity among modules. You can get more development efficiency with keeping advantages of SPA as you choose ServerComponent.

figure

Installation

Add this line to your application's Gemfile and run bundle install:

gem 'server_component'

Run this command to initialize configuration file and base classes.

$ rails generate server_component:install

🚀 Getting Started

Add a new component controller:

class Api::CounterComponentController < ServerComponent::Base
  def self.initial_state
    {
      count: 0
    }
  end

  action :increment
  def increment
    set_state do |s|
      s.count { |prev| prev + 1 }
    end
  end

  action :decrement
  def decrement
    set_state do |s|
      s.count { |prev| prev - 1 }
    end
  end
end

Add new lines into routes.rb:

component_scope :api do
  component :counter, actions: [:increment, :decrement]
end

Install server_component of NPM package in your frontend module.

Here is a sample client code (with Babel >= 7 and decorator):

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import ServerComponent, { server_component, consumer } from 'server_component';

@server_component('counter')
class CounterContainer extends Component { }

@consumer('counter')
class CounterBody extends Component {
  render() {
    const { increment, decrement, state: { count } } = this.props.counter;
    return (
      <div>
        <h1>Counter</h1>
        <button onClick={() => increment()}>increment</button>
        <button onClick={() => decrement()}>decrement</button>
        <hr />
        <div>Count: {count}</div>
      </div>
    );
  }
}

class Main extends Component {
  render() {
    return (
      <ServerComponent.Use name="api">
        <CounterContainer>
          <CounterBody />
        </CounterContainer>
      </ServerComponent.Use>
    );
  }
}

ReactDOM.render(<Main />, document.getElementById('root'));

(Note that the conventional Rails' meta tags for CSRF token are needed.)

If you don't use decorators, here's an alternative:

import ServerComponent, { connectServer, consume } from 'server_component';

const CounterContainer = connectServer(class extends Component {}, 'counter');

const CounterBody = consume(class extends Component {
  :
}, 'counter');

🎓 Usage

Write view files with Jsrb

Jsrb, a view handler that generates JavaScript, is included in this library. Because the action of the server component merely renders executable JavaScript code instead of JSON, you have to build code safely. But don't be serious! Most of state updating functions won't be much complicated. Using Jsrb, you can easily and safely construct JavaScript code.

Example

In controllers/books_component_controller.rb:

action :create
def create
  @book = Book.new(book_params)
  @book.save
  render :js
end

In views/books_component/create.js.jsrb:

# Note that set_state must take `js` as an argument.
set_state(js) do |s|
  s.total { |prev| prev + 1 }
  s.books do |books|
    books.concat id: @book.id, title: @book.title
  end
end

Fallback to conventional JSON handling

The component controller is just a Rails controller, so you can render JSON in a conventional manner. The client side handles response by Content-Type and status properly, so you don't have to reconstruct the status handling logic.

Example

In controllers/books_component_controller.rb:

action :create
def create
  @book = Book.new(book_params)
  if @book.save
    render json: { book: { id: @book.id, title: @book.title }
  else
    render json: { errors: @book.error_messages }, status: :unprocessable_entity
  end
end

In frontend:

@server_component('book')
class BookContainer extends Component {
  // method name sufixed with `Ok` automatically
  // executed after 2XX response
  createOk({ book }) {
    this.setState({
      books: this.state.books.concat(book),
    });
  }

  // method name sufixed with `Err` automatically
  // executed after 4XX response
  createErr({ errors }) {
    this.setState({ errors });
  }
}

Utility for temporary UI state

It is a common UI pattern to display the user operated resource in a particular state while waiting the server response. In such cases, you have to setState in a certain component before request. ServerComponent's controller has an utility to implement this behavior.

Example

action :create do |c|
  c.before do |s, data|
    # data is a request payload
    s.books do |book|
      book.concat title: data[:title], creating: true
    end
  end
end
def create
  @book = Book.new(book_params)
  @book.save
  set_state do |s|
    s.books do |books|
      books = books.filter { |b| !b[:creating] }
      if @book.persisted?
        books.push id: @book.id, title: @book.title
      end
      books
    end
  end
end

Uploading files

File uploading requires FormData request because its Content-Type must be multipart/form-data. You have to specify explicitly that the action should accept file if you need.

Example

action :upload do |c|
  c.accept_file!
end
def upload
  params[:file] # => ActionDispatch::Http::UploadedFile
end

🤝 Contributing

Contributions are extremely welcome on Github Issue or Pull Request!

📝 License

The gem is available as open source under the terms of the MIT License.