Serialize Query Params Back Into A Url String In Rails
Filter forms, pagination links, and sort toggles all need to rebuild the current URL with one parameter changed and the rest preserved. Doing it with string concatenation breaks on the first array parameter or special character.
Description
to_query on a hash produces a properly encoded, alphabetically sorted query string, handling nested hashes and arrays with the bracket notation Rails parses on the way back in.
In a view, url_for(params.permit(...).merge(...)) is usually better β it keeps the current controller and action and only rewrites the parameters you name.
Always permit before merging. Passing raw params to url_for raises, and blindly permitting everything lets a crafted link inject parameters into every generated URL on the page.
Sample input:
{ status: "pending", tags: ["urgent", "new"], page: 2 }.to_query
Sample Output:
page=2&status=pending&tags%5B%5D=urgent&tags%5B%5D=new
Answer
# Hash to query string
{ status: "pending", page: 2 }.to_query
# => "page=2&status=pending"
# Arrays and nesting are handled
{ tags: ["urgent", "new"], filter: { min: 10 } }.to_query
# => "filter%5Bmin%5D=10&tags%5B%5D=urgent&tags%5B%5D=new"
# Parse it back
Rack::Utils.parse_nested_query("tags[]=urgent&tags[]=new")
# => { "tags" => ["urgent", "new"] }
# In a controller or helper β keep current params, change one
class ApplicationController < ActionController::Base
PERMITTED_FILTERS = [:status, :sort, :direction, :page, :q, tags: []].freeze
helper_method :url_with_params
def url_with_params(overrides = {})
url_for(params.permit(*PERMITTED_FILTERS).merge(overrides).to_h)
end
end
<%= link_to "Next", url_with_params(page: @page + 1) %>
<%= link_to "Sort by date", url_with_params(sort: "created_at", direction: "desc") %>
<%= link_to "Clear status", url_with_params(status: nil) %>
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! π