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.
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.