DEV Community

Cover image for Updating User Settings Without a Save Button
Jess Alejo
Jess Alejo

Posted on

Updating User Settings Without a Save Button

Alright, so today I obsessed over something simple—user settings. You know, those toggles for notifications and profile preferences. Normally, you'd throw in a checkbox, slap on a "Save" button, and call it a day. But no, I wanted something fancier.

The Problem

I don't want users clicking "Save" whenever changing a setting. Feels outdated. Instead, toggles should just work—flip the switch, boom, saved.

So, I went down the Stimulus + Rails 8 + JSONB rabbit hole.

Step 1: Ditch the Extra Columns

I refuse to clutter my database with a dozen boolean columns for settings. JSONB to the rescue

class AddSettingsToUsers < ActiveRecord::Migration[8.0]
  def change
    add_column :users, :settings, :jsonb, default: {}
  end
end
Enter fullscreen mode Exit fullscreen mode

Now I can store all settings in one place. Future-proof, flexible, and no schema changes when I add new settings.

Step 2: Default Settings Without a Mess

I don't want to check for nil every time I call a setting, so I merged defaults with what's actually stored.

class User < ApplicationRecord
  DEFAULT_SETTINGS = { "profile_visibility" => true, "disable_ads" => false }.freeze

  def user_settings
    DEFAULT_SETTINGS.merge(settings || {})
  end

  def update_settings(new_settings)
    update(settings: user_settings.merge(new_settings))
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, if a setting doesn't exist yet, it falls back to the default. No surprises.

Step 3: The UI - Toggles instead of Checkboxes

Because checkboxes are boring, and Bootstrap 5 has a built-in form-switch class to make toggles look cleaner.

.setting-item.form-check.form-switch.form-check-md.mb-3
  = check_box_tag "user_settings[profile_visibility]",
                  class: "form-check-input",
                  checked: user.user_settings["profile_visibility"],
                  "data-setting" => "profile_visibility"
  = label_tag "user_settings[profile_visibility]",
              "Profile visibility",
              class: "form-check-label"
Enter fullscreen mode Exit fullscreen mode

Step 4: Making It Actually Work (With Stimulus)

Here's the fun part. When a user flips a toggle, it immediately sends an AJAX request to update the setting. No reloads, no extra clicks.

updateSetting(event) {
  const setting = event.target.dataset.setting
  const value = event.target.checked

  fetch("/profile/update_settings", {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')
        .content,
    },
    body: JSON.stringify({ setting, value }),
  })
    .then((response) => response.json())
    .then((data) => {
      if (!data.success) {
        alert("Failed to update setting.")
        event.target.checked = !value // Revert on failure
      }
    })
}
Enter fullscreen mode Exit fullscreen mode

Wait, What's This "X-CSRF-Token" Thing?

Rails has built-in security mechanisms to prevent Cross-Site Request Forgery (CSRF) attacks. If you try making a PATCH, POST, PUT, or DELETE request without a CSRF token, Rails will reject it.

Since my settings update happens via fetch(), I need to include this token in the request headers. Instead of hardcoding it, Rails provides a helper to generate it dynamically in the layout:

-# application.html.haml
!!!
%html
  %head

    = csrf_meta_tags
Enter fullscreen mode Exit fullscreen mode
  • csrf_meta_tags -> Inserts the CSRF token.
<html>
  <head>

    <meta name="csrf-token" content="dvj7leYFe3zAzv0BkIMOlUnyOqBuPSMzwAr_rFV7GqViAsvcDDFWh_suMlKu7coEDj5WtvL5oMs6l2c_KtSeQg">

Enter fullscreen mode Exit fullscreen mode

Then, in my JavaScript, I grab the token like this:

headers: { 
  "Content-Type": "application/json",
  "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
}
Enter fullscreen mode Exit fullscreen mode

This way, every request includes the correct CSRF token without me worrying about it. Rails is happy, security stays intact, and everything just works.

Final Thoughts

✅ No database clutter
✅ No "Save" button nonsense
✅ No unnecessary page reloads
✅ Everything just works

Might be over-engineering things sometimes, but hey, that’s all part of the learning process—and I’m enjoying every bit of it! 🚀

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more