Exception: ReactOnRails::SmartError

Inherits:
Error
  • Object
show all
Defined in:
lib/react_on_rails/smart_error.rb,
sig/react_on_rails/smart_error.rbs

Overview

SmartError provides enhanced error messages with actionable suggestions rubocop:disable Metrics/ClassLength

Constant Summary collapse

DOCS_BASE_URL =

Returns:

  • (String)
"https://reactonrails.com/docs/reference/error-reference"
UNKNOWN_ERROR_DEFINITION =

Returns:

{
  code: "ROR000",
  title: "Unknown Error",
  summary: "An unexpected SmartError type was raised.",
  sample_context: {}.freeze
}.freeze
ERROR_DEFINITIONS =

Returns:

{
  component_not_registered: {
    code: "ROR001",
    title: "Component Not Registered",
    summary: "React on Rails could not find the component in the client-side component registry.",
    sample_context: {
      component_name: "ProductCard",
      available_components: %w[ProductList ProductDetails UserProfile].freeze
    }.freeze
  }.freeze,
  missing_auto_loaded_bundle: {
    code: "ROR002",
    title: "Auto-loaded Bundle Missing",
    summary: "A component is configured for auto-loading, but its generated bundle is missing.",
    sample_context: {
      component_name: "Dashboard",
      expected_path: "app/javascript/packs/generated/Dashboard.js"
    }.freeze
  }.freeze,
  missing_auto_loaded_store_bundle: {
    code: "ROR003",
    title: "Auto-loaded Store Bundle Missing",
    summary: "A Redux store is configured for auto-loading, but its generated store bundle is missing.",
    sample_context: {
      component_name: "AppStore",
      expected_path: "app/javascript/packs/generated/AppStore.js"
    }.freeze
  }.freeze,
  hydration_mismatch: {
    code: "ROR004",
    title: "Hydration Mismatch",
    summary: "The server-rendered HTML does not match the React tree rendered in the browser.",
    sample_context: {
      component_name: "UserProfile"
    }.freeze
  }.freeze,
  server_rendering_error: {
    code: "ROR005",
    title: "Server Rendering Failed",
    summary: "Server-side rendering failed while rendering a React component.",
    sample_context: {
      component_name: "ComplexComponent",
      error_message: "window is not defined"
    }.freeze
  }.freeze,
  redux_store_not_found: {
    code: "ROR006",
    title: "Redux Store Not Found",
    summary: "A component requested a Redux store that was not registered.",
    sample_context: {
      store_name: "AppStore",
      available_stores: %w[UserStore ProductStore].freeze
    }.freeze
  }.freeze,
  configuration_error: {
    code: "ROR007",
    title: "Configuration Error",
    summary: "React on Rails detected invalid or incomplete configuration.",
    sample_context: {
      details: "config.server_bundle_js_file points to a missing file"
    }.freeze
  }.freeze
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(error_type:, component_name: nil, props: nil, js_code: nil, **additional_context) ⇒ SmartError

Returns a new instance of SmartError.

Parameters:

  • error_type: (Symbol, String)
  • component_name: (String, nil) (defaults to: nil)
  • props: (String, nil) (defaults to: nil)
  • js_code: (String, nil) (defaults to: nil)
  • additional_context (Object)


101
102
103
104
105
106
107
108
109
110
# File 'lib/react_on_rails/smart_error.rb', line 101

def initialize(error_type:, component_name: nil, props: nil, js_code: nil, **additional_context)
  @error_type = error_type
  @component_name = component_name
  @props = props
  @js_code = js_code
  @additional_context = additional_context

  message = build_error_message
  super(message)
end

Instance Attribute Details

#additional_contextHash[Symbol, untyped] (readonly)

Returns the value of attribute additional_context.

Returns:

  • (Hash[Symbol, untyped])


99
100
101
# File 'lib/react_on_rails/smart_error.rb', line 99

def additional_context
  @additional_context
end

#component_nameString? (readonly)

Returns the value of attribute component_name.

Returns:

  • (String, nil)


99
100
101
# File 'lib/react_on_rails/smart_error.rb', line 99

def component_name
  @component_name
end

#error_typeSymbol, String (readonly)

Returns the value of attribute error_type.

Returns:

  • (Symbol, String)


99
100
101
# File 'lib/react_on_rails/smart_error.rb', line 99

def error_type
  @error_type
end

#js_codeString? (readonly)

Returns the value of attribute js_code.

Returns:

  • (String, nil)


99
100
101
# File 'lib/react_on_rails/smart_error.rb', line 99

def js_code
  @js_code
end

#propsString? (readonly)

Returns the value of attribute props.

Returns:

  • (String, nil)


99
100
101
# File 'lib/react_on_rails/smart_error.rb', line 99

def props
  @props
end

Class Method Details

.docs_url_for(error_type) ⇒ String?

Parameters:

  • error_type (Symbol, String)

Returns:

  • (String, nil)


88
89
90
91
92
93
# File 'lib/react_on_rails/smart_error.rb', line 88

def self.docs_url_for(error_type)
  normalized_error_type = normalize_error_type(error_type)
  return unless error_definitions.key?(normalized_error_type)

  "#{DOCS_BASE_URL}##{error_definition_for(normalized_error_type).fetch(:code).downcase}"
end

.error_definition_for(error_type) ⇒ error_definition

Parameters:

  • error_type (Symbol, String)

Returns:



84
85
86
# File 'lib/react_on_rails/smart_error.rb', line 84

def self.error_definition_for(error_type)
  ERROR_DEFINITIONS.fetch(normalize_error_type(error_type), UNKNOWN_ERROR_DEFINITION)
end

.error_definitionsHash[Symbol, error_definition]

Returns:



80
81
82
# File 'lib/react_on_rails/smart_error.rb', line 80

def self.error_definitions
  ERROR_DEFINITIONS
end

.normalize_error_type(error_type) ⇒ Symbol

Parameters:

  • error_type (Symbol, String)

Returns:

  • (Symbol)


95
96
97
# File 'lib/react_on_rails/smart_error.rb', line 95

def self.normalize_error_type(error_type)
  error_type.respond_to?(:to_sym) ? error_type.to_sym : error_type
end

Instance Method Details

#additional_infoString

rubocop:disable Metrics/AbcSize

Returns:

  • (String)


427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/react_on_rails/smart_error.rb', line 427

def additional_info
  info = []

  info << "#{Rainbow('Component:').blue} #{component_name}" if component_name

  if additional_context[:available_components]&.any?
    info << "#{Rainbow('Registered components:').blue} #{additional_context[:available_components].join(', ')}"
  end

  info << "#{Rainbow('Rails Environment:').blue} development (detailed errors enabled)" if Rails.env.development?

  info << "#{Rainbow('Auto-load bundles:').blue} enabled" if ReactOnRails.configuration.auto_load_bundle

  return "" if info.empty?

  "\n#{Rainbow('📋 Context:').blue.bright}\n#{info.join("\n")}"
end

#build_error_messageString

Returns:

  • (String)


145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/react_on_rails/smart_error.rb', line 145

def build_error_message
  header = Rainbow("❌ React on Rails Error [#{code}]: #{error_type_title}").red.bright

  message = <<~MSG
    #{header}

    #{error_description}

    #{error_reference_section}

    #{Rainbow('💡 Suggested Solution:').yellow.bright}
    #{solution}

    #{additional_info}
    #{troubleshooting_section}
  MSG

  message.strip
end

#codeString

Returns:

  • (String)


112
113
114
# File 'lib/react_on_rails/smart_error.rb', line 112

def code
  error_definition.fetch(:code)
end

#component_not_registered_solutionString

rubocop:disable Metrics/AbcSize

Returns:

  • (String)


249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/react_on_rails/smart_error.rb', line 249

def component_not_registered_solution
  suggestions = []

  # Check for similar component names
  if component_name && !component_name.empty?
    similar = find_similar_components(component_name)
    suggestions << "Did you mean one of these? #{similar.map { |s| Rainbow(s).green }.join(', ')}" if similar.any?
  end

  suggestions << <<~SOLUTION
    #{Rainbow('🚀 Recommended: Use Auto-Bundling (No Registration Required!)').green.bright}

    1. Enable auto-bundling in your view:
       #{Rainbow("<%= react_component(\"#{component_name}\", props: {}, auto_load_bundle: true) %>").cyan}

    2. Place your component in the components directory:
       #{Rainbow("app/javascript/#{ReactOnRails.configuration.components_subdirectory || 'components'}/#{component_name}/#{component_name}.jsx").cyan}
    #{'   '}
       Component structure:
       #{Rainbow("#{ReactOnRails.configuration.components_subdirectory || 'components'}/").cyan}
       #{Rainbow("└── #{component_name}/").cyan}
       #{Rainbow("    └── #{component_name}.jsx").cyan} (must export default)

    3. Generate the bundle:
       #{Rainbow('bundle exec rake react_on_rails:generate_packs').cyan}

    #{Rainbow("✨ That's it! No manual registration needed.").yellow}

    ─────────────────────────────────────────────

    #{Rainbow('Alternative: Manual Registration').gray}

    If you prefer manual registration:
    1. Register in your entry file:
       #{Rainbow("ReactOnRails.register({ #{component_name}: #{component_name} });").cyan}

    2. Import the component:
       #{Rainbow("import #{component_name} from './components/#{component_name}';").cyan}
  SOLUTION

  suggestions.join("\n")
end

#configuration_error_solutionString

Returns:

  • (String)


401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/react_on_rails/smart_error.rb', line 401

def configuration_error_solution
  <<~SOLUTION
    Review your React on Rails configuration:

    1. Check #{Rainbow('config/initializers/react_on_rails.rb').cyan}

    2. Common configuration issues:
       - Invalid bundle paths
       - Missing Node modules location
       - Incorrect component subdirectory

    3. Run configuration doctor:
       #{Rainbow('rake react_on_rails:doctor').cyan}
  SOLUTION
end

#default_solutionString

Returns:

  • (String)


417
418
419
420
421
422
423
424
# File 'lib/react_on_rails/smart_error.rb', line 417

def default_solution
  <<~SOLUTION
    1. Check the browser console for JavaScript errors
    2. Review your server logs: #{Rainbow('tail -f log/development.log').cyan}
    3. Run diagnostics: #{Rainbow('rake react_on_rails:doctor').cyan}
    4. Set #{Rainbow('FULL_TEXT_ERRORS=true').cyan} for complete error output
  SOLUTION
end

#docs_urlString?

Returns:

  • (String, nil)


116
117
118
119
120
# File 'lib/react_on_rails/smart_error.rb', line 116

def docs_url
  return unless self.class.error_definitions.key?(normalized_error_type)

  self.class.docs_url_for(normalized_error_type)
end

#error_definitionerror_definition

Returns:



169
170
171
# File 'lib/react_on_rails/smart_error.rb', line 169

def error_definition
  self.class.error_definitions.fetch(normalized_error_type, UNKNOWN_ERROR_DEFINITION)
end

#error_descriptionString

rubocop:disable Metrics/CyclomaticComplexity

Returns:

  • (String)


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/react_on_rails/smart_error.rb', line 201

def error_description
  case normalized_error_type
  when :component_not_registered
    <<~DESC
      Component '#{component_name}' was not found in the component registry.

      React on Rails offers two approaches:
      • Auto-bundling (recommended): Components load automatically, no registration needed
      • Manual registration: Traditional approach requiring explicit registration
    DESC
  when :missing_auto_loaded_bundle
    <<~DESC
      Component '#{component_name}' is configured for auto-loading but its bundle is missing.
      Expected location: #{additional_context[:expected_path]}
    DESC
  when :missing_auto_loaded_store_bundle
    <<~DESC
      Redux store '#{component_name}' is configured for auto-loading but its bundle is missing.
      Expected location: #{additional_context[:expected_path]}
    DESC
  when :hydration_mismatch
    <<~DESC
      The server-rendered HTML doesn't match what React rendered on the client.
      Component: #{component_name}
    DESC
  when :server_rendering_error
    <<~DESC
      An error occurred while server-side rendering component '#{component_name}'.
      #{additional_context[:error_message]}
    DESC
  when :redux_store_not_found
    <<~DESC
      Redux store '#{additional_context[:store_name]}' was not found.
      Available stores: #{additional_context[:available_stores]&.join(', ') || 'none'}
    DESC
  when :configuration_error
    <<~DESC
      Invalid configuration detected.
      #{additional_context[:details]}
    DESC
  else
    "An unexpected error occurred."
  end
end

#error_reference_sectionString

Returns:

  • (String)


173
174
175
176
177
# File 'lib/react_on_rails/smart_error.rb', line 173

def error_reference_section
  lines = ["#{Rainbow('Code:').blue} #{code}"]
  lines << "#{Rainbow('Docs:').blue} #{docs_url}" if docs_url
  lines.join("\n")
end

#error_type_titleString

Returns:

  • (String)


179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/react_on_rails/smart_error.rb', line 179

def error_type_title
  case normalized_error_type
  when :component_not_registered
    "Component '#{component_name}' Not Registered"
  when :missing_auto_loaded_bundle
    "Auto-loaded Bundle Missing"
  when :missing_auto_loaded_store_bundle
    "Auto-loaded Store Bundle Missing"
  when :hydration_mismatch
    "Hydration Mismatch"
  when :server_rendering_error
    "Server Rendering Failed"
  when :redux_store_not_found
    "Redux Store Not Found"
  when :configuration_error
    "Configuration Error"
  else
    "Unknown Error"
  end
end

#find_similar_components(name) ⇒ Array[String]

rubocop:disable Metrics/CyclomaticComplexity

Parameters:

  • name (String)

Returns:



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/react_on_rails/smart_error.rb', line 451

def find_similar_components(name)
  return [] unless additional_context[:available_components]

  available = additional_context[:available_components]
  return [] if available.empty?

  available = available.uniq

  # Simple similarity check - could be enhanced with Levenshtein distance
  similar = available.select do |comp|
    comp.downcase.include?(name.downcase) || name.downcase.include?(comp.downcase)
  end

  # Also check for common naming patterns
  if similar.empty?
    # Check if user forgot to capitalize
    capitalized = name.capitalize
    similar = available.select { |comp| comp == capitalized }

    # Check for common suffixes
    if similar.empty? && !name.end_with?("Component")
      with_suffix = "#{name}Component"
      similar = available.select { |comp| comp == with_suffix }
    end
  end

  similar.take(3) # Limit suggestions
  # rubocop:enable Metrics/CyclomaticComplexity
end

#hydration_mismatch_solutionString

Returns:

  • (String)


341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/react_on_rails/smart_error.rb', line 341

def hydration_mismatch_solution
  <<~SOLUTION
    Common causes and solutions:

    1. **Random IDs or timestamps**: Use consistent values between server and client
       #{Rainbow('// Bad: Math.random() or Date.now()').red}
       #{Rainbow('// Good: Use props or deterministic values').green}

    2. **Browser-only APIs**: Check for client-side before using:
       #{Rainbow("if (typeof window !== 'undefined') { ... }").cyan}

    3. **Different data**: Ensure props are identical on server and client
       - Check your redux store initialization
       - Verify railsContext is consistent

    4. **Conditional rendering**: Avoid using user agent or viewport checks

    Debug tips:
    - Set #{Rainbow('prerender: false').cyan} temporarily to isolate the issue
    - Check browser console for hydration warnings
    - Compare server HTML with client render
  SOLUTION
end

#missing_auto_loaded_bundle_solutionString

rubocop:enable Metrics/AbcSize

Returns:

  • (String)


293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/react_on_rails/smart_error.rb', line 293

def missing_auto_loaded_bundle_solution
  <<~SOLUTION
    1. Run the pack generation task:
       #{Rainbow('bundle exec rake react_on_rails:generate_packs').cyan}

    2. Ensure your component is in the correct directory:
       #{Rainbow("app/javascript/#{ReactOnRails.configuration.components_subdirectory || 'components'}/#{component_name}/").cyan}

    3. Check that the component file follows naming conventions:
       - Component file: #{Rainbow("#{component_name}.jsx").cyan} or #{Rainbow("#{component_name}.tsx").cyan}
       - Must export default

    4. Verify webpack/shakapacker is configured for nested entries:
       #{Rainbow("config.nested_entries_dir = 'components'").cyan}
  SOLUTION
end

#missing_auto_loaded_store_bundle_solutionString

Returns:

  • (String)


310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/react_on_rails/smart_error.rb', line 310

def missing_auto_loaded_store_bundle_solution
  source_path = packer_source_path_for_message
  store_subdirectory = ReactOnRails.configuration.stores_subdirectory || "ror_stores"
  store_source_path = "#{source_path}/**/#{store_subdirectory}/#{component_name}.js"

  <<~SOLUTION
    1. Run the pack generation task:
       #{Rainbow('bundle exec rake react_on_rails:generate_packs').cyan}

    2. Ensure your store is in a directory matching stores_subdirectory under packer_source_path:
       #{Rainbow(store_source_path).cyan}

    3. Check that the store file follows naming conventions:
       - Store file: #{Rainbow("#{component_name}.js").cyan} or #{Rainbow("#{component_name}.ts").cyan}
       - Must export default a store generator function

    4. Verify stores_subdirectory is configured:
       #{Rainbow("config.stores_subdirectory = 'ror_stores'").cyan}
  SOLUTION
end

#normalized_error_typeSymbol

Returns:

  • (Symbol)


165
166
167
# File 'lib/react_on_rails/smart_error.rb', line 165

def normalized_error_type
  self.class.normalize_error_type(error_type)
end

#packer_source_path_for_messageString

Returns:

  • (String)


331
332
333
334
335
336
337
338
339
# File 'lib/react_on_rails/smart_error.rb', line 331

def packer_source_path_for_message
  return "app/javascript" unless defined?(::Shakapacker) && ::Shakapacker.respond_to?(:config)

  config = ::Shakapacker.config
  return "app/javascript" unless config.respond_to?(:config_path) && config.config_path.exist?
  return "app/javascript" unless config.respond_to?(:source_path)

  config.source_path
end

#redux_store_not_found_solutionString

Returns:

  • (String)


385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/react_on_rails/smart_error.rb', line 385

def redux_store_not_found_solution
  <<~SOLUTION
    1. Register your Redux store:
       #{Rainbow("ReactOnRails.registerStore({ #{additional_context[:store_name]}: #{additional_context[:store_name]} });").cyan}

    2. Ensure the store is imported:
       #{Rainbow("import #{additional_context[:store_name]} from './store/#{additional_context[:store_name]}';").cyan}

    3. Initialize the store before rendering components that depend on it:
       #{Rainbow("<%= redux_store('#{additional_context[:store_name]}', props: {}) %>").cyan}

    4. Check store dependencies in your component:
       #{Rainbow("store_dependencies: ['#{additional_context[:store_name]}']").cyan}
  SOLUTION
end

#server_rendering_error_solutionString

Returns:

  • (String)


365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/react_on_rails/smart_error.rb', line 365

def server_rendering_error_solution
  <<~SOLUTION
    1. Check your JavaScript console output:
       #{Rainbow("tail -f log/development.log | grep 'React on Rails'").cyan}

    2. Common issues:
       - Missing Node.js dependencies: #{Rainbow('cd client && npm install').cyan}
       - Syntax errors in component code
       - Using browser-only APIs without checks

    3. Debug server rendering:
       - Set #{Rainbow('config.trace = true').cyan} in your configuration
       - Set #{Rainbow('config.development_mode = true').cyan} for better errors
       - Check #{Rainbow('config.server_bundle_js_file').cyan} points to correct file

    4. Verify your server bundle:
       #{Rainbow('bin/shakapacker').cyan} or #{Rainbow('bin/webpack').cyan}
  SOLUTION
end

#solutionString

Returns:

  • (String)


122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/react_on_rails/smart_error.rb', line 122

def solution
  case normalized_error_type
  when :component_not_registered
    component_not_registered_solution
  when :missing_auto_loaded_bundle
    missing_auto_loaded_bundle_solution
  when :missing_auto_loaded_store_bundle
    missing_auto_loaded_store_bundle_solution
  when :hydration_mismatch
    hydration_mismatch_solution
  when :server_rendering_error
    server_rendering_error_solution
  when :redux_store_not_found
    redux_store_not_found_solution
  when :configuration_error
    configuration_error_solution
  else
    default_solution
  end
end

#troubleshooting_sectionString

rubocop:enable Metrics/AbcSize

Returns:

  • (String)


446
447
448
# File 'lib/react_on_rails/smart_error.rb', line 446

def troubleshooting_section
  "\n#{Rainbow('🔧 Need More Help?').magenta.bright}\n#{Utils.default_troubleshooting_section}"
end