Toggling view layouts with Kredis, Turbo Frames, and Rails
Tags: rails, ruby, webdev, tutorial
Published: 2022-03-08 02:31:25

Kredis is a new [gem](https://github.com/rails/kredis) that makes it easier to work with Redis keys in Ruby on Rails. Kredis [was added](https://github.com/rails/rails/commit/5d9ee652013e527129792016c103ee1133106276) a suggested gem for new Rails applications starting with the release of Rails 7.0 in December of 2021 and is likely to become a larger force in the Rails world in the coming years.

From [the documentation](https://github.com/rails/kredis#kredis), Kredis “encapsulates higher-level types and data structures around a single key, so you can interact with them as coherent objects rather than isolated procedural commands”.

What this means for us is that we can use Kredis to make it easier to use Redis as a data store in our application. With Kredis, it is simple to use Redis to read and write complex data structures. Kredis’ integration with ActiveRecord allows us to work with Redis data alongside existing models.

Today we will use Kredis to power a card/list view toggle for a resource’s index page, persisting the user’s view preference across requests. This tutorial will offer a gentle introduction into how Kredis works while exploring how Kredis can help us implement a common real-world UX pattern.

We will also add a bit of Turbo functionality in the form of a Turbo Frame to wrap the list of items to make applying the user’s view preference a bit more efficient.

When we are finished, our application will work like this:

![A screen recording of a user of a web application toggling a list of players between a card and a list-based layout and navigating into and back out of a player detail page.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0exkcjh3mw26q10536m4.gif)

Before we begin, this tutorial is best suited for folks with experience building simple applications with Ruby on Rails. If you have never used Rails before, this tutorial will be difficult to follow. You do not need any prior experience with Turbo or Kredis to follow along with this tutorial.

As usual, you can find the complete code for this application on [Github](https://github.com/DavidColby/kredis_view_toggle/tree/final).

Let’s start building.

## Application setup
To begin, create a new Rails 7 application from your terminal and scaffold up a `Players` resource, which we will use as the base for our Kredis-powered view toggle.

```bash
rails new kredis-players --css=tailwind
cd kredis-players
./bin/rails g scaffold Player name:string team:string
./bin/rails db:migrate
```

Note the inclusion of the `css=tailwind` option in the `rails new` command. We will use Tailwind to style our application so we can stay focused on building with Kredis instead of copy/pasting CSS.

Once the application is created and the Players resource is scaffolded, you can start up the server and build Tailwind’s CSS with `bin/dev`.

Because we are building a view toggle for the Players index page, you may also want to seed the database with a few players to save you some manual typing. Head to `db/seeds.rb` and update it:

```ruby
10.times do |n|
  Player.create(name: "Player #{n}",  team: "Dallas Mavericks")
end
```

Then seed the database from your terminal:

## Build the list and card views
Before introducing Kredis to the application, we will begin by building a list/card view toggle that allows users to swap between the two views by reading URL parameters directly in the view.

Since we used the Rails scaffold generator, we already have a simple list view for Players ready on the index page. Let’s start by updating the generated views to be a little easier to process.

Update `app/views/players/_player.html.erb` like this:

```erb
<li class="border border-gray-200 rounded">
  <div href="#" class="block hover:bg-gray-50">
    <div class="flex items-center px-4 py-4 sm:px-6">
      <div class="min-w-0 flex-1 flex items-center">
        <div class="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4">
          <div>
            <p class="text-sm font-medium"><%= player.name %></p>
          </div>
          <div class="hidden md:block">
            <div>
              <p class="text-sm text-gray-900">
                <%= player.team %>
              </p>
            </div>
          </div>
        </div>
      </div>
      <div>
        <p>
          <%= link_to "View", player, class: "text-blue-700 hover:text-blue-500" %>
        </p>
      </div>
    </div>
  </div>
</li>
```

Just regular ERB and some Tailwind classes for styling. Now update `app/views/players/index.html.erb`:

```erb
<div class="w-full">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center">
    <h1 class="font-bold text-4xl">Players</h1>
    <%= link_to 'New player', new_player_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
  </div>

  <div id="players" class="min-w-full">
    <div>
      <ul role="list" class="space-y-4">
        <%= render @players %>
      </ul>
    </div>
  </div>
</div>
```

Again, just ERB and Tailwind, nothing fancy here.

Next we will add the card view, starting with a `_card` partial. Create the new partial from your terminal:

```bash
touch app/views/players/_card.html.erb
```

And then fill in the new `_card` partial:

```erb
<li class="col-span-1 flex shadow-sm rounded-md">
  <div class="flex-1 flex items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-md">
    <div class="flex-1 px-4 py-2 text-sm">
      <p class="text-gray-900 font-medium hover:text-gray-600"><%= player.name %></p>
      <p class="text-gray-500"><%= player.team %></p>
    </div>
    <div class="px-4 py-2">
      <p><%= link_to "View", player, class: "text-blue-700 hover:text-blue-500" %></p>
    </div>
  </div>
</li>
```

With the card partial created, we can now add a simple toggle to the Players index page to switch between the list view and the card view.

Head to `app/views/players/index.html.erb` and update it:

```erb
<div class="w-full">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center">
    <h1 class="font-bold text-4xl">Players</h1>
    <%= link_to 'New player', new_player_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
  </div>

  <div id="players" class="min-w-full mt-8">
    <% if params[:view] == "card" %>
      <div class="my-4">
        <%= link_to "List view", players_path(view: "list"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
      </div>
      <ul role="list" class="grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <%= render @players %>
      </ul>
    <% else %>
      <div class="my-4">
        <%= link_to "Card view", players_path(view: "card"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
      </div>
        <ul role="list" class="space-y-4">
          <%= render collection: @players, partial: "players/card", as: :player %>
        </ul>
    <% end %>
  </div>
</div>
```

Here we add a messy but functional `if` statement that checks the value of `params[:view]`. When the view param equals card, we render the players as a grid of cards, otherwise the players are rendered as a list. Both of the view also have a link to toggle the index page to the opposite view, with `link_to players_path(view: "list"/"card")`.

This “works” as a starting point. Head to [http://localhost:3000/players](http://localhost:3000/players) and click the button to toggle between the views and the page layout changes. However, we can quickly see the limits of this `params` based approach:
1. Toggle the index page to from a list to cards
2. Click on the view link for any player
3. Click the “Back to players” link on the show page

![A screen recording of a user of a web application toggling from a list to a card view on the page. Then they navigate into and back out of a player detail page. When they leave the player detail page, the view has reset from a card view to a list view, losing the user's preference.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xectzfoap0wpwkql804h.gif)

Instead of the card layout, the Players index page switches back to the list view. What is happening here? The Players index view relies on the presence of a `view` URL parameter to determine which layout to display. Because params do not persist between requests, the layout of the Players index page will always fall back to the default list view when visiting `/players` without any URL parameters.

This is where `Kredis` comes in. Instead of using the url params to set the view of the Players index page, we can use Kredis to persist the user’s view preference between requests so that the index page retains the expected layout.

Let’s see how Kredis works.

## Using Kredis to store preferences
Kredis is a suggested gem in Rails 7, which means it is included in the default Gemfile but it is commented out. To install Kredis, first uncomment it in the Gemfile:

```bash
- # gem "kredis"
+ gem "kredis"
```

Then from your terminal:

```bash
bundle install
./bin/rails kredis:install
```

Restart your Rails application after installing the Kredis gem and running the install task to avoid errors about Kredis being undefined.

At this point, you will also need a Redis server running in your development environment. If you do not already have Redis installed and running, Mac users will find [this gist](https://gist.github.com/tomysmile/1b8a321e7c58499ef9f9441b2faa0aa8) helpful. Linux users may find [this guide](https://www.ubuntupit.com/how-to-install-and-configure-redis-on-linux-system/) helpful.

With Kredis installed and Redis running in our local environment, we can now use Kredis to store the user’s view preference instead of relying on the presence of URL parameters.

Head to `app/controllers/players_controller.rb` and update the `index` action:

```ruby
def index
  @players = Player.all
  @user_preferences = Kredis.hash('preferences')
  @user_preferences.update(view: params[:view]) if params[:view].present?
end
```

Here we are using Kredis to fetch a `preferences` key from Redis, initializing it as a [hash](https://github.com/rails/kredis/blob/20aa4f272763151d462e9c9e125fcebf2eed4a5d/lib/kredis/types/hash.rb#L3) and setting the `user_preferences` instance variable to be a new instance of a Kredis Hash.

Then, when `params[:view]` is present in the request, we update the `preferences` hash to store the value of `view`. `update` here updates the `preferences` key in Redis along with updating the `user_preferences` instance variable.

Because `@user_preferences` is an instance of a Kredis Hash, we can treat it (mostly) like a regular hash, as demonstrated by a little bit of experimenting in the console:

```bash
irb(main):012:0> preferences = Kredis.hash('preferences')
=>
#<Kredis::Types::Hash:0x00007faed43f0f90
...
irb(main):013:0> preferences.keys
  Kredis Proxy (0.3ms)  HKEYS preferences
=> []
irb(main):014:0> preferences.update(view: 'card')
  Kredis Proxy (0.2ms)  HSET preferences [{:view=>"card"}]
=> 1
irb(main):015:0> preferences.keys
  Kredis Proxy (0.3ms)  HKEYS preferences
=> ["view"]
irb(main):016:0> preferences['view']
  Kredis Proxy (0.3ms)  HGET preferences ["view"]
=> "card"
irb(main):017:0> preferences.delete('view')
  Kredis Proxy (0.3ms)  HDEL preferences ["view"]
=> 1
irb(main):018:0> preferences.keys
  Kredis Proxy (0.2ms)  HKEYS preferences
=> []
```

To use our new persistent Kredis value instead of URL parameters when rendering the index page, head back to `app/views/players/index.html.erb` and replace the existing `if` block with the below:

```erb
<% if @user_preferences[:view] == "card" %>
  <div class="my-4">
    <%= link_to "List view", players_path(view: "list"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
  </div>
  <ul role="list" class="grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
    <%= render @players, partial: "players/card", as: :player %>
  </ul>
<% else %>
  <div class="my-4">
    <%= link_to "Card view", players_path(view: "card"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
  </div>
  <ul role="list" class="space-y-4">
    <%= render @players %>
  </ul>
<% end %>
```

Here we updated the `if` condition to check `@user_preferences` instead of `params` to determine which layout to render.

With that change in place, head back to [http://localhost:3000/players](http://localhost:3000/players) and toggle between the list and card views. If all has gone well, toggling should still work. Even better, if you click on a player and then click to return back to the players index page, your list view preference will be retained.

Neat!

## Adding a Turbo Frame
Now that we do not need the URL parameter on every request, it also makes sense to think about what actually needs to be updated when the user toggles between the list and card views. The rest of the page will not change — only the list of players will.

[Turbo Frames](https://turbo.hotwired.dev/handbook/frames) give us the tools to adjust the application to only update the players section of the page when the user toggles the view, making the application feel faster and more responsive to user input.

For our application, Turbo Frames also resolve a subtle problem that you might have encountered while testing Kredis.

[Turbo Drive](https://turbo.hotwired.dev/handbook/drive) takes a snapshot of every page to speed up applications. In almost all circumstances, this caching is something we just get for free without needing to think about; however, in this case, caching our players index page creates some issues that we need to resolve.

The issue works like this:
1. Head to [http://localhost:3000/players](http://localhost:3000/players)
2. Toggle the view from list to card
3. Click on the view link for a player
4. Click on the link to go back to the players index page
5. Notice that for a brief moment, the index page renders the list view before replacing it with the card view

![A screen recording of a user of a web application toggling a list of players between a card and a list-based layout and navigating into and back out of a player detail page. When they navigate back to the list page, the conent flashes from a list layout to a card-based layout.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irnu22y82ajdpk2k8trj.gif)

This flash of content happens because Turbo Drive caches the index page before navigating from `/players` to `/players?view=card`. The next time you visit `/players`, Drive uses the original, list-view version of `/players` from the cache to "preview" the index page before updating it with the new, card-view version of the index page rendered from the server.

Turbo Frames resolve this issue because Turbo does not cache a page when navigation is scoped within a Turbo Frame. Turbo Drive’s snapshot caching only occurs when a full-page navigation occurs.

This means that with Turbo Frames users are free to toggle between list and card views on the players index page as often as they like — the content of the index page will not be cached until they navigate away from the index page entirely.

Because the content is not cached until the final navigation, the correct layout of the page gets cached and the "preview" displayed by Turbo on the next visit to the index page matches the final version, eliminating the flash of bad cached content.

Let’s see how this works in practice.

First, update `app/views/players/index.html.erb` to wrap the list of players in a Turbo Frame:

```erb
<div class="w-full">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center">
    <h1 class="font-bold text-4xl">Players</h1>
    <%= link_to 'New player', new_player_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
  </div>

  <%= turbo_frame_tag "players", class: "min-w-full mt-8" do %>
    <% if @user_preferences[:view] == "card" %>
      <div class="my-4">
        <%= link_to "List view", players_path(view: "list"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
      </div>
      <ul role="list" class="grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <%= render @players, partial: "players/card", as: :player %>
      </ul>
    <% else %>
      <div class="my-4">
        <%= link_to "Card view", players_path(view: "card"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
      </div>
        <ul role="list" class="space-y-4">
          <%= render @players %>
        </ul>
    <% end %>
  <% end %>
</div>
```

Here we removed the “players” div and replaced that div with a `<turbo-frame id="players"`, using the turbo-rails provided `turbo_frame_tag` helper method.

Now that the list of players is wrapped in a Turbo Frame, links inside of that frame will update the content of that frame instead of updating the entire page.

This means that when the user clicks on the view toggle links, Turbo will extract the content of the `players` Turbo Frame from the response and use that content to update the `players` frame while discarding the rest of the content. When a Turbo Frame request is initiated, Rails helpfully renders the response without a layout, saving a (very small amount of) work on the server.

Finish up adding Turbo Frame support by updating the links to view players in both the `card` and `player` partials like this:

```erb
<%= link_to "View", player, class: "text-blue-700 hover:text-blue-500", data: { turbo_frame: "_top" } %>
```

The addition of `data-turbo-frame="_top"` to links wrapped inside of a `<turbo-frame>` tells Turbo to perform a normal full-page navigation, instead of scoping the navigation within the frame. This ensures that going to `players#show` still works as expected.

After making these changes, head back to [http://localhost:3000/players](http://localhost:3000/players), toggle the view from list to card (or card to list) and then click to view a player, and finally click back out to the players index page. If all has gone well you will not see any flashing cached content.

![A screen recording of a user of a web application toggling a list of players between a card and a list-based layout and navigating into and back out of a player detail page.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/coqvd78h4ghcqz067t80.gif)

## Improving our Kredis usage
Our current implementation of Kredis will only work if our application has exactly one user. This is because we are using a static key, `preferences`, to set the players list layout.

In the real world, we will (hopefully!) have more than one user in our application. Each of our users should be able to store their own view preferences or our view toggle will not be a very useful feature! Let's wrap up this tutorial by looking at how we can use Kredis in ActiveRecord models to store user-specific preferences, making view toggling much more useful.

From your terminal, create a `User` model:

```bash
./bin/rails g model User session:string
./bin/rails db:migrate
```

And then update `app/controllers/application_controller.rb`:

```ruby
class ApplicationController < ActionController::Base
  helper_method :current_user

  def current_user    
    @current_user ||= User.find_or_create_by(session: session.id.to_s)
  end
end
```

We do not want to build out a whole user authentication system to learn more about Kredis, so here we are faking it by using `session.id` to find or create a new user in the database and then defining `current_user` as a `helper_method` available throughout our application.

In a real application, `current_user` would be a real, actual, logged in user, but our fake users will be good enough to demonstrate the key concept here.

Our goal is to be able to associate user preferences with a `User` using Kredis. Our first implementation of Kredis hardcoded a `preferences` key to store all preferences. Our new implementation will give each `User` their own unique key through Kredis’ integration with ActiveRecord.

Head to `app/models/user.rb` and update it like this:

```ruby
class User < ApplicationRecord
  kredis_hash :preferences
end
```

This update means that all users will have a unique key/value pair that stores their `preferences` in Redis. We can access this attribute with `user.preferences`, which will return an instance of a `Kredis::Hash`, just like `Kredis.hash('preferences')` did in our controller.

To use this new `preferences` attribute, update the `PlayersController#index` action in `app/controllers/players_controller.rb`:

```ruby
def index
  @players = Player.all
  current_user.preferences.update(view: params[:view]) if params[:view].present?
end
```

Here we use the same `update` method we used before, this time using `current_user.preferences` to access a unique hash for the current user.

Then update the players index view to reference the right hash:

```erb
<div class="w-full">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center">
    <h1 class="font-bold text-4xl">Players</h1>
    <%= link_to 'New player', new_player_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
  </div>

  <%= turbo_frame_tag "players", class: "min-w-full mt-8" do %>
    <% if current_user.preferences[:view] == "card" %>
      <div class="my-4">
        <%= link_to "List view", players_path(view: "list"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
      </div>
      <ul role="list" class="grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4">
        <%= render @players, partial: "players/card", as: :player %>
      </ul>
    <% else %>
      <div class="my-4">
        <%= link_to "Card view", players_path(view: "card"), class: "bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded" %>
      </div>
        <ul role="list" class="space-y-4">
          <%= render @players %>
        </ul>
    <% end %>
  <% end %>
</div>
```

The `if` statement now checks `current_user.preferences` instead of `@user_preferences`.

To test out these changes, open two separate browsers, toggle the list/card view to different options in each window, and then refresh each window to see that each “user” has their own unique preferences.

With that last change, you have reached the end of this tutorial! Incredible work today.

## Wrapping up
Today we learned a little about how to use Kredis to unlock more power from our Redis-enabled Ruby on Rails applications.

Kredis’ tight integration with ActiveRecord gives it a leg up on other tools that are often used to do this type of work — if you have used Rails for a while, you have probably stuffed view preferences, filter and search terms, and other information into the session to persist the information across page turns. Redis is a more resilient and more appropriate tool for storing this type of information, and Kredis makes it simple to use Redis for this type of work in Ruby on Rails applications.

While we used a hash for storing data, Kredis supports a variety of datatypes to suit your needs — explore [the repo](https://github.com/rails/kredis) to see the full list of available types.

One of the more exciting aspects of Kredis is the tools that can be built on top of it — we can build our own simple functionality like the preference storage we created today, but other smart folks are building gems on top of Kredis to enable functionality like [presence tracking](https://github.com/julianrubisch/cubism) for application resources or [complex, multi-stage forms](https://allfutures.leastbad.com/).

That’s all for today. As always, thanks for reading!