Next On Rails
Next On Rails (aka NOR) is a Ruby gem and a NPM package that help developers
connecting Rails --api backend app to
Next.js frontend app.
The Ruby gem setup Rails --api app to serve json in the jsonapi
format using the awesome Netflix fast
jsonapi gem and configure for you
Devise with Devise Token
Auth
for authentication.
The NPM package provides a set of React
hooks and utility to interact with
backend. Basically allow developers to:
The NPM package also provides a requester
object that performs HTTP requests
to Rails backend taking care of Devise Token Auth authentication token
management.
Installation
Create a fresh Rails --api app:
rails new YOUR-APP --api
Add in the Gemfile Next On Rails
gem 'next_on_rails'
bundle
Install
rails g nor:install
This generator setup your backend and generate a new frontend app in Next.js
inside the ./frontend
directory.
What's appened?
- Generation of the
User
model for authentication with Devise - Add
UserSerializer
in app/serializers/user_serializer.rb
and CurrentUserSerializer
in app/serializers/current_user_serializer.rb
- Add Devise config file in
config/initializers/devise.rb
- Add Devise Token Auth config file in
config/initializers/devise_token_auth.rb
- Add CORS configuration in
config/initializers/cors.rb
- Add Devise routes in
config/routes.rb
- Install a new
ApplicationController
that extends from NextOnRails::ApplicationController
- Setup
letter_opener
gem for development environment - Generate a Next.js frontend app in
./frontend
directory
Now you are ready to use Next On Rails! Let's generate something:
rails g nor:scaffold post title body:text public:boolean
rails db:migrate
And run your backend and fronend with foreman start
. Rails backend runs on
port 3000, while Next.js frontend runs on port 3001 (with
express.js)
So now visit http://localhost:3001. You should see
the homepage with login and registration form. You can also visit the post
CRUD pages just generated at
http://localhost:3001/posts.
The Frontend App
The frontend app is a normal Next.js app with some enhancement:
- Configured to use SASS (see
./frontend/next.config.js
) - Bootstrap ready
- The
App
React component is not the default import App from 'next/app'
but import App from 'next-on-rails/app'
(some High Order Components has been applied) - In the
./frontend/components
there are some usefull React components:
≤Flash/>
(for display flash messages)<LoginForm/>
<RegistrationForm/>
<Input/>
, <CheckBox/>
, <Select/>
, <TextArea/>
forms
next-on-rails NPM package
Let's say that we have the resources posts, so in our backend we have the
model ./app/models/post.rb
and the controller
./app/controllers/posts_controller.rb
with the usual crud actions index,
show, create, update and delete. Now let see how we can handle this
resources from the Next.js frontend app using the next-on-rails NPM package.
First of all we need to create a /posts
page with the list of the posts. So
we add the file ./frontend/pages/posts/index.js
:
const PostsIndex = props => {
return (
<div>
<h1>Posts</h1>
<!-- content here -->
</div>
)
}
export default PostsIndex
We have just created a simple React functional component. This component will
render the entire /posts
html page. Now it would be nice if the component
was initialized with a property posts
, an array with the posts to display in
this page. To do this we need to request the posts to the Rails backend at
http://localhost:3000/posts.json
(standard
restful
route).
getInitialResources
Next On Rails can help us with the function getInitialResources
that
return an async function that return the object { <resourcesName>: arrayOfResources }
that is the getInitialProps
function that we need!! So
we can change our component:
import { getInitialResources } from 'next-on-rails/resources'
const PostsIndex = props => {
return (
<div>
<h1>Posts</h1>
<ul>
{props.posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
)
}
PostsIndex.getInitialProps = getInitialResources('posts')
export default PostsIndex
Now the React component will be initialized with the property posts
that is
the array with all posts returned by the Rails backend posts#index
endpoint.
So getInitialResources
call the index action of the controller associated at
the resources passed as first parameter ('posts'
in this example). It has
also an optional second parameter: the request parameters used to make the
request to the backend.
getInitialResource
We can do the same for the single post page. We need to create a /posts/:id
page. So we add the file ./frontend/pages/posts/show.js
. In this case we
have to add the route to express (in ./frontend/server.js
file) cause the
route /posts/:id
is dynamic:
server.get('/posts/:id(\\d+)', (req, res) => {
app.render(req, res, '/posts/show', { id: req.params.id })
})
The React component for this page will be:
import { getInitialResource } from 'next-on-rails/resources'
const PostsShow = props => {
return (
<div>
<h1>Post #{props.post.id}</h1>
<h2>{props.post.title}</h2>
<p>
{props.post.body}
</p>
</div>
)
}
PostsShow.getInitialProps = getInitialResource('post')
export default PostsShow
In this case we have used the function getInitialResource
that is similar to
getInitialResources
but call the endpoint posts#show
passing the parameter
id
retrieved from the current url. The property returned is no more an
array, but a js object with the post ({post: {id: 1, title: ..., body: ..., ...}}
).
NB: first, we have said that the Rails backend responds in jsonapi format,
but the post object is not in jsonapi format. Next On Rails normalize the
jsonapi format internally for us. So in our frontend, we have the data ready
to use!
composeGetInitialResources
...
useResources
Now let's say we want update a post. We need to create a /posts/:id/edit
page. So we add the file ./frontend/pages/posts/edit.js
and add the route to express.
The React component for this page will be:
import { getInitialResource, useResources, useResourceForm } from 'next-on-rails/resources'
import { Input, TextArea, CheckBox, HiddenIdField } from '../inputs'
const PostsEdit = props => {
const [, post, { update }] = useResources('posts', [], props.post)
const [submit, getError] = useResourceForm(update)
return (
<div>
<h1>Edit Post #{post.id}</h1>
<form onSubmit={submit}>
<HiddenIdField id={post.id} />
<Input name="title" value={post.title} error={getError('title')} />
<TextArea name="body" value={post.body} error={getError('body')} />
<CheckBox name="public" value={post.public} error={getError('public')} />
<button type="submit" className="btn btn-primary">
Save
</button>
</form>
</div>
)
}
PostsEdit.getInitialProps = getInitialResource('post')
export default PostsEdit
Ok, we have a lot to say about this component. Don't worry!
Let's start from the React hook useResources
.
The idea is that we have a state consisting of 2 elements. An array with a
collection of resources and an object with a single resource. For example the
array with all posts and an object with the current post. This 2 elements
should be bound together so if I change the resource also the element with
the same id in the resources array should change accordingly, and vice versa.
This hook returns an array with four elements:
[arrayOfResources, currentResourceObject, { actions }, dispatch]
.
The first two elements are the state, the array of resources (first one) and a
single resource (second one). The third is an object with the five functions
to perform the CRUD operations: index
, show
, create
, update
and
destroy
. The fourth is the dispatch function returned by the
useReducer
React
hook that is used internally by the useResources
hook.
The third element is an object with five function, let's see one by one:
-
function index(params)
has as its argument HTTP params and performs a
request to the resources_controller#index
action of Rails backend. For
example you can call index({ page: 2 })
to get array of posts.
Returns a promise that will be resolved with the array of resources or
rejected with the errors.
-
function show(id, params)
has as its first argument the ID of a resource
and as its second argument HTTP params. Performs a request to the
resources_controller#show
action of Rails backend. Returns a promise that
will be resolved with the object of the resource with the corresponding ID
or rejected with the errors.
-
function create(params)
has as its argument the params to create a new
resource. For example to create a new post you can call
create({ title: 'Spiderman is dead', body: '...', public: true })
.
Performs a request to the resources_controller#create
action of Rails
backend. Returns a promise that will be resolved with the object just
created or rejected with the errors.
-
function update(id, params)
has as its first argument the ID of the
resource to update and as its second argument params to update. For example
to update a post you can call
update(1, { title: 'Spiderman is alive', body: '...', public: true })
.
Performs a request to the resources_controller#update
action of Rails
backend. Returns a promise that will be resolved with the object just
updated or rejected with the errors.
-
function destroy(id)
has as its argument the ID of the resource to delete.
Performs a request to the resources_controller#destroy
action of Rails
backend. Returns a promise that will be resolved with no arguments or
rejected with the errors.
So now in our previous example we can understand the line
const [, post, { update }] = useResources('posts', [], props.post)
We want a state with a post (initialized with the property post
) and a
function to update it! For this component we don't need posts array and any
other CRUD functions.
useResourceForm
Now let's talk about the other very useful hook that NOR makes us available:
useResourceForm
. This hook take a CRUD function which we talked about
earlier as first argument, a success callback as second argument and an error
callback as third argument.
It returns an array with two functions: [submit, getError]
. The first one is
a function that is ready to pass as value of onSubmit
form tag attribute. It
collects all form data with
FormData
web
API object and then call the CRUD function (passed as first parameter) with
this form data. The resource ID can be passed as hidden field tag.
The second function getError
takes a resource field as string, like the post
attribute 'title'
, and returns a validation error message for this attribute
if there is one. The errors are modelled as a React state. So at the beginnig
there are no errors and the function will return always null
. After a failed
submit the function can return the validation error message. So if we call
getError('title')
we get null
if there is no validation errors or for
example "Title can't be blank" if there is.
So we can now understand the line in the previous example
const [submit, getError] = useResourceForm(update)
We get the submit
function to update a post and the getError
function to
display the validation error messages.
useCurrentUser
Always in our React components we can use this hook:
import { useCurrentUser } from 'next-on-rails/current-user'
const HelloUser = () => {
const { currentUser } = useCurrentUser()
return (
<p>{ currentUser ? `Hello ${currentUser.username}` : 'You are not logged' }</p>
)
}
export default HelloUser
This hook return an object with two key. currentUser
is the object with the
current user logged or null
. setCurrentUser
is a function to update the
current user.
const { currentUser, setCurrentUser } = useCurrentUser()
useFlash
We can use flash messages also in our Next.js frontend app. Here the previous example with flash messages:
import { getInitialResource, useResources, useResourceForm } from 'next-on-rails/resources'
import { useFlash } from 'next-on-rails/utils'
import { Input, TextArea, CheckBox, HiddenIdField } from '../inputs'
import Flash from '../flash'
const PostsEdit = props => {
const { setFlash } = useFlash()
const [, post, { update }] = useResources('posts', [], props.post)
const [submit, getError] = useResourceForm(update, () => {
setFlash('notice', 'Post updated successfully')
})
return (
<div>
<Flash />
<h1>Edit Post #{post.id}</h1>
<form onSubmit={submit}>
<HiddenIdField id={post.id} />
<Input name="title" value={post.title} error={getError('title')} />
<TextArea name="body" value={post.body} error={getError('body')} />
<CheckBox name="public" value={post.public} error={getError('public')} />
<button type="submit" className="btn btn-primary">
Save
</button>
</form>
</div>
)
}
PostsEdit.getInitialProps = getInitialResource('post')
export default PostsEdit
We can use the hook useFlash
from which we can get a function to set a flash
message and then we can display the component Flash
that will show the
message. You can find the Flash
component in the file
./frontend/components/flash.js
where you can customize it as you want.
useLoading
useLoading
is another utility
Requester
Devise
Config
Add custom action