What's New in Rails 7
Tags: rails, ruby
Published: 2021-12-22 12:01:21

Rails 7 is just around the corner. We don't have a confirmed release date, but it is expected to be [available before Christmas](https://twitter.com/rafaelfranca/status/1467974458015158273), 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.

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 brought a lot of baggage, was hard to understand and make any changes to, especially while maintaining upgradability.

Now, the default for new apps created with `rails new` is to use import maps through the [`importmaps-rails` gem](https://github.com/rails/importmap-rails/). 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`:

```sh
$ ./bin/importmap pin date-fns
```

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

```ruby
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 as you used to:

```js
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 as 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:

```sh
$ ./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](https://hotwired.dev/) 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](https://edgeguides.rubyonrails.org/active_record_encryption.html#setup), you can write code like this:

```rb
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:

```rb
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](https://edgeguides.rubyonrails.org/classic_to_zeitwerk_howto.html).

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

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

```rb
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:

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

will produce

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

### Ruby `debug`

The new default for debugging has changed from `byebug` to the [`debug` gem](https://edgeguides.rubyonrails.org/debugging_rails_applications.html#debugging-with-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.

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

```rb
# 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.

```rb
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](https://devcenter.heroku.com/articles/request-timeout#long-polling-and-streaming-responses). 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](https://edgeguides.rubyonrails.org/7_0_release_notes.html).
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! 

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**