Skip to content

Stabilizers

Steven Steffen edited this page Jan 12, 2024 · 1 revision

Stabilizing Filters

Here we'll go over the process of stabilizing filters so that they can be viewed, reused, or edited at a later time. We use the term "stabilization" because after a filter goes through the stabilization process, you are returned a string or an ID that will always and forever reference a specific filter in a specific state.

Stabilization is similar to serializing or encoding, in that a filter with a state $x will always stabilize to an ID $y, and an ID $y will always resolve to a filter with state $x. It is bi-directional.

Persisting? Serializing?

Why do we call it stabilizing and not persisting? Persisting implies that the data will be stored somewhere, which is not necessarily true of all of Refine's stabilization drivers. The Database Stabilizer persists the data, but the URL Encoder does not.

Why don't we call it serializing? Serializing usually implies that all of the data is still present in the resulting value. That's not true of all of Refine's stabilization drivers either! The result of the URL Encoder contains all the data, but the Database Stabilizer returns a small pointer to a record in the database.

We landed on the term "stabilization" because it encompasses both concepts.

Refine Defaults

By default, the filters are URL Encoded stabilized. This enables us to pass a stable_id to your controller and it is used in the apply_filter method that is included in the FilterApplicationController helper.

Stabilization - Rehydrating Filters

You may call from_stable_id on a filter at any time. You'll need to pass in the Stabilizer that you want to use.

# Rehydrate a filter from a `stable_id`
filter = Stabilizers::UrlEncodedStabilizer.new.from_stable_id(id: params[:stable_id])

You can optionally send in an initial_query when rehydrating the filter. This is useful if you're using cancancan or pundit and want to preserve tenancy

# Rehydrate a filter from a `stable_id`
filter = Stabilizers::UrlEncodedStabilizer.new.from_stable_id(id: params[:stable_id], initial_query: initial_query)

Getting the query from a stabilized filter

Once you have the filter object, you can call get_query on the object to return the query object.

# Rehydrate a filter from a `stable_id`
filter = Stabilizers::UrlEncodedStabilizer.new.from_stable_id(id: params[:stable_id])
# Get the query - returns and Active Record::Relation object 
filter.get_query

Switching stabilizers

If you'd like some of your filters to be saved to you refine_stored_filters table, you can "translate" from a URLEncoded id to a Database id. You may want to only save filters once the entire model has pass validation for example. Here is what that would look like:

  def save_filter_and_return_id(id:, initial_query: nil)
    # How to use (in controller) -> filter_id = save_filter_and_return_id(id: params[:stable_id], initial_query: scope)
    # This method returns a primary key of the filter in your refine_stored_filters table which you can then add to your model
    filter = Stabilizers::UrlEncodedStabilizer.new.from_stable_id(id: id, initial_query: initial_query)
    Stabilizers::DatabaseStabilizer.new.to_stable_id(filter: filter)
  end

Provided Stabilizers

We have provided 2 stabilizers out of the box for you. Each one does something a little bit different that makes sense in different situations. You can read more about them on their individual pages.

  • Database - Persists the state to the database.
  • UrlEncoded - Encodes the filter's state but doesn't persist it anywhere. Totally self-contained.

Writing Your Own Stabilizer

All stabilizers must implement the Stabilizer interface, which enforces two methods: from_stable_id and to_stable_id.

As long as your class conforms to that interface, you are free to implement any custom logic that you like.

Database Stabilization

Database stabilization is the most straightforward of all the stabilizers, as it simply stores the filter's state in the database.

You need to create a migration to add the table that will hold filter ids and their corresponding state. If you choose to use this stabilizer, make sure you've published and run the provided migration:

rails generate migration CreateRefineStoredFilters state:json name:string filter_type:string
rails db:migrate

This will create a refine_stored_filters table in your database.

The StoredFilter class can be overridden with additional functionality (i.e. additional validations). The following is the base class:

module Refine
  class StoredFilter < ApplicationRecord
    validates_presence_of :state
    self.table_name = "refine_stored_filters"

    def refine_filter
      Refine::Rails.configuration.stabilizer_classes[:db].new.from_stable_id(id: id)
    end

    def blueprint
      JSON.parse(state)["blueprint"]
    end
  end
end

Render the filter with included stored filters view by setting stored_filter to true <%= render partial: 'filter_builder_dropdown', locals: { stored_filters: true } %>

Stabilizing via URL Encoding

The UrlEncoded stabilizer is a little different than the Database Stabilizer in that it doesn't save anything anywhere. What this stabilizer does is takes the filter's state and turns it into a rawurlencode encoded string which then gets sent back to the frontend under the stable_id key.

The common use case for the UrlEncoded stabilizer is to receive the stable_id on the frontend, and then update the user's window location to include that id, eg example.com/users?filter={stable_id}. Putting that stable id into the URL allows the user to copy, share, refresh, or otherwise store the URL and navigate back to exactly where they were before.

If your app sees high usage, then the UrlEncoded stabilizer is a great way to allow users to not lose all of their progress without having to save every filter to the database.

A Warning!

There is one extremely important consideration when using the UrlEncoded stabilizer: the size of the encoded string is directly proportional to the size of the filter's state. If your end user adds many conditions with very long values, then it is possible that your encoded state could end up being greater than 2,000 characters.

This consideration is so important because it is generally recommended that URLs not exceed 2,000 characters. If you're not putting the entire UrlEncoded string in the URL, then this consideration is irrelevant. If you are, however, you will need to consider the possibility of a user creating a filter that encodes to an extremely long string.

By default the string is gzipped before it's encoded, but you still need to consider the potential length of string.