What's new in Rails 7

Rails 7 is just around the corner. We don’t have a confirmed release date, but it is expected to be available before Christmas, so not very long to go. The latest version as of this post’s publication is 7.0.0.rc1, the first release candidate. Basecamp, HEY, Github, and Shopify have all been running the Rails 7 alpha in production, so we can expect even the release candidate to be pretty stable.

Rails 7 was released on December 15.

In this post, we will look at some of the new features and changes that Rails 7 will bring.

Node and Webpack Not Required

Yes, you read that right! JavaScript in Rails 7 will no longer require NodeJS or Webpack. And you can still use npm packages.

Transpiling ES6 with Babel and bundling with Webpack require a lot of setup. While Rails supported it pretty well with the Webpacker gem, this brings a lot of baggage, is hard to understand and make any changes to, especially while maintaining upgradability.

The default for new apps created with rails new is now to use import maps through the importmaps-rails gem. Instead of writing a package.json and installing dependencies with npm or yarn, you use ./bin/importmap CLI to pin (or unpin or update) dependencies.

For example, to install date-fns:

$ ./bin/importmap pin date-fns

This will add a line in config/importmap.rb like:

pin "date-fns", to: "https://ga.jspm.io/npm:date-fns@2.27.0/esm/index.js"

In your JavaScript code, you can continue using everything like you used to:

import { formatDistance, subDays } from 'date-fns'

formatDistance(subDays(new Date(), 3), new Date(), { addSuffix: true })
//=> "3 days ago"

One thing to keep in mind with this setup is that there is no transpiling between what you write and what the browser gets. For the most part, this is ok since all browsers that matter now support ES6 out of the box.

But this also means that you won’t be able to use TypeScript or jsx because they require transpilation to JS before use.

So, if you want to use React with JSX, you still have to fall back to a different setup (using webpack/rollup/esbuild).

Rails 7 can do this for you. All you need is one command with your chosen strategy:

$ ./bin/rails javascript:install:[esbuild|rollup|webpack]

Turbolinks and UJS Replaced by Turbo and Stimulus

Applications generated with Rails 7 will get Turbo and Stimulus (from Hotwire) by default, instead of Turbolinks and UJS. Hotwire is a new approach that delivers fast updates to the DOM by sending HTML over the wire.

Encryption at Database Layer

Rails 7 allows marking certain database fields as encrypted using the encrypts method on ActiveRecord::Base. This means that after an initial setup, you can write code like this:

class Message < ApplicationRecord
  encrypts :text
end

You can continue using the encrypted attributes like you would any other attribute. Rails 7 will encrypt and decrypt it automatically between the database and your application.

But this comes with a slight quirk: you cannot query the database by that field unless you pass a deterministic: true option to the encrypts method. The deterministic mode is less secure than the default non-deterministic mode, so only use it for attributes you absolutely need to query.

Asynchronous Querying

There is now a load_async method that you can use when querying data to fetch results in the background. This is especially important when you need to load several un-related queries from a controller action. You can run:

def PostsController
  def index
    @posts = Post.load_async
    @categories = Category.load_async
  end
end

This will fire both queries in the background at the same time. So, if each query takes 200ms, the total time spent fetching the data is ~200ms instead of 400ms, if they are fetched serially.

Zeitwerk Mode for Rails 7

This is a breaking change for older applications that still run the classic loader. All Rails 7 applications must use Zeitwerk mode, but the switch is pretty easy. Check out the full Zeitwerk upgrade guide.

Other Rails 7 Updates

Retry Jobs Unlimited Times

ActiveJob now allows passing :unlimited as the attempts parameter on retry_on. Rails will continue to attempt the job without any maximum number of attempts.

class MyJob < ActiveJob::Base
  retry_on(AlwaysRetryException, attempts: :unlimited)

  def perform
    raise "KABOOM"
  end
end

Named Variants

You can now name variants on ActiveStorage instead of specifying size on every access.

class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize: "100x100"
  end
end

#Call avatar.variant(:thumb) to get a thumb variant of an avatar:
<%= image_tag user.avatar.variant(:thumb) %>

Hash to HTML Attributes

There is a new tag.attributes method for use in views that translates a hash into HTML attributes:

<input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>

will produce

<input type="text" aria-label="Search">

Ruby debug

The new default for debugging has changed from byebug to the debug gem.

Instead of calling byebug, you now need to call debugger in the code to enter a debugging session.

Assert a Single Record with sole

When querying records, you can now call sole or find_sole_by (instead of first or find_by) when you want to assert that the query should only match a single record.

Product.where(["price = %?", price]).sole
# => ActiveRecord::RecordNotFound      (if no Product with given price)
# => #<Product ...>                    (if one Product with given price)
# => ActiveRecord::SoleRecordExceeded  (if more than one Product with given price)

user.api_keys.find_sole_by(key: key)
# as above

Check Presence / Absence of an Association

We can now use where.associated(:association) to check if an association is present on a record instead of joining and checking for the existence of an id.

# Before:
account.users.joins(:contact).where.not(contact_id: nil)

# After:
account.users.where.associated(:contact)

Stream Generated Files from Controller Actions

You can now use send_stream inside a controller action to start streaming a file that is being generated on the fly.

send_stream(filename: "subscribers.csv") do |stream|
  stream.write "email_address,updated_at\n"

  @subscribers.find_each do |subscriber|
    stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
  end
end

This provides an immediate (partial) response to the user so that they know something is happening and has an added benefit if you deploy on Heroku.

Since the file will start streaming immediately, Heroku will not terminate the connection. This means you don’t need to resort to background jobs to generate one-off files that take longer than 30 seconds.

Upgrading to Rails 7

As with previous versions of Rails, upgrading is simple. While we don’t have an official upgrade guide yet, the steps will remain the same:

  1. Change the Rails version number in the Gemfile (7.0.0.rc1 as of the publication date) and run bundle update.
  2. Run bundle exec rails app:update. Follow the interactive CLI and add/replace/modify the files as required.
  3. Run your tests and verify everything works as expected.

Wrap up

You can see the full list of bug fixes, features, and changes in the Rails 7 release notes. These are not comprehensive at the moment, but we can expect them to be updated soon.

If you are still running Rails 6 or lower, please note that with the final release of Rails 7, Rails 6.1 will enter the “security issues only” mode and will no longer receive bug fixes. This will also mark the EOL for Rails 5.2, as it will no longer receive any fixes.

Have fun coding!


This article was originally posted on AppSignal Blog


Published 15 Dec 2021

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter