In a Rails Gemfile, the require: false option tells Bundler not to automatically load the gem when your Rails application starts. Here’s what it means and when to use it:
What It Does
gem 'some_gem', require: false
Without require: false: The gem is automatically required (loaded) when your Rails app boots
With require: false: The gem is installed but won’t be loaded until you explicitly require it
When to Use It
Performance Optimization: For gems you don’t need in all environments (like development-only tools)
Conditional Loading: When you only need a gem in specific circumstances
Reduced Memory Usage: Avoids loading unnecessary gems into memory
Avoid Naming Conflicts: If a gem might conflict with others when loaded
Example Usage
# Only load in development
group :development do
gem 'brakeman', require: false
end
# Load manually when needed
gem 'nokogiri', require: false
# Then in your code:
def parse_xml
require 'nokogiri'
# use Nokogiri...
end
Common Gems That Use This
Testing tools (RSpec, Cucumber)
Performance monitoring tools
Debugging tools (byebug, pry)
Gems used only in rake tasks
Remember that without require: false, Bundler will automatically require the gem, which is the default behavior for most gems in your application.
Ruby and Ruby on Rails are rich, expressive, and powerful technologies that make web development both elegant and productive. In this post, we’ll explore some critical concepts that developers often encounter, along with detailed explanations, advantages, disadvantages, and real-world Rails examples.
1. Garbage Collection (GC) in Ruby
Ruby’s VM uses a mark‑and‑sweep collector with generational enhancements to reduce pause times.
How it works
Generational Division: Objects are split into young (eden/survivor) and old generations. Young objects are collected more frequently.
Mark Phase: It traverses from root nodes (globals, stack, constants) marking reachable objects.
Sweep Phase: Clears unmarked (garbage) objects.
Compaction (in newer versions): Optionally compacts memory to reduce fragmentation.
# Trigger a minor GC (young generation)
GC.start(full_mark: false)
# Trigger a major GC (both generations)
GC.start(full_mark: true)
Benefits
Automatic memory management: Developers focus on logic, not free/delete calls.
ActiveRecord provides tools to fetch associations efficiently and avoid the N+1 query problem.
Method
SQL Generated
Behavior
Pros
Cons
joins
INNER JOIN
Filters by associated table
Efficient filtering; single query
Doesn’t load associated objects fully
preload
2 separate queries
Loads parent then child separately
Avoids N+1; simple to use
Two queries; might fetch unnecessary data
includes
JOIN or 2 queries
Auto‑decides between JOIN or preload
Flexible; avoids N+1 automatically
Harder to predict SQL; can generate large JOINs
eager_load
LEFT OUTER JOIN
Forces single JOIN query
Always one query with data
Large result sets; potential data duplication
Examples
# joins: Filter variants with women category products
> ProductVariant.joins(:product).where(product: {category: 'women'})
ProductVariant Load (3.4ms) SELECT "product_variants".* FROM "product_variants" INNER JOIN "products" "product" ON "product"."id" = "product_variants"."product_id" WHERE "product"."category" = 'women'
# preload: Load variants separately
> products = Product.preload(:variants).limit(10)
Product Load (1.4ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 10
ProductVariant Load (0.5ms) SELECT "product_variants".* FROM "product_variants" WHERE "product_variants"."product_id" IN (14, 15, 32)
> products.each { |product| product.variants.size}
# includes: Smart loading
products = > Product.includes(:variants).where("category = ?", 'women')
Product Load (1.7ms) SELECT "products".* FROM "products" WHERE (category = 'women') /* loading for pp */ LIMIT 11
ProductVariant Load (0.8ms) SELECT "product_variants".* FROM "product_variants" WHERE "product_variants"."product_id" IN (14, 15)
# eager_load: Always join
Product.eager_load(:variants).where(variants: { stock_quantity: 5 })
> Product.eager_load(:variants).where(variants: { stock_quantity: 5 })
SQL (3.1ms) SELECT DISTINCT "products"."id" FROM "products" LEFT OUTER JOIN "product_variants" "variants" ON "variants"."product_id" = "products"."id" WHERE "variants"."stock_quantity" = 5 LIMIT 11
SQL (1.6ms) SELECT "products"."id" AS t0_r0, "products"."description" AS t0_r1, "products"."category" AS t0_r2, "products"."created_at" AS t0_r3, "products"."updated_at" AS t0_r4, "products"."name" AS t0_r5, "products"."rating" AS t0_r6, "products"."brand" AS t0_r7, "variants"."id" AS t1_r0, "variants"."product_id" AS t1_r1, "variants"."sku" AS t1_r2, "variants"."mrp" AS t1_r3, "variants"."price" AS t1_r4, "variants"."discount_percent" AS t1_r5, "variants"."size" AS t1_r6, "variants"."color" AS t1_r7, "variants"."stock_quantity" AS t1_r8, "variants"."specs" AS t1_r9, "variants"."created_at" AS t1_r10, "variants"."updated_at" AS t1_r11 FROM "products" LEFT OUTER JOIN "product_variants" "variants" ON "variants"."product_id" = "products"."id" WHERE "variants"."stock_quantity" = 5 AND "products"."id" = 15
When to Use
joins: Filtering, counting, or conditions across tables.
preload: You only need associated objects later, with less risk of huge joins.
includes: Default choice; let AR decide.
eager_load: Complex filtering on associations in one query.
3. Achieving Multiple Inheritance via Mixins
Ruby uses modules as mixins to simulate multiple inheritance.
Pattern
module Auditable
def audit(message)
puts "Audit: #{message}"
end
end
module Taggable
def tag(*names)
@tags = names
end
end
class Article
include Auditable, Taggable
end
article = Article.new
tag "ruby", "rails"
audit "Created article"
Benefits
Code reuse: Share behavior across unrelated classes.
Separation of concerns: Each module encapsulates specific functionality.
Drawbacks
Method conflicts: Last included module wins; resolve with Module#prepend or alias_method.
Rails Example: Concerns
# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern
included do
after_create :track_create
end
def track_create
AnalyticsService.log(self)
end
end
class User < ApplicationRecord
include Trackable
end
4. Thread vs Fiber
Ruby offers preemptive threads and cooperative fibers for concurrency.
Aspect
Thread
Fiber
Scheduling
OS-level, preemptive
Ruby-level, manual (Fiber.yield/ resume)
Overhead
Higher (context switch cost)
Lower (lightweight)
Use Cases
Parallel I/O, CPU-bound (with GVL caveat)
Managing event loops, non-blocking flows
GVL Impact
All threads share GIL (Global VM Lock)
Fibers don’t bypass GVL
Thread Example
threads = 5.times.map do
Thread.new { sleep 1; puts "Done in thread #{Thread.current.object_id}" }
end
threads.each(&:join)
Fiber Example
fiber1 = Fiber.new do
puts "Fiber1 start"
Fiber.yield
puts "Fiber1 resume"
end
fiber2 = Fiber.new do
puts "Fiber2 start"
fiber1.resume
puts "Fiber2 resume"
end
fiber2.resume # orchestrates both fibers
Rails Example: Action Cable
Action Cable uses EventMachine or async fibers to handle multiple WebSocket connections efficiently.
5. Proc vs Lambda
Both are callable objects, but differ in return behavior and argument checks.
# Using a lambda for a conditional callback
class User < ApplicationRecord
after_save -> { Analytics.track(self) }, if: -> { saved_change_to_email? }
end
6. Exception Handling in Ruby
Ruby’s exception model is dynamic and flexible.
Syntax
begin
risky_operation
rescue SpecificError => e
handle_error(e)
rescue AnotherError
fallback
else
puts "No errors"
ensure
cleanup_resources
end
Benefits
Granular control: Multiple rescue clauses per exception class.
Flow control: rescue can be used inline (foo rescue nil).
Drawbacks
Performance: Raising/catching exceptions is costly.
Overuse: Rescuing StandardError broadly can hide bugs.
Rails Example: Custom Exceptions
class PaymentError < StandardError; end
def process_payment
raise PaymentError, "Insufficient funds" unless valid_funds?
rescue PaymentError => e
errors.add(:base, e.message)
end
7. Key Ruby on Rails Modules
Rails is modular, each gem serves a purpose:
Module
Purpose
Benefits
ActiveRecord
ORM: models to DB tables
DRY queries, validations, callbacks
ActionController
Controllers: request/response cycle
Filters, strong parameters
ActionView
View templates (ERB, Haml)
Helpers, partials
ActiveModel
Model conventions for non-DB classes
Validations, callbacks without DB
ActiveJob
Job framework (sidekiq, resque adapters)
Unified API for background jobs
ActionMailer
Email composition & delivery
Interceptors, mailer previews
ActionCable
WebSocket support
Streams, channels
ActiveStorage
File uploads & CDN integration
Direct uploads, variants
ActiveSupport
Utility extensions (core extensions, inflections)
Time calculations, i18n, concerns support
8. Method Visibility: public, protected, private
Visibility controls encapsulation and API design.
Modifier
Access From
Use Case
public
Everywhere
Public API methods
private
Same instance only
Helper methods not meant for external use
protected
Instances of same class or subclasses
Comparison or interaction between related objects
class Account
def transfer(to, amount)
validate_balance(amount)
to.deposit(amount)
end
private
def validate_balance(amount)
raise "Insufficient" if balance < amount
end
protected
def balance
@balance
end
end
Advantages
Encapsulation: Hides implementation details.
Inheritance control: Fine‑grained access for subclasses.
Disadvantages
Rigidity: Can complicate testing private methods.
Confusion: Protected rarely used, often misunderstood.
Above Summary
By diving deeper into these core concepts, you’ll gain a solid understanding of Ruby’s internals, ActiveRecord optimizations, module mixins, concurrency strategies, callable objects, exception patterns, Rails modules, and visibility controls. Practice these patterns in your own projects to fully internalize their benefits and trade‑offs.
Other Ruby on Rails Concepts 💡
Now, we’ll explore several foundational topics in Ruby on Rails, complete with detailed explanations, code examples, and a balanced look at advantages and drawbacks.
What is Rack? Rack is the Ruby interface between web servers (e.g., Puma, Unicorn) and Ruby web frameworks (Rails, Sinatra). It standardizes how HTTP requests and responses are handled, enabling middleware stacking and pluggable request processing.
Middleware Rack middleware are modular components that sit in the request/response pipeline. Each piece can inspect, modify, or short-circuit requests before they reach your Rails app, and likewise inspect or modify responses before they go back to the client.
# lib/simple_logger.rb
class SimpleLogger
def initialize(app)
@app = app
end
def call(env)
Rails.logger.info("[Request] #{env['REQUEST_METHOD']} #{env['PATH_INFO']}")
status, headers, response = @app.call(env)
Rails.logger.info("[Response] status=#{status}")
[status, headers, response]
end
end
# config/application.rb
config.middleware.use SimpleLogger
Benefits:
Cross-cutting concerns (logging, security, caching) can be isolated.
Easily inserted, removed, or reordered.
Drawbacks:
Overuse can complicate request flow.
Harder to trace when many middlewares are chained.
2. The N+1 Query Problem
What is N+1? Occurs when Rails executes one query to load a collection, then an additional query for each record when accessing an association.
Prevention: use eager loading (includes, preload, eager_load).
@users = User.includes(:posts)
@users.each { |u| u.posts.count } # still 2 queries only
Benefits of Eager Loading:
Dramatically reduces SQL round-trips.
Improves response times for collections.
Drawbacks:
May load unnecessary data if associations aren’t used.
Can lead to large, complex SQL (especially with eager_load).
3. Using Concerns
What are Concerns? Modules under app/models/concerns (or app/controllers/concerns) to extract and share reusable logic.
# app/models/concerns/archivable.rb
module Archivable
extend ActiveSupport::Concern
included do
scope :archived, -> { where(archived: true) }
end
def archive!
update!(archived: true)
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Archivable
end
When to Extract:
Shared behavior across multiple models/controllers.
To keep classes focused and under ~200 lines.
Benefits:
Promotes DRY code.
Encourages separation of concerns.
Drawbacks:
Can mask complexity if overused.
Debugging call stacks may be less straightforward.
4. HABTM vs. Has Many Through
HABTM (has_and_belongs_to_many):
Simple many-to-many with a join table without a Rails model.
class Post < ApplicationRecord
has_and_belongs_to_many :tags
end
Has Many Through:
Use when the join table has additional attributes or validations.
class Tagging < ApplicationRecord
belongs_to :post
belongs_to :tag
validates :tagged_by, presence: true
end
class Post < ApplicationRecord
has_many :taggings
has_many :tags, through: :taggings
end
Benefits & Drawbacks:
Pattern
Benefits
Drawbacks
HABTM
Minimal setup; fewer files
Cannot store metadata on relationship
Has Many Through
Full join model control; validations
More boilerplate; extra join model to maintain
5. Controller Hooks (Callbacks)
Rails controllers provide before_action, after_action, and around_action callbacks.
class ArticlesController < ApplicationController
before_action :authenticate_user!
before_action :set_article, only: %i[show edit update destroy]
def show; end
private
def set_article
@article = Article.find(params[:id])
end
end
Use Cases:
Authentication/authorization
Parameter normalization
Auditing/logging
Benefits:
Centralize pre- and post-processing logic.
Keep actions concise.
Drawbacks:
Overuse can obscure the action’s core logic.
Callback order matters and can introduce subtle bugs.
6. STI vs. Polymorphic Associations vs. Ruby Inheritance
Feature
STI
Polymorphic
Plain Ruby Inheritance
DB Structure
Single table + type column
Separate tables + *_type + *_id
No DB persistence
Flexibility
Subclasses share schema
Can link many models to one
Full OOP, no DB ties
When to Use
Subtypes with similar attributes
Comments, attachments across models
Pure Ruby services, utilities
STI Example:
class Vehicle < ApplicationRecord; end
class Car < Vehicle; end
class Truck < Vehicle; end
All in vehicles table, differentiated by type.
Polymorphic Example:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
Benefits & Drawbacks:
STI: simple table; limited when subclasses diverge on columns.
Polymorphic: very flexible; harder to enforce foreign-key constraints.
Ruby Inheritance: best for non-persistent logic; no DB coupling.
7. rescue_from in Rails API Controllers
rescue_from declares exception handlers at the controller (or ApplicationController) level:
class Api::BaseController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity
private
def render_not_found(e)
render json: { error: e.message }, status: :not_found
end
def render_unprocessable_entity(e)
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end
end
Benefits:
Centralized error handling.
Cleaner action code without repetitive begin…rescue.
Drawbacks:
Must carefully order rescue_from calls (first match wins).
Overly broad handlers can mask unexpected bugs.
Summary
This post has covered advanced Rails concepts with practical examples, advantages, and pitfalls. By understanding these patterns, you can write cleaner, more maintainable Rails applications. Feedback and questions are welcome—let’s keep the conversation going!
Here we will look into the detailed explanation of some Ruby concepts with practical examples, and real-world scenarios:
1. Handling Many Constants in a Ruby Class
Problem: A class with numerous constants becomes cluttered and harder to maintain.
Solutions & Examples:
Nested Module for Grouping:
class HTTPClient
module StatusCodes
OK = 200
NOT_FOUND = 404
SERVER_ERROR = 500
end
def handle_response(code)
case code
when StatusCodes::OK then "Success"
when StatusCodes::NOT_FOUND then "Page missing"
end
end
end
Why: Encapsulating constants in a module improves readability and avoids namespace collisions.
Dynamic Constants with const_set:
class DaysOfWeek
%w[MON TUE WED THU FRI SAT SUN].each_with_index do |day, index|
const_set(day, index + 1)
end
end
puts DaysOfWeek::MON # => 1
Use Case: Generate constants programmatically (e.g., days, months).
class App
CONSTANTS = YAML.load_file('config/constants.yml')
def self.error_message(code)
CONSTANTS['error_codes'].key(code)
end
end
Why: Centralize configuration for easy updates.
2. Meta-Programming: Dynamic Methods & Classes
Examples:
define_method for Repetitive Methods:
class User
ATTRIBUTES = %w[name email age]
ATTRIBUTES.each do |attr|
define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") { |value| instance_variable_set("@#{attr}", value) }
end
end
user = User.new
user.name = "Alice"
puts user.name # => "Alice"
Use Case: Auto-generate getters/setters for multiple attributes.
Dynamic Classes with Class.new:
Animal = Class.new do
def speak
puts "Animal noise!"
end
end
dog = Animal.new
dog.speak # => "Animal noise!"
Use Case: Generate classes at runtime (e.g., for plugins).
class_eval for Modifying Existing Classes:
String.class_eval do
def shout
upcase + "!"
end
end
puts "hello".shout # => "HELLO!"
Why: Add/redefine methods in existing classes dynamically.
3. Why Classes Are Objects in Ruby
Explanation:
Every class is an instance of Class.
String.class # => Class
Classes inherit from Module and ultimately Object, allowing them to have methods and variables:
class Dog
@count = 0 # Class instance variable
def self.increment_count
@count += 1
end
end
Real-World Impact: You can pass classes as arguments, modify them at runtime, and use them like any other object.
4. super Keyword: Detailed Usage
Examples:
Implicit Argument Passing:
class Vehicle
def start_engine
"Engine on"
end
end
class Car < Vehicle
def start_engine
super + " (Vroom!)"
end
end
puts Car.new.start_engine # => "Engine on (Vroom!)"
Explicit super() for No Arguments:
class Parent
def greet
"Hello"
end
end
class Child < Parent
def greet
super() + " World!" # Explicitly call Parent#greet with no args
end
end
Pitfall: Forgetting () when overriding a method with parameters.
5. Blocks in Ruby Methods: Scenarios
A simple ruby method that accepts a block and executing via yield:
Note: We can call yield any number times that we want.
Proc
Procs are similar to blocks, however, they differ in that they may be saved to a variable to be used again and again. In Ruby, a proc can be called directly using the .call method.
To create Proc, we call new on the Proc class and follow it with the block of code
my_proc = Proc.new { |x| x*x*9 }
=> #<Proc:0x000000011f64ed38 (irb):34>
my_proc.call(6)
=> 324
> my_proc.call # try to call without an argument
(irb):34:in 'block in <top (required)>': undefined method '*' for nil (NoMethodError)
lambda
> my_lambda = lambda { |x| x/3/5 }
=> #<Proc:0x000000011fe6fd28 (irb):44 (lambda)>
> my_lambda.call(233)
=> 15
> my_lambda = lambda.new { |x| x/3/5 } # wrong
in 'Kernel#lambda': tried to create Proc object without a block (ArgumentError)
> my_lambda = lambda # wrong
(irb):45:in 'Kernel#lambda': tried to create Proc object without a block (ArgumentError)
> my_lambda.call # try to call without an argument
(irb):46:in 'block in <top (required)>': wrong number of arguments (given 0, expected 1) (ArgumentError)
Difference 1: lambda gets an ArgumentError if we call without an argument and Proc doesn’t.
Difference 2: lambda returns to its calling method rather than returning itself like Proc from its parent method.
irb* def proc_method
irb* my_proc = Proc.new { return "Proc returns" }
irb* my_proc.call
irb* "Retun by proc_method" # neaver reaches here
irb> end
=> :proc_method
irb> p proc_method
"Proc returns"
=> "Proc returns"
def open_file(path)
file = File.open(path, 'w')
yield(file) if block_given?
ensure
file.close
end
open_file('log.txt') { |f| f.write("Data") }
Why: Ensures the file is closed even if an error occurs.
Custom Iterators:
class MyArray
def initialize(items)
@items = items
end
def custom_each
@items.each { |item| yield(item) }
end
end
MyArray.new([1,2,3]).custom_each { |n| puts n * 2 }
proc = Proc.new { puts "I am the proc block" }
lambda = lambda { puts "I am the lambda block"}
proc_test.call # => I am the proc block
lambda_test.call # => I am the lambda block
6. Enums in Ruby
Approaches:
Symbols/Constants:
class TrafficLight
STATES = %i[red yellow green].freeze
def initialize
@state = STATES.first
end
def next_state
@state = STATES[(STATES.index(@state) + 1) % STATES.size]
end
end
Rails ActiveRecord Enum:
class User < ActiveRecord::Base
enum role: { admin: 0, user: 1, guest: 2 }
end
user = User.new(role: :admin)
user.admin? # => true
Why: Generates helper methods like admin? and user.admin!.
7. Including Enumerable
Why Needed:
Enumerable methods (map, select, etc.) rely on each being defined.
Example Without Enumerable:
class MyCollection
def initialize(items)
@items = items
end
def each(&block)
@items.each(&block)
end
end
# Without Enumerable:
collection = MyCollection.new([1,2,3])
collection.map { |n| n * 2 } # Error: Undefined method `map`
With Enumerable:
class MyCollection
include Enumerable
# ... same as above
end
collection.map { |n| n * 2 } # => [2,4,6]
8. Class Variables (@@)
Example & Risks:
class Parent
@@count = 0
def self.count
@@count
end
def increment
@@count += 1
end
end
class Child < Parent; end
Parent.new.increment
Child.new.increment
puts Parent.count # => 2 (Shared across Parent and Child)
Why Avoid: Subclasses unintentionally modify the same variable. Alternative (Class Instance Variables):
class Parent
@count = 0
def self.count
@count
end
def self.increment
@count += 1
end
end
9. Global Variables ($)
Example & Issues:
$logger = Logger.new($stdout)
def log_error(message)
$logger.error(message) # Accessible everywhere
end
# Problem: Tight coupling; changing $logger affects all code.
When to Use: Rarely, for truly global resources like $stdout or $LOAD_PATH. Alternative: Dependency injection or singleton classes.
class AppConfig
attr_reader :logger
def initialize(logger:)
@logger = logger
end
def info(msg)
@logger.info(msg)
end
end
config = AppConfig.new(Logger.new($stdout))
info = config.info("Safe")
Summary:
Constants: Organize with modules or external files.
Meta-Programming: Use define_method/Class.new for dynamic code.
Ruby is a dynamic, object-oriented programming language designed for simplicity and productivity. Here are some of its most exciting features:
1. Everything is an Object
In Ruby, every value is an object, even primitive types like integers or nil. This allows you to call methods directly on literals. Example:
5.times { puts "Ruby!" } # 5 is an Integer object with a `times` method
3.14.floor # => 3 (Float object method)
true.to_s # => "true" (Boolean → String)
nil.nil? # => true (Method to check if object is nil)
2. Elegant and Readable Syntax
Ruby’s syntax prioritizes developer happiness. Parentheses and semicolons are often optional. Example:
# A method to greet a user (parentheses optional)
def greet(name = "Guest")
puts "Hello, #{name.capitalize}!"
end
greet "alice" # Output: "Hello, Alice!"
3. Blocks and Iterators
Ruby uses blocks (anonymous functions) to create powerful iterators. Use {} for single-line blocks or do...end for multi-line. Example:
# Multiply even numbers by 2
numbers = [1, 2, 3, 4]
result = numbers.select do |n|
n.even?
end.map { |n| n * 2 }
puts result # => [4, 8]
4. Mixins via Modules
Modules let you share behavior across classes without inheritance. Example:
module Loggable
def log(message)
puts "[LOG] #{message}"
end
end
class User
include Loggable # Mix in the module
end
user = User.new
user.log("New user created!") # => [LOG] New user created!
5. Metaprogramming
Ruby can generate code at runtime. For example, dynamically define methods. Example:
class Person
# Define methods like name= and name dynamically
attr_accessor :name, :age
end
person = Person.new
person.name = "Alice"
puts person.name # => "Alice"
6. Duck Typing
Focus on behavior, not type. If it “quacks like a duck,” treat it as a duck. Example:
def print_length(obj)
obj.length # Works for strings, arrays, or any object with a `length` method
end
puts print_length("Hello") # => 5
puts print_length([1, 2, 3]) # => 3
7. Symbols
Symbols (:symbol) are lightweight, immutable strings used as identifiers. Example:
A set is a Ruby class that helps you create a list of unique items. A set is a class that stores items like an array. But with some special attributes that make it 10x faster in specific situations! All the items in a set are guaranteed to be unique.
What’s the difference between a set & an array? A set has no direct access to elements:
> seen[3]
(irb):19:in '<main>': undefined method '[]' for #<Set:0x000000012fc34058> (NoMethodError)
But a set can be converted into an array any time you need:
> seen.to_a
=> [4, 8, 9, 90]
> seen.to_a[3]
=> 90
Set: Fast lookup times (with include?)
If you need these then a set will give you a good performance boost, and you won’t have to be calling uniq on your array every time you want unique elements. Reference: https://www.rubyguides.com/2018/08/ruby-set-class/
Superset & Subset
A superset is a set that contains all the elements of another set.
Set.new(10..40) >= Set.new(20..30)
A subset is a set that is made from parts of another set:
Ruby minimizes boilerplate code with conventions. Example:
class Book
attr_accessor :title, :author # Auto-generates getters/setters
def initialize(title, author)
@title = title
@author = author
end
end
book = Book.new("Ruby 101", "Alice")
puts book.title # => "Ruby 101"
# No need to free memory manually
1000.times { String.new("temp") } # GC cleans up unused objects
15. Community and Ecosystem
RubyGems (packages) like:
Rails: Full-stack web framework.
RSpec: Testing framework.
Sinatra: Lightweight web server.
Install a gem:
gem install rails
16. Error Handling
Use begin/rescue for exceptions:
begin
puts 10 / 0
rescue ZeroDivisionError => e
puts "Error: #{e.message}" # => "Error: divided by 0"
end
17. Open Classes
Modify existing classes (use carefully!):
class String
def reverse_and_upcase
self.reverse.upcase
end
end
puts "hello".reverse_and_upcase # => "OLLEH"
18. Reflection
Inspect objects at runtime:
class Dog
def bark
puts "Woof!"
end
end
dog = Dog.new
puts dog.respond_to?(:bark) # => true
puts Dog.instance_methods # List all methods
Ruby’s design philosophy emphasizes developer productivity and joy. These features make it ideal for rapid prototyping, web development (with Rails), scripting, and more.
Ruby, with its elegant syntax and dynamic nature, empowers developers to write expressive and flexible code. But beneath its simplicity lie powerful—and sometimes misunderstood – concepts like modules, mixins, and meta-programming that define the language’s true potential. Whether you’re wrestling with method lookup order, curious about how method_missing enables magic-like behaviour, or want to leverage eigen classes to bend Ruby’s object model to your will, understanding these fundamentals is key to writing clean, efficient, and maintainable code.
In this guide, we’ll unravel Ruby 3.4’s threading model, its Global Interpreter Lock (GIL) nuances, and how Ruby on Rails 8 leverages concurrency for scalable web applications. We’ll unpack foundational concepts like the Comparable module, Hash collections, and functional programming constructs such as lambdas and Procs. Additionally, we’ll demystify Ruby’s interpreted nature, contrast compilers with interpreters, and highlight modern GC advancements that optimize memory management. Through practical examples, we’ll also examine Ruby’s exception handling, the purpose of respond_to?, Ruby’s core mechanics, from modules and classes to the secrets of the ancestor chain, equipping you with the knowledge to transform from a Ruby user to a Ruby architect. Let’s dive in! 🔍
Why Call It “Magic”?
Ruby lets you break conventional rules and invent your own behavior.
It feels like “sorcery” compared to statically-typed languages.
Frameworks like Rails rely heavily on this (e.g., has_many, before_action).
method_missing
Ruby’s way of saying, “If a method doesn’t exist, call this instead!”
Lets you handle undefined methods dynamically (e.g., building DSLs or proxies).
Dynamic Method Creation (define_method, send)
Define methods on the fly based on conditions or data.
Example: Automatically generating getters/setters without attr_accessor.
Ghost Methods & respond_to_missing?
Methods that “don’t exist” syntactically but behave like they do.
Makes objects infinitely adaptable (e.g., Rails’ find_by_* methods).
Singleton Methods (Eigenclass Wizardry)
Attaching methods to individual objects (even classes!) at runtime.
Example: Adding a custom method to just one string:
Use define_method or eval for dynamic method creation:
class MyClass
[:a, :b].each { |m| define_method(m) { ... } }
end
6. Eigenclass (Singleton Class)
Hidden class where singleton methods live. Accessed via singleton_class or class << obj.
obj = Object.new
eigenclass = class << obj; self; end
eigenclass.define_method(:foo) { ... }
Class methods are stored in the class’s eigenclass.
7. Ancestors (Method Lookup)
Order: Eigenclass → prepended modules → class → included modules → superclass.
class C; include M; prepend P; end
C.ancestors # => [P, C, M, Object, ...]
8. method_missing
Called when a method is not found. Override for dynamic behavior.
class Proxy
def method_missing(method, *args)
# Handle unknown methods here
end
end
9. String Interpolation
Embed code in #{} within double-quoted strings or symbols:
name = "Alice"
puts "Hello, #{name.upcase}!" # => "Hello, ALICE!"
Single quotes ('') disable interpolation.
10. Threading in Ruby 3.4 and Ruby on Rails 8
Does Ruby 3.4 support threads? Yes, Ruby 3.4 supports threads via its native Thread class. However, due to the Global Interpreter Lock (GIL) in MRI (Matz’s Ruby Interpreter), Ruby threads are concurrent but not parallel for CPU-bound tasks. I/O-bound tasks (e.g., HTTP requests, file operations) can still benefit from threading as the GIL is released during I/O waits.
How to do threading in Ruby:
threads = []
3.times do |i|
threads << Thread.new { puts "Thread #{i} running" }
end
threads.each(&:join) # Wait for all threads to finish
In Ruby on Rails 8:
Use threading for background tasks, API calls, or parallel processing.
Ensure thread safety: Avoid shared mutable state; use mutexes or thread-safe data structures.
Rails automatically manages database connection pools for threads.
Mix in Comparable to add comparison methods (<, >, <=, >=, ==, between?) to a class. Define <=> (spaceship operator) to compare instances:
class Person
include Comparable
attr_reader :age
def initialize(age)
@age = age
end
def <=>(other)
age <=> other.age
end
end
alice = Person.new(30)
bob = Person.new(25)
alice > bob # => true
12. Why Ruby is interpreted?
Compiler vs. Interpreter
Compiler
Interpreter
Translates entire code upfront.
Translates and executes line-by-line.
Faster execution.
Slower execution.
Harder to debug.
Easier to debug.
Examples: C++, Rust.
Examples: Python, Ruby.
Why Ruby is interpreted? Ruby prioritizes developer productivity and dynamic features (e.g., metaprogramming). MRI uses an interpreter, but JRuby (JVM) and TruffleRuby use JIT compilation.
Which is better?
Compiler: Better for performance-critical applications.
Interpreter: Better for rapid development and scripting.
13. respond_to? in Ruby
Checks if an object can respond to a method:
str = "hello"
str.respond_to?(:upcase) # => true
Other Languages:
Python: hasattr(obj, 'method')
JavaScript: 'method' in obj
Java/C#: No direct equivalent (static typing avoids runtime checks).
14. Ruby Hash
A key-value collection:
user = { name: "Alice", age: 30 }
Advantages:
Fast O(1) average lookup.
Flexible keys (symbols, strings, objects).
Ordered in Ruby 1.9+.
Use Cases:
Configuration settings.
Caching (e.g., Rails.cache).
Grouping data (e.g., group_by).
15. Lambdas and Procs
Lambda (strict argument check, returns from itself):
lambda = ->(x, y) { x + y }
lambda.call(2, 3) # => 5
Proc (flexible arguments, returns from enclosing method):
def test
proc = Proc.new { return "Exiting" }
proc.call
"Never reached"
end
test # => "Exiting"
Use Cases:
Passing behavior to methods (e.g., map(&:method)).
Callbacks and event handling.
16. Ruby 3.4 Garbage Collector (GC)
Improvements:
Generational GC: Separates objects into young (short-lived) and old (long-lived) generations for faster collection.
Incremental GC: Reduces pause times by interleaving GC with program execution.
Compaction: Reduces memory fragmentation (introduced in Ruby 2.7).
How It Works:
Mark Phase: Traces reachable objects.
Sweep Phase: Frees memory of unmarked objects.
Compaction: Rearranges objects to optimize memory usage.
17. Exception Handling in Ruby
begin
# Risky code
File.open("file.txt") { |f| puts f.read }
rescue Errno::ENOENT => e
# Handle file not found
puts "File missing: #{e.message}"
rescue StandardError => e
# General error handling
puts "Error: #{e}"
ensure
# Always execute (e.g., cleanup)
puts "Execution completed."
end
rescue: Catches exceptions.
else: Runs if no exception.
ensure: Runs regardless of success/failure.
Summary
Mixins: Use include (instance methods), extend (class methods), or prepend.
Singletons: Methods on individual objects (e.g., def obj.method).
Meta-Programming: Dynamically define methods/variables with instance_variable_get, define_method, etc.
Lookup: Follows the ancestors chain, including modules and eigenclasses.
Eigenclasses: The hidden class behind every object for singleton methods.
Regular expressions (regex) are powerful tools for pattern matching and text manipulation. In Ruby, they’re implemented through the Regexp class. Let’s start with the basics and gradually build up to more complex patterns.
1. Basic Matching
Literal Characters
The simplest regex matches exact text:
"hello".match(/hello/) #=> #<MatchData "hello">
Special Characters
Some characters have special meaning and need escaping with \:
# Matching a literal dot
"file.txt".match(/file\.txt/) #=> #<MatchData "file.txt">
2. Character Classes
Simple Character Sets
Match any one character from a set:
# Match either 'a', 'b', or 'c'
"bat".match(/[abc]/) #=> #<MatchData "b">
Ranges
Match any character in a range:
# Match any lowercase letter
"hello".match(/[a-z]/) #=> #<MatchData "h">
# Match any digit
"Room 101".match(/[0-9]/) #=> #<MatchData "1">
Negated Character Sets
Match any character NOT in the set:
# Match any character that's not a vowel
"hello".match(/[^aeiou]/) #=> #<MatchData "h">
3. Shorthand Character Classes
Ruby provides shortcuts for common character classes:
\d # Any digit (0-9)
\D # Any non-digit
\w # Word character (letter, digit, underscore)
\W # Non-word character
\s # Whitespace (space, tab, newline)
\S # Non-whitespace
? # 0 or 1 times
* # 0 or more times
+ # 1 or more times
{n} # Exactly n times
{n,} # n or more times
{n,m} # Between n and m times
Examples:
# Match between 3 and 5 digits
"12345".match(/\d{3,5}/) #=> #<MatchData "12345">
# Match 'color' or 'colour'
"colour".match(/colou?r/) #=> #<MatchData "colour">
5. Anchors
Match positions rather than characters:
^ # Start of line
$ # End of line
\A # Start of string
\Z # End of string
\b # Word boundary
Examples:
# Check if string starts with 'Hello'
"Hello world".match(/^Hello/) #=> #<MatchData "Hello">
# Check if string ends with 'world'
"Hello world".match(/world$/) #=> #<MatchData "world">
6. Grouping and Capturing
Parentheses create groups and capture matches:
# Capture date components
match = "2023-05-18".match(/(\d{4})-(\d{2})-(\d{2})/)
match[1] #=> "2023" (year)
match[2] #=> "05" (month)
match[3] #=> "18" (day)
7. Alternation
The pipe | acts like an OR operator:
# Match 'cat' or 'dog'
"dog".match(/cat|dog/) #=> #<MatchData "dog">
8. Modifiers
Change how the regex works:
i # Case insensitive
m # Multiline mode (dot matches newline)
x # Ignore whitespace (for readability)
Examples:
# Case insensitive match
"HELLO".match(/hello/i) #=> #<MatchData "HELLO">
Finds all occurrences of a pattern in a string and returns them as an array.
The scan method in Ruby is a powerful string method that allows you to find all occurrences of a pattern in a string. It returns an array of matches, making it extremely useful for text processing and data extraction tasks.
text = "hello world hello ruby"
matches = text.scan(/hello/)
puts matches.inspect
# Output: ["hello", "hello"]
Matching Multiple Patterns
text = "The quick brown fox jumps over the lazy dog"
matches = text.scan(/\b\w{3}\b/) # Find all 3-letter words
puts matches.inspect
# Output: ["The", "fox", "the", "dog"]
"a1 b2 c3".scan(/(\w)(\d)/) { |letter, num| puts "#{letter} -> #{num}" }
# Output:
# a -> 1
# b -> 2
# c -> 3
text = "Prices: $10, $20, $30"
total = 0
text.scan(/\$(\d+)/) { |match| total += match[0].to_i }
puts total
# Output: 60
5. Case-Insensitive Search
"Ruby is COOL!".scan(/cool/i) # => ["COOL"]
6. Extract Email Addresses
"Email me at test@mail.com".scan(/\S+@\S+/) # => ["test@mail.com"]
text = "Contact us at support@example.com or sales@company.org"
emails = text.scan(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/)
puts emails.inspect
# Output: ["support@example.com", "sales@company.org"]
Performance Characteristics: Ruby’s #scan Method
The #scan method is generally efficient for most common string processing tasks, but its performance depends on several factors:
String length – Larger strings take longer to process
Pattern complexity – Simple patterns are faster than complex regex
Number of matches – More matches mean more memory allocation
Performance Considerations
1. Time Complexity 🧮
Best case: O(n) where n is string length
Worst case: O(n*m) for complex regex patterns (with backtracking)
2. Memory Usage 🧠
Creates an array with all matches
Each match is a new string object (memory intensive for large results)
Benchmark 📈 Examples
require 'benchmark'
large_text = "Lorem ipsum " * 10_000
# Simple word matching
Benchmark.bm do |x|
x.report("simple scan:") { large_text.scan(/\w+/) }
x.report("complex scan:") { large_text.scan(/(?:^|\s)(\w+)(?=\s|$)/) }
end
Typical results:
user system total real
simple scan: 0.020000 0.000000 0.020000 ( 0.018123)
complex scan: 0.050000 0.010000 0.060000 ( 0.054678)
# Slower (creates match groups)
text.scan(/(\w+)/)
# Faster
text.scan(/\w+/)
Use blocks to avoid large arrays:
# Stores all matches in memory
matches = text.scan(pattern)
# Processes matches without storing
text.scan(pattern) { |m| process(m) }
Consider alternatives for very large strings:
# For simple splits, String#split might be faster
words = text.split
# For streaming processing, use StringIO
When to Be Cautious ⚠️
Processing multi-megabyte strings
Using highly complex regular expressions
When you only need the first few matches (consider #match instead)
The #scan method is optimized for most common cases, but for performance-critical applications with large inputs, consider benchmarking alternatives.
#inject Method (aka #reduce)
Enumerable#inject takes two arguments: a base case and a block.
Each item of the Enumerable is passed to the block, and the result of the block is fed into the block again and iterate next item.
In a way the inject function injects the function between the elements of the enumerable. inject is aliased as reduce. You use it when you want to reduce a collection to a single value.
For example:
product = [ 2, 3, 4 ].inject(1) do |result, next_value|
result * next_value
end
product #=> 24
Purpose
Accumulates values by applying an operation to each element in a collection
Can produce a single aggregated result or a compound value
require 'benchmark'
large_array = (1..1_000_000).to_a
Benchmark.bm do |x|
x.report("inject:") { large_array.inject(0, :+) }
x.report("each + var:") do
sum = 0
large_array.each { |n| sum += n }
sum
end
end
Typical results show inject is slightly slower than explicit iteration but more concise:
user system total real
inject: 0.040000 0.000000 0.040000 ( 0.042317)
each + var: 0.030000 0.000000 0.030000 ( 0.037894)
Optimization Tips 💡
Use symbol shorthand when possible (faster than blocks):
# Faster
array.inject(:+)
# Slower
array.inject { |sum, n| sum + n }
Preallocate mutable objects when building structures:
# Good for hashes
items.inject({}) { |h, (k,v)| h[k] = v; h }
# Better for arrays
items.inject([]) { |a, e| a << e.transform; a }
Avoid unnecessary object creation in blocks:
# Bad - creates new string each time
strings.inject("") { |s, x| s + x.upcase }
# Good - mutates original string
strings.inject("") { |s, x| s << x.upcase }
Consider alternatives for simple cases:
# For simple sums
array.sum # (Ruby 2.4+) is faster than inject(:+)
# For concatenation
array.join is faster than inject(:+)
When to Be Cautious ⚠️
With extremely large collections where memory matters
When the block operations are very simple (explicit loop may be faster)
When building complex nested structures (consider each_with_object)
The inject method provides excellent readability with generally good performance for most use cases.
Minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking.
minitest/test is a small and incredibly fast unit testing framework. It provides a rich set of assertions to make your tests clean and readable.
minitest/spec is a functionally complete spec engine. It hooks onto minitest/test and seamlessly bridges test assertions over to spec expectations.
minitest/benchmark is an awesome way to assert the performance of your algorithms in a repeatable manner. Now you can assert that your newb co-worker doesn’t replace your linear algorithm with an exponential one!
minitest/mock by Steven Baker, is a beautifully tiny mock (and stub) object framework.
minitest/pride shows pride in testing and adds coloring to your test output
minitest/test_task – a full-featured and clean rake task generator. – Minitest Github
♦️ Incredibly small and fast runner, but no bells and whistles.
Let’s take the given example in the doc, we’d like to test the following class:
class Meme
def i_can_has_cheezburger?
"OHAI!"
end
def will_it_blend?
"YES!"
end
end
🧪 Unit tests
Define your tests as methods beginning with test_.
require "minitest/autorun"
class TestMeme < Minitest::Test
def setup
@meme = Meme.new
end
def test_that_kitty_can_eat
assert_equal "OHAI!", @meme.i_can_has_cheezburger?
end
def test_that_it_will_not_blend
refute_match /^no/i, @meme.will_it_blend?
end
def test_that_will_be_skipped
skip "test this later"
end
end
# File lib/minitest/test.rb, line 153
def setup; end
♦️ Runs before every test. Use this to set up before each test run.
The terms “unit test” and “spec” are often used in software testing, and while they can overlap, they have some key differences:
🧪 Unit Test vs 📋 Spec: Key Differences
🔬Unit Test
Purpose: Tests a single unit of code (typically a method, function, or class) in isolation
Scope: Very focused and narrow – tests one specific piece of functionality
Style: Usually follows a more traditional testing approach with setup, execution, and assertion
Framework examples: Minitest (like in your Ruby file), JUnit, pytest
Structure: Often uses test_ prefix or Test classes with assertion methods
📝 Spec (Specification)
Purpose: Describes the behavior and requirements of the system in a more readable, documentation-like format
Scope: Can cover unit-level, integration, or acceptance testing
Style: Uses natural language descriptions that read like specifications
Framework examples: RSpec, Jasmine, Mocha, Jest
Structure: Uses descriptive blocks like describe, it, should
⚖️ Key Differences
1. ✍️ Writing Style:
Unit Test: def test_array_is_empty with assertions
Spec: describe "when array is empty" do it "should return error message"
2. 👁️ Readability:
Unit Test: More code-focused, technical
Spec: More human-readable, business-focused
3. 🎯 Philosophy:
Unit Test: Test the implementation
Spec: Specify the behavior (BDD – Behavior Driven Development)
📊 Example Comparison
🏷️ Our current Minitest code:
def array_is_an_empty_array
assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
end
🎨 RSpec equivalent (spec style):
describe "two_sum" do
context "when array is empty" do
it "returns an error message" do
expect(two_sum([], 9)).to eq('Provide an array with length 2 or more')
end
end
end
Both test the same functionality, but specs emphasize describing behavior in natural language, making them easier for non-technical stakeholders to understand. 🎉
🔬 Mini-test equivalent:
# frozen_string_literal: true
require 'minitest/spec'
require_relative 'two_sum'
describe "TwoSum" do
describe "when array is empty" do
it "returns an error message" do
_(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
end
end
end
The underscore _() in Minitest spec style is a wrapper method that converts the value into an expectation object.
Here’s why it’s used:
🔍 Why the Underscore _()?
🎯 Purpose:
The _() method wraps your actual value and returns a special expectation object that has assertion methods like must_equal, must_be, etc.
⚙️ How it works:
# This:
_(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
# Is equivalent to:
# 1. two_sum([], 9) returns some value
# 2. _() wraps that value into an expectation object
# 3. .must_equal() is called on that expectation object
🔄 Alternative Syntaxes
1. 📝 Using expect() (more readable):
expect(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
2. 🔧 Using value() (explicit):
value(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
3. 🏷️ Traditional unit test style (no wrapper needed):
assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
💡 Why underscore specifically?
Minimal syntax: _() is short and doesn’t clutter the code
Ruby convention: Underscore often indicates “throwaway” or “utility” variables
Non-conflicting: Unlikely to conflict with your method names
Chainable: Creates a fluent interface for assertions
🎨 Complete comparison:
# Spec style with underscore
_(result).must_equal expected
# Spec style with expect (more readable)
expect(result).must_equal expected
# Unit test style (no wrapper)
assert_equal expected, result
The expect() version is often preferred because it’s more self-documenting!
🎨 Alternative Spec Style with Expectation Syntax:
# frozen_string_literal: true
require 'minitest/spec'
require_relative 'two_sum'
describe "TwoSum" do
describe "when array is empty" do
it "returns an error message" do
expect(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
end
end
end
⚖️ Key Differences in Minitest
🔧 Setup Required:
Unit Test: require 'minitest/test'
Spec: require 'minitest/spec'
✍️ Assertion Style:
Unit Test: assert_equal expected, actual
Spec: _(actual).must_equal expected or expect(actual).must_equal expected
🏗️ Structure:
Unit Test: Classes with test_ methods
Spec: describe blocks with it blocks
🎯 Readability:
Both styles work with Minitest, but the spec style reads more like natural language documentation, making it easier to understand the intended behavior!
Excellent question! Let me clarify the relationship between TDD/BDD (methodologies) and RSpec/Minitest (testing frameworks):
Ruby on Rails is known for its developer-friendly conventions, but it’s also built with security in mind. While the framework provides many features to guard against common threats, it’s up to developers to understand and apply them correctly.
In this post, we’ll walk through essential Rails security measures, tackle real-world threats, and share best practices – with examples for both API-only and full-stack Rails applications.
🚨 Common Web Threats Rails Helps Mitigate
SQL Injection
Cross-Site Scripting (XSS)
Cross-Site Request Forgery (CSRF)
Mass Assignment
Session Hijacking
Insecure Deserialization
Insecure File Uploads
Authentication & Authorization flaws
Let’s explore how Rails addresses these and what you can do to reinforce your app.
1. 🧱 SQL Injection
🛡️ Rails Protection:
Threat: Attackers inject malicious SQL through user inputs to read, modify, or delete database records
Rails uses Active Record with prepared statements to prevent SQL injection by default.
Arel: Build complex queries without string interpolation.
3. Victim visitsattacker.com while still logged into the bank.
4. Browser auto-sends the bank session cookie with the forged POST—and the transfer goes through, because the bank sees a “legitimate” logged-in request.
🛡️ Rails’ CSRF Protection
Rails ships with built-in defenses against CSRF by embedding an unguessable token in forms and verifying it on each non-GET request.
1.protect_from_forgery
In ApplicationController, Rails by default includes:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
This causes Rails to raise an exception if the token is missing or invalid.
Cross-Site Request Forgery (CSRF) is an attack that tricks a user’s browser into submitting a request (e.g. form submission, link click) to your application without the user’s intention, leveraging the fact that the browser automatically includes credentials (cookies, basic auth headers, etc.) with each request.
🔧 Disabling or Customizing CSRF
♦️ Disable for APIs (stateless JSON endpoints):
class Api::BaseController < ActionController::API skip_before_action :verify_authenticity_token end
♦️ Use Null Session (allowing some API use without exception):
protect_from_forgery with: :null_session
✅ Key Takeaways
CSRF exploits the browser’s automatic credential sending.
Rails guards by inserting and validating an unguessable token.
Always keep protect_from_forgery with: :exception in your base controller for full-stack Rails apps.
Fuzz & Pen Testing: Use tools like ZAP Proxy, OWASP ZAP.
Use RSpec tests for role restrictions, parameter whitelisting, and CSRF.
describe "Admin access" do
it "forbids non-admins from deleting users" do
delete admin_user_path(user)
expect(response).to redirect_to(root_path)
end
end
Continuous Integration – Integrate scans in CI pipeline (GitHub Actions example):
# config/initializers/rack_attack.rb
Rack::Attack.throttle('req/ip', limit: 60, period: 1.minute) do |req|
req.ip
end
in Rails 8 we can use rate_limit for Controller actions like:
rate_limit to: 10, within: 1.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
Pagination & Filtering: Prevent large payloads to avoid DoS.
📝 Summary: Best Practices Checklist
✅ Use Strong Parameters ✅ Escape output (no raw unless absolutely trusted) ✅ Sanitize user content ✅ Use Devise or Sorcery for auth ✅ Authorize every resource with Pundit or CanCanCan ✅ Store files safely and validate uploads ✅ Enforce HTTPS in production ✅ Regularly run Brakeman and bundler-audit ✅ Rate-limit APIs with Rack::Attack ✅ Keep dependencies up to date
🔐 Final Thought
Rails does a lot to keep you safe — but security is your responsibility. Follow these practices and treat every external input as potentially dangerous. Security is not a one-time setup — it’s an ongoing process.
Declarative UI: build complex interfaces by composing small, reusable components.
Virtual DOM: efficient updates, smoother user experience.
Rich ecosystem: hooks, context, testing tools, and libraries like Redux.
Easy to learn once you grasp JSX and component lifecycle.
Why use React in Rails?
Leverage Rails’ backend power (ActiveRecord, routing, authentication) with React’s frontend flexibility.
Build single-page-app-like interactions within a Rails monolith or progressively enhance ERB views.
2. Prerequisites
Ruby 3.4.x installed (recommend using rbenv or RVM or Mise).
Rails 8.x (we’ll install below).
Node.js (>= 16) and npm or Yarn.
Code editor (VS Code, RubyMine, etc.).
Why Node.js is Required for React
React’s ecosystem relies on a JavaScript runtime and package manager:
Build tools (ESBuild, Webpack, Babel) run as Node.js scripts to transpile JSX/ES6 and bundle assets.
npm/Yarn fetch and manage React and its dependencies from the npm registry.
Script execution: Rails generators and custom npm scripts (e.g. rails javascript:install:react, npm run build) need Node.js to execute.
Without Node.js, you cannot install packages or run the build pipeline necessary to compile and serve React components.
What is Node.js?
Node.js is an open-source, cross-platform JavaScript runtime built on Chrome’s V8 engine. It enables JavaScript to be executed on the server (outside the browser) and provides:
Server-side scripting: build web servers, APIs, and backend services entirely in JavaScript.
Command-line tools: run scripts for tasks like building, testing, or deploying applications.
npm ecosystem: access to hundreds of thousands of packages for virtually any functionality, from utility libraries to full frameworks.
Event-driven, non-blocking I/O: efficient handling of concurrent operations, making it suitable for real-time applications.
Node.js is the backbone that powers React’s tooling, package management, and build processes.
3. Installing Ruby 3.4 and Rails 8
1. Install Ruby 3.4.0 (example using rbenv):
# install rbenv and ruby-build if not yet installed
brew install rbenv ruby-build
rbenv install 3.4.0
rbenv global 3.4.0
ruby -v # => ruby 3.4.0p0
We’ll scaffold a fresh project using ESBuild for JavaScript bundling, which integrates seamlessly with React.
rails new design_studio_react \
--database=postgresql \
-j esbuild
cd design_studio_react
--database=postgresql: sets PostgreSQL as the database adapter.
-j esbuild: configures ESBuild for JS bundling (preferred for React in Rails 8).
4.1 About ESBuild
ESBuild is a next-generation JavaScript bundler and minifier written in Go. Rails 8 adopted ESBuild by default for JavaScript bundling due to its remarkable speed and modern feature set:
Blazing-fast builds: ESBuild performs parallel compilation and leverages Go’s concurrency, often completing bundling in milliseconds even for large codebases.
Built‑in transpilation: it supports JSX and TypeScript out of the box, so you don’t need separate tools like Babel unless you have highly custom transforms.
Tree shaking: ESBuild analyzes import/export usage to eliminate dead code, producing smaller bundles.
Plugin system: you can extend ESBuild with plugins for asset handling, CSS bundling, or custom file types.
Simplicity: configuration is minimal—Rails’ -j esbuild flag generates sensible defaults, and you can tweak options in package.json or a separate esbuild.config.js.
How Rails Integrates ESBuild
When you run:
rails new design_studio_react --database=postgresql -j esbuild
Rails will:
1. Install the esbuild npm package alongside react dependencies.
Developer experience: near-instant rebuilds let you see JSX changes live without delay.
Production readiness: built‑in minification and tree shaking keep your asset sizes small.
Future-proof: the plugin ecosystem grows, and Rails can adopt newer bundlers (like SWC or Vite) with a similar pattern.
With ESBuild, your React components compile quickly, your development loop tightens, and your production assets stay optimized—making it the perfect companion for a modern Rails 8 + React stack.
5. What is Virtual DOM
The Virtual DOM is one of React’s most important concepts. Let me explain it clearly with examples.
🎯 What is the Virtual DOM?
The Virtual DOM is a JavaScript representation (copy) of the actual DOM that React keeps in memory. It’s a lightweight JavaScript object that describes what the UI should look like.
📚 Real DOM vs Virtual DOM
Real DOM (What the browser uses):
<!-- This is the actual DOM in the browser -->
<div id="todo-app">
<h1>My Todo List</h1>
<ul>
<li>React List</li>
<li>Build a todo app</li>
</ul>
</div>
// React compares old vs new Virtual DOM
const differences = [
{
type: 'ADD',
location: 'ul.children',
element: { type: 'li', props: { children: 'Build Todo App' } }
}
];
Step 5: Reconciliation (Updating Real DOM)
// React updates ONLY what changed in the real DOM
const ul = document.querySelector('ul');
const newLi = document.createElement('li');
newLi.textContent = 'Build Todo App';
ul.appendChild(newLi); // Only this line runs!
🚀 Why Virtual DOM is Fast
Without Virtual DOM (Traditional approach):
// Traditional DOM manipulation
function updateTodoList(todos) {
const ul = document.querySelector('ul');
ul.innerHTML = ''; // Clear everything!
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo;
ul.appendChild(li); // Recreate everything!
});
}
With Virtual DOM (React approach):
// React's approach
function updateTodoList(oldTodos, newTodos) {
const differences = findDifferences(oldTodos, newTodos);
differences.forEach(diff => {
if (diff.type === 'ADD') {
// Only add the new item
const li = document.createElement('li');
li.textContent = diff.todo;
ul.appendChild(li);
}
});
}
🎭 Real Example with Our Todo App
Let’s trace through what happens when you add a todo:
// React compares and finds:
const changes = [
{
type: 'INSERT',
location: 'ul',
element: { type: 'li', key: 3, props: { children: 'Master React Hooks ⏳' } }
}
];
// React updates ONLY what changed:
const ul = document.querySelector('ul');
const newLi = document.createElement('li');
newLi.textContent = 'Master React Hooks ⏳';
ul.appendChild(newLi); // Only this operation!
🎯 Key Benefits of Virtual DOM
1. Performance:
// Without Virtual DOM: Updates entire list
document.querySelector('ul').innerHTML = generateEntireList(todos);
// With Virtual DOM: Updates only what changed
document.querySelector('ul').appendChild(newTodoElement);
2. Predictability:
// You write declarative code
const TodoList = ({ todos }) => (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
// React handles the imperative updates
// You don't need to manually add/remove DOM elements
3. Batching:
// Multiple state updates in one event
const handleButtonClick = () => {
setTodos([...todos, newTodo]); // Change 1
setInputValue(''); // Change 2
setCount(count + 1); // Change 3
};
// React batches these into one DOM update!
// For simple apps, Virtual DOM has overhead
// Direct DOM manipulation can be faster for simple operations
document.getElementById('counter').textContent = count;
❌ “Virtual DOM prevents all DOM operations”
// React still manipulates the real DOM
// Virtual DOM just makes it smarter about WHEN and HOW
✅ “Virtual DOM optimizes complex updates”
// When you have many components and complex state changes
// Virtual DOM's diffing algorithm is much more efficient
🧠 Does React show Virtual DOM to the user?
No. The user only ever sees the real DOM. The Virtual DOM (VDOM) is never shown directly. It’s just an internal tool used by React to optimize how and when the real DOM gets updated.
🧩 What is Virtual DOM exactly?
A JavaScript-based, lightweight copy of the real DOM.
Stored in memory.
React uses it to figure out what changed after state/props updates.
👀 What the user sees:
The real, visible HTML rendered to the browser — built from React components.
This is called the Real DOM.
🔁 So why use Virtual DOM at all?
✅ Because manipulating the real DOM is slow.
React uses VDOM to:
Build a new virtual DOM after every change.
Compare (diff) it with the previous one.
Figure out the minimum real DOM updates required.
Apply only those changes to the real DOM.
This process is called reconciliation.
🖼️ Visual Analogy
Imagine the Virtual DOM as a sketchpad. React draws the new state on it, compares it with the old sketch, and only updates what actually changed in the real-world display (real DOM).
✅ TL;DR
Question
Answer
Does React show the virtual DOM to user?
❌ No. Only the real DOM is ever visible to the user.
What is virtual DOM used for?
🧠 It’s used internally to calculate DOM changes efficiently.
Is real DOM updated directly?
✅ Yes, but only the minimal parts React determines from the VDOM diff.
🧪 Example Scenario
👤 The user is viewing a React app with a list of items and a button:
Compares previous VDOM (10 <li> items) vs new VDOM (20 <li> items).
Finds that 10 new <li> nodes were added.
This is called the reconciliation process.
⚙️ 4. React Updates the Real DOM
React tells the browser: “Please insert 10 new <li> elements inside the <ul>.”
✅ Only these 10 DOM operations happen. ❌ React does not recreate the entire <ul> or all 20 items.
🖼️ What the User Sees
On the screen (the real DOM):
<ul>
<li>Item 1</li>
...
<li>Item 20</li>
</ul>
The user never sees the Virtual DOM — they only see the real DOM updates that React decides are necessary.
🧠 Summary: Virtual DOM vs Real DOM
Step
Virtual DOM
Real DOM
Before click
10 <li> nodes in memory
10 items visible on screen
On click
New VDOM generated with 20 <li> nodes
React calculates changes
Diff
Compares new vs old VDOM
Determines: “Add 10 items”
Commit
No UI shown from VDOM
Only those 10 new items added to browser DOM
✅ Key Point
🧠 The Virtual DOM is a tool for React, not something the user sees. 👁️ The user only sees the final, optimized changes in the real DOM.
🎯 Summary
Virtual DOM is React’s:
JavaScript representation of the real DOM
Diffing algorithm that compares old vs new Virtual DOM
Reconciliation process that updates only what changed
Performance optimization for complex applications
Abstraction layer that lets you write declarative code
Think of it as React’s smart assistant that:
Remembers what your UI looked like before
Compares it with what it should look like now
Makes only the necessary changes to the real DOM
This is why you can write simple, declarative code like {todos.map(todo => <li>{todo}</li>)} and React handles all the complex DOM updates efficiently!
🔄 After the Virtual DOM Diff, How React Updates the Real DOM
🧠 Step-by-Step:
React creates a diff between the new and previous virtual DOM trees.
React then creates a list of “instructions” called the update queue.
Examples:
“Insert <li>Item 11</li> at position 10″
“Remove <div> at index 3″
“Change text of button to ‘Read Less'”
These changes are passed to React’s reconciliation engine.
React uses the browser’s DOM APIs (document.createElement, appendChild, removeChild, etc.) to apply only the minimal changes.
✅ So instead of doing:
document.body.innerHTML = newHTML; // inefficient, replaces all
React does:
const newEl = document.createElement("li");
newEl.textContent = "Item 11";
ul.appendChild(newEl); // just this
❓ Why Didn’t Browsers Do This Earlier?
Excellent historical question. The short answer is: Browsers give us the tools, but React gave us the strategy.
⚠️ Why browsers didn’t do it automatically:
Reason
Explanation
🧱 Low-level APIs
The browser exposes DOM APIs (appendChild, setAttribute), but they’re imperative — devs must write the logic.
🤯 Complexity
Managing DOM efficiently across many updates (nested, reordered, conditional elements) is hard and bug-prone manually.
🔁 Manual state syncing
Before React, developers had to manually keep UI in sync with state. That logic got complex and messy fast.
📦 No built-in abstraction
Browsers don’t offer a built-in “virtual diff engine” or abstraction like React’s VDOM.
🤖 What React Added That Browsers Don’t
Feature
Browser DOM
React (with VDOM)
Efficient diffing
❌ No
✅ Yes (reconciliation)
Declarative UI
❌ No
✅ Yes (return <UI />)
Component abstraction
❌ No
✅ Yes (function/class components)
State-driven rendering
❌ Manual
✅ Built-in
Minimal updates
❌ Up to you
✅ Automatic via VDOM
✅ TL;DR
React calculates exactly what changed via the virtual DOM diffing.
It then uses native DOM APIs to update only what’s necessary in the real DOM.
Browsers give you low-level control, but not an optimized strategy for updating UI based on state — React filled that gap beautifully.
Now Let’s break down how a React app starts after you run:
npx create-react-app my-app
cd my-app
npm start
What actually happens behind the scenes? Let’s unpack it step-by-step 👇
⚙️ Step 1: npx create-react-app — What It Does
This command:
Downloads and runs the latest version of the create-react-app tool (CRA).
Sets up a project with:
A preconfigured Webpack + Babel build system
Development server
Scripts and dependencies
Installs React, ReactDOM, and a bunch of tools inside node_modules.
Key folders/files created:
my-app/
├── node_modules/
├── public/
├── src/
│ └── index.js 👈 main entry point
├── package.json
Step 2: npm start — How the App Runs
When you run:
npm start
It’s actually running this line from package.json:
"scripts": {
"start": "react-scripts start"
}
So it calls:
react-scripts start
🧠 What is react-scripts?
react-scripts is a package from Facebook that:
Runs a development server using Webpack Dev Server
Compiles JS/JSX using Babel
Watches your files for changes (HMR)
Starts a browser window at http://localhost:3000
It configures:
Webpack
Babel
ESLint
PostCSS
Source maps … all behind the scenes, so you don’t have to set up any configs manually.
📦 Libraries Involved
Tool / Library
Purpose
React
Core UI library (react)
ReactDOM
Renders React into actual DOM (react-dom)
Webpack
Bundles your JS, CSS, images, etc.
Babel
Converts modern JS/JSX to browser-friendly JS
Webpack Dev Server
Starts dev server with live reloading
react-scripts
Runs all the above with pre-made configs
🏗️ Step 3: Entry Point — src/index.js
The app starts here:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
ReactDOM.createRoot(...) finds the <div id="root"> in public/index.html.
Then renders the <App /> component into it.
The DOM inside the browser updates — and the user sees the UI.
✅ TL;DR
Step
What Happens
npx create-react-app
Sets up a full React project with build tools
npm start
Calls react-scripts start, which runs Webpack dev server
react-scripts
Handles build, hot reload, and environment setup
index.js
Loads React and renders your <App /> to the browser DOM
Browser Output
You see your live React app at localhost:3000
6. Installing and Configuring React
Rails 8 provides a generator to bootstrap React + ESBuild.
Run the React installer: rails javascript:install:react This will:
Install react and react-dom via npm.
Create an example app/javascript/components/HelloReact.jsx component.
Configure ESBuild to transpile JSX.
Verify your application layout: In app/views/layouts/application.html.erb, ensure you have: <%= javascript_include_tag "application", type: "module", defer: true %>
Mount the React component: Replace (or add) a div placeholder in an ERB view, e.g. app/views/home/index.html.erb:<div id="hello-react" data-props="{}"></div>
Initialize mount point In app/javascript/application.js:
import "./components"
In app/javascript/components/index.js:
import React from "react"
import { createRoot } from "react-dom/client"
import HelloReact from "./HelloReact"
document.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("hello-react")
if (container) {
const root = createRoot(container)
const props = JSON.parse(container.dataset.props || "{}")
root.render(<HelloReact {...props} />)
}
})
Your React component will now render within the Rails view!