Socket
Socket
Sign inDemoInstall

next-on-rails

Package Overview
Dependencies
17
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    next-on-rails

Utility to connect Rails API backend to Next.js frontend


Version published
Weekly downloads
0
decreased by-100%
Maintainers
1
Install size
3.26 MB
Created
Weekly downloads
 

Readme

Source

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 Devise for you with Devise Token Auth for authentication.

The NPM package provides a set of React hooks and utility to interact with backend. Basically it allows 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?

  1. Generation of the User model for authentication with Devise
  2. Add UserSerializer in app/serializers/user_serializer.rb and CurrentUserSerializer in app/serializers/current_user_serializer.rb
  3. Add Devise config file in config/initializers/devise.rb
  4. Add Devise Token Auth config file in config/initializers/devise_token_auth.rb
  5. Add CORS configuration in config/initializers/cors.rb
  6. Add Devise routes in config/routes.rb
  7. Install a new ApplicationController that extends from NextOnRails::ApplicationController
  8. Add Foreman gem gem and Procfile
  9. Setup letter_opener gem for development environment
  10. 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)

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 enhancements:

  • 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 have 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, 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 destroy. Now lets 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. 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 returns an async function that returns 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 used the function getInitialResource that is similar to getInitialResources but it calls 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

This function is like getInitialResources or getInitialResource but take a list of resource names and combine the property to pass to the React component. For example if we want to prefetch a post and a list of authors (in an edit page we can edit the post author for example) we can use this function:

PostsEdit.getInitialProps = composeGetInitialResources('post', 'authors')

For this function the plurality of names are very important: singular names will be request the show action, plural names the index action. With this function is not possible to pass HTTP extra params. If you need to pass extra HTTP params you should call getInitialResources or getInitialResource and compose yourself the results.

composeGetInitialResources performs a single HTTP request.

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 hook to know when the frontend is loading data from backend. Is quite simple:

import { useLoading } from 'next-on-rails/utils'

const Loader = props => {
  const loading = useLoading()

  if (loading) {
    return <img src='loader.gif' alt="loading" />
  } else {
    return null
  }
}

Requester

To perform HTTP request to backend you should use the requester.

import requester from 'next-on-rails/requester'

requester.get('/posts')
  .then(function (response) {
    // handle success
    console.log(response)
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })

It is a wrapper over Axios and it works in the same way. You can also access to axios object with requester.axios.

So why the requester? Because is handle for you the Devise Token Auth authentication token, adding the token in the request header and change it at every request based on the previous response. The requester save the token on cookies.

It also normalize the jsonapi response. For example if reponse json is

{
  "data": [
    {
      "id": "1",
      "type": "post",
      "attributes": {
        "id": 1,
        "title": "My first blog post",
        "body": "Amazing",
        "public": true
      }
    },
    {
      "id": "2",
      "type": "post",
      "attributes": {
        "id": 2,
        "title": "My second blog post",
        "body": "Wow",
        "public": false
      }
    }
  ]
}

it will be normalized in:

[
  {
    "id": 1,
    "title": "My first blog post",
    "body": "Amazing",
    "public": true
  },
  {
    "id": 2,
    "title": "My second blog post",
    "body": "Wow",
    "public": false
  }
]

Config

NOR try to follow the Rails conventions, but you can customize something editing the configuration file ./frontend/next-on-rails.config.js. The default config is this:

{
  baseURL: 'http://localhost:3000',
  flashTimeout: 5000,
  devisePaths: {
    validateToken: '/auth/validate_token',
    signIn: '/auth/sign_in',
    signOut: '/auth/sign_out',
    signUp: '/auth',
    passwordReset: '/auth/password',
    passwordChange: '/auth/password',
    unlock: '/auth/unlock'
  },
  deviseFormInputNames: {
    email: 'email',
    password: 'password',
    passwordConfirmation: 'password_confirmation'
  }
}

You can customize the resource names and actions:

resources: {
  posts: {
    routes: {
      index: { method: 'get', url: '/all_posts.json' }
    },
    name: {
      singular: 'post',
      plural: 'posts'
    }
  }
}

Add custom action

Until now we have always talk about the standard CRUD action. But how to do if we need manage a custom controller action?

We can add in our post controller a new action:

def like
  @post = Post.find(params[:id])
  @post.increment!(:like_counter)
  render json: PostSerializer.new(@post)
end

And in routes.rb

resources :posts do
  member do
    patch :like
  end
end

Our frontend page component should be:

import { getInitialResource, useResources } from 'next-on-rails/resources'
import requester from 'next-on-rails/requester'

const PostsShow = props => {
  const [, post, , dispatch] = useResources('posts', [], props.post)

  const like = (event) => {
    requester.patch(`/posts/${post.id}/like`)
      .then(response => {
        dispatch({ type: 'set', resource: response.data })
      })
      .catch(console.log)
  }

  return (
      <div className="container pt-4">
        <h1 className="mb-5">Post #{post.id}</h1>

        <p>{post.body}</p>

        <div>
          <button onClick={like}>Like</button>
          {post.like_counter} people like it
        </div>
    </div>
  )
}

PostsShow.getInitialProps = getInitialResource('post')

export default PostsShow

We have create a custom function like that performs the posts#like action and on success we replace the current post state object with the new post object returned from the HTTP request. In this way the like_counter will be updated and React re-render the incremented counter.

Keywords

FAQs

Last updated on 02 Aug 2019

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc