Module: UniversalRenderer::Renderable

Extended by:
ActiveSupport::Concern
Defined in:
lib/universal_renderer/renderable.rb

Overview

Controller-side entry point for server-side rendering. Drive it either declaratively with ClassMethods#enable_ssr, or imperatively by calling #render_ssr from the action. A failed or unconfigured render is a no-op: #ssr? returns false and the layout falls back to client-side rendering.

Defined Under Namespace

Modules: ClassMethods, Streaming

Constant Summary collapse

NON_PAGE_RENDER_OPTIONS =

Render options that mean "this is not a page". The request format is still HTML for a render json: inside a form post, or for a Turbo Frame, so the format check alone does not catch them.

%i[
  body file inline js json nothing partial plain xml
].freeze

Instance Method Summary collapse

Instance Method Details

#add_prop(key_or_hash, data_value = nil) ⇒ void

This method returns an undefined value.

Adds a prop or a hash of props to be sent to the SSR service. Props are deep-stringified if a hash is provided.

Examples:

Adding a single prop

add_prop(:user_id, 123)

Adding multiple props from a hash

add_prop({theme: "dark", locale: "en"})

Parameters:

  • key_or_hash (String, Symbol, Hash)

    The key for the prop or a hash of props.

  • data_value (Object, nil) (defaults to: nil)

    The value for the prop if key_or_hash is a key. If key_or_hash is a Hash, this parameter is ignored.



153
154
155
156
157
158
159
# File 'lib/universal_renderer/renderable.rb', line 153

def add_prop(key_or_hash, data_value = nil)
  if data_value.nil? && key_or_hash.is_a?(Hash)
    ssr_props.merge!(key_or_hash.deep_stringify_keys)
  else
    ssr_props[key_or_hash.to_s] = data_value
  end
end

#add_query_data(query_key, data) ⇒ void

This method returns an undefined value.

Adds a React Query cache entry that can be hydrated on SSR/client boot.

Entries accumulate under the react_query prop as { "query_key" => [...], "data" => ... }. The NPM package's hydrateReactQuery(props, queryClient) consumes exactly this shape — use it in setup rather than reimplementing the loop.

Parameters:

  • query_key (Array, String, Symbol)

    The React Query key.

  • data (Object)

    The cached query data.



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/universal_renderer/renderable.rb', line 207

def add_query_data(query_key, data)
  parts = query_key.is_a?(Array) ? query_key : [query_key]

  # React Query compares keys structurally, so numeric parts have to stay
  # numeric. `deep_stringify_keys` below only touches hash keys.
  normalized = parts.map { |part| part.is_a?(Symbol) ? part.to_s : part }

  push_prop(
    :react_query,
    { query_key: normalized, data: data }.deep_stringify_keys
  )
end

#push_prop(key, value_to_add) ⇒ void

This method returns an undefined value.

Allows a prop to be treated as an array, pushing new values to it. If the prop does not exist or is nil, it's initialized as an empty array. If the prop exists but is not an array (e.g., set as a scalar by add_prop), its current value will be converted into the first element of the new array. If value_to_add is an array, its elements are concatenated to the existing array. Otherwise, value_to_add is appended as a single element.

Examples:

Pushing a single value

push_prop(:notifications, "New message")

Pushing multiple values from an array

push_prop(:tags, ["rails", "ruby"])

Appending to an existing scalar value (converts to array)

add_prop(:item, "first")
push_prop(:item, "second") # ssr_props becomes { "item" => ["first", "second"] }

Parameters:

  • key (String, Symbol)

    The key of the prop to modify.

  • value_to_add (Object, Array)

    The value or array of values to add to the prop.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/universal_renderer/renderable.rb', line 178

def push_prop(key, value_to_add)
  props = ssr_props
  prop_key = key.to_s
  current_value = props[prop_key]

  if current_value.nil?
    props[prop_key] = []
  elsif !current_value.is_a?(Array)
    props[prop_key] = [current_value]
  end
  # At this point, props[prop_key] is guaranteed to be an array.

  if value_to_add.is_a?(Array)
    props[prop_key].concat(value_to_add)
  else
    props[prop_key] << value_to_add
  end
end

#render(**options) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/universal_renderer/renderable.rb', line 127

def render(*, **options)
  return super unless ssr_enabled_for_request?
  return super if options.keys.intersect?(NON_PAGE_RENDER_OPTIONS)
  # No layout, so nothing calls the helpers that would emit the payload.
  return super if options[:layout] == false

  if ssr_streaming?
    success = render_ssr_stream(*, **options)
    super unless success
  else
    render_ssr
    super
  end
end

#render_ssr(props = nil) ⇒ UniversalRenderer::SSR::Response? Also known as: fetch_ssr

Fetches the SSR payload for the current request and remembers it, so the view helpers and #ssr? can see it. Idempotent.

Parameters:

  • props (Hash, nil) (defaults to: nil)

    Props to merge first. Ignored once a render has happened, since merging then would change ssr_props without affecting the response.

Returns:



73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/universal_renderer/renderable.rb', line 73

def render_ssr(props = nil)
  return @_ssr_response if defined?(@_ssr_response)

  add_prop(props) if props.present?

  @_ssr_response =
    UniversalRenderer::Client::Base.call(request.original_url, ssr_props)

  @ssr = @_ssr_response # pre-0.6 layouts read this ivar

  @_ssr_response
end

#ssr?Boolean

Whether this request has server-rendered content to emit. Use it to pick between a hydration entry point and a client-render entry point.

True while streaming too, where the HTML arrives after the layout renders.

Returns:

  • (Boolean)


103
104
105
# File 'lib/universal_renderer/renderable.rb', line 103

def ssr?
  ssr_streaming? || ssr_response.present?
end

#ssr_propsHash

The props accumulated for this request. Mutating the returned hash is supported, but prefer #add_prop / #push_prop / #add_query_data.

rubocop:disable Naming/MemoizedInstanceVariableName -- the ivar name is part of the pre-0.6 surface; layouts and specs in the wild read it.

Returns:

  • (Hash)


60
61
62
# File 'lib/universal_renderer/renderable.rb', line 60

def ssr_props
  @universal_renderer_props ||= {}
end

#ssr_responseUniversalRenderer::SSR::Response?

Returns The payload from the most recent #render_ssr, or nil if none succeeded.

Returns:



90
91
92
93
94
95
# File 'lib/universal_renderer/renderable.rb', line 90

def ssr_response
  return @_ssr_response if defined?(@_ssr_response)

  # Tolerate layouts and controllers that assigned @ssr by hand.
  @ssr
end

#ssr_streaming?Boolean

Whether this request is being streamed.

False unless the request would also stream, so the layout never emits the <!-- SSR_HEAD --> / <!-- SSR_BODY --> markers into a page no renderer will see. A failed stream downgrades this for the same reason.

Returns:

  • (Boolean)


114
115
116
117
118
# File 'lib/universal_renderer/renderable.rb', line 114

def ssr_streaming?
  return @_ssr_streaming if defined?(@_ssr_streaming)

  self.class.ssr_streaming_preference.present? && ssr_enabled_for_request?
end