Skip to content
This repository was archived by the owner on Oct 19, 2018. It is now read-only.

Commit 12949af

Browse files
committed
improved fix for #155
1 parent 2abd065 commit 12949af

File tree

6 files changed

+93
-58
lines changed

6 files changed

+93
-58
lines changed

lib/rails-helpers/top_level_rails_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def self.search_path
1212
param :controller
1313
param :render_params
1414

15-
backtrace :on
15+
backtrace :off
1616

1717
def render
1818
paths_searched = []

lib/react/component.rb

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,40 +43,23 @@ def initialize(native_element)
4343
@native = native_element
4444
end
4545

46-
def render
47-
raise 'no render defined'
48-
end unless method_defined?(:render)
49-
50-
def update_react_js_state(object, name, value)
51-
return if @rendering_now
52-
if object
53-
name = "#{object.class}.#{name}" unless object == self
54-
set_state(
55-
'***_state_updated_at-***' => Time.now.to_f,
56-
name => value
57-
)
58-
else
59-
set_state name => value
60-
end
61-
end
62-
6346
def emit(event_name, *args)
64-
self.params["_on#{event_name.to_s.event_camelize}"].call(*args)
47+
params["_on#{event_name.to_s.event_camelize}"].call(*args)
6548
end
6649

6750
def component_will_mount
6851
IsomorphicHelpers.load_context(true) if IsomorphicHelpers.on_opal_client?
6952
@props_wrapper = self.class.props_wrapper.new(Hash.new(`#{@native}.props`))
7053
set_state! initial_state if initial_state
7154
State.initialize_states(self, initial_state)
72-
State.set_state_context_to(self) { self.run_callback(:before_mount) }
55+
State.set_state_context_to(self) { run_callback(:before_mount) }
7356
rescue Exception => e
7457
self.class.process_exception(e, self)
7558
end
7659

7760
def component_did_mount
7861
State.set_state_context_to(self) do
79-
self.run_callback(:after_mount)
62+
run_callback(:after_mount)
8063
State.update_states_to_observe
8164
end
8265
rescue Exception => e
@@ -118,15 +101,33 @@ def component_will_unmount
118101

119102
attr_reader :waiting_on_resources
120103

104+
def update_react_js_state(object, name, value)
105+
if object
106+
name = "#{object.class}.#{name}" unless object == self
107+
set_state(
108+
'***_state_updated_at-***' => Time.now.to_f,
109+
name => value
110+
)
111+
else
112+
set_state name => value
113+
end
114+
end
115+
116+
def render
117+
raise 'no render defined'
118+
end unless method_defined?(:render)
119+
121120
def _render_wrapper
122-
@rendering_now = true
123-
State.set_state_context_to(self) do
124-
React::RenderingContext.render(nil) {render || ""}.tap { |element| @waiting_on_resources = element.waiting_on_resources if element.respond_to? :waiting_on_resources }
121+
State.set_state_context_to(self, true) do
122+
element = React::RenderingContext.render(nil) { render || '' }
123+
@waiting_on_resources =
124+
element.waiting_on_resources if element.respond_to? :waiting_on_resources
125+
element
125126
end
127+
# rubocop:disable Lint/RescueException # we want to catch all exceptions regardless
126128
rescue Exception => e
129+
# rubocop:enable Lint/RescueException
127130
self.class.process_exception(e, self)
128-
ensure
129-
@rendering_now = false
130131
end
131132

132133
def watch(value, &on_change)
@@ -136,6 +137,5 @@ def watch(value, &on_change)
136137
def define_state(*args, &block)
137138
State.initialize_states(self, self.class.define_state(*args, &block))
138139
end
139-
140140
end
141141
end

lib/react/component/class_methods.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ module ClassMethods
66
def reactrb_component?
77
true
88
end
9-
9+
1010
def backtrace(*args)
1111
@dont_catch_exceptions = (args[0] == :none)
1212
@backtrace_off = @dont_catch_exceptions || (args[0] == :off)
1313
end
1414

15-
def process_exception(e, component, reraise = nil)
16-
message = ["Exception raised while rendering #{component}"]
17-
if e.backtrace && e.backtrace.length > 1 && !@backtrace_off
18-
append_backtrace(message, e.backtrace)
19-
else
20-
message[0] += ": #{e.message}"
15+
def process_exception(e, component, reraise = @dont_catch_exceptions)
16+
unless @dont_catch_exceptions
17+
message = ["Exception raised while rendering #{component}: #{e.message}"]
18+
if e.backtrace && e.backtrace.length > 1 && !@backtrace_off
19+
append_backtrace(message, e.backtrace)
20+
end
21+
`console.error(#{message.join("\n")})`
2122
end
22-
`console.error(#{message.join("\n")})`
23-
raise e if reraise || @dont_catch_exceptions
23+
raise e if reraise
2424
end
2525

2626
def append_backtrace(message_array, backtrace)

lib/react/component/should_component_update.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module Component
2424
#
2525
module ShouldComponentUpdate
2626
def should_component_update?(native_next_props, native_next_state)
27-
State.set_state_context_to(self) do
27+
State.set_state_context_to(self, false) do
2828
next_params = Hash.new(native_next_props)
2929
# rubocop:disable Style/DoubleNegation # we must return true/false to js land
3030
if respond_to?(:needs_update?)

lib/react/state.rb

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ def method_missing(method, *args)
3333
end
3434

3535
class State
36+
37+
@rendering_level = 0
38+
3639
class << self
3740
attr_reader :current_observer
3841

@@ -41,12 +44,23 @@ def initialize_states(object, initial_values) # initialize objects' name/value p
4144
end
4245

4346
def get_state(object, name, current_observer = @current_observer)
44-
# get current value of name for object, remember that the current object depends on this state, current observer can be overriden with last param
47+
# get current value of name for object, remember that the current object depends on this state,
48+
# current observer can be overriden with last param
4549
new_observers[current_observer][object] << name if current_observer && !new_observers[current_observer][object].include?(name)
4650
states[object][name]
4751
end
4852

49-
def set_state2(object, name, value) # set object's name state to value, tell all observers it has changed. Observers must implement update_react_js_state
53+
def set_state(object, name, value, wait_till_thread_completes = nil)
54+
states[object][name] = value
55+
if wait_till_thread_completes
56+
notify_observers_after_thread_completes(object, name, value)
57+
elsif @rendering_level == 0
58+
notify_observers(object, name, value)
59+
end
60+
value
61+
end
62+
63+
def notify_observers(object, name, value)
5064
object_needs_notification = object.respond_to? :update_react_js_state
5165
observers_by_name[object][name].dup.each do |observer|
5266
observer.update_react_js_state(object, name, value)
@@ -55,23 +69,14 @@ def set_state2(object, name, value) # set object's name state to value, tell al
5569
object.update_react_js_state(nil, name, value) if object_needs_notification
5670
end
5771

58-
def set_state(object, name, value, delay=nil)
59-
states[object][name] = value
60-
if delay
61-
@delayed_updates ||= []
62-
@delayed_updates << [object, name, value]
63-
@delayed_updater ||= after(0.001) do
64-
delayed_updates = @delayed_updates
65-
@delayed_updates = []
66-
@delayed_updater = nil
67-
delayed_updates.each do |object, name, value|
68-
set_state2(object, name, value)
69-
end
70-
end
71-
else
72-
set_state2(object, name, value)
72+
def notify_observers_after_thread_completes(object, name, value)
73+
(@delayed_updates ||= []) << [object, name, value]
74+
@delayed_updater ||= after(0) do
75+
delayed_updates = @delayed_updates
76+
@delayed_updates = []
77+
@delayed_updater = nil
78+
delayed_updates.each { |args| notify_observers(*args) }
7379
end
74-
value
7580
end
7681

7782
def will_be_observing?(object, name, current_observer)
@@ -108,18 +113,20 @@ def remove # call after component is unmounted
108113
current_observers.delete(@current_observer)
109114
end
110115

111-
def set_state_context_to(observer) # wrap all execution that may set or get states in a block so we know which observer is executing
116+
def set_state_context_to(observer, rendering = nil) # wrap all execution that may set or get states in a block so we know which observer is executing
112117
if `typeof window.reactive_ruby_timing !== 'undefined'`
113118
@nesting_level = (@nesting_level || 0) + 1
114119
start_time = Time.now.to_f
115120
observer_name = (observer.class.respond_to?(:name) ? observer.class.name : observer.to_s) rescue "object:#{observer.object_id}"
116121
end
117122
saved_current_observer = @current_observer
118123
@current_observer = observer
124+
@rendering_level += 1 if rendering
119125
return_value = yield
120126
return_value
121127
ensure
122128
@current_observer = saved_current_observer
129+
@rendering_level -= 1 if rendering
123130
@nesting_level = [0, @nesting_level - 1].max if `typeof window.reactive_ruby_timing !== 'undefined'`
124131
return_value
125132
end
@@ -130,11 +137,10 @@ def states
130137

131138
[:new_observers, :current_observers, :observers_by_name].each do |method_name|
132139
define_method(method_name) do
133-
instance_variable_get("@#{method_name}") or
140+
instance_variable_get("@#{method_name}") ||
134141
instance_variable_set("@#{method_name}", Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } })
135142
end
136143
end
137-
138144
end
139145
end
140146
end

spec/react/state_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,34 @@
2222
# React::State.set_state(object, attribute, value) +
2323
# React::State.get_state(object, attribute)
2424
it "can be accessed outside of react using get/set_state"
25+
26+
it 'ignores state updates during rendering' do
27+
stub_const 'StateTest', Class.new(React::Component::Base)
28+
StateTest.class_eval do
29+
export_state :boom
30+
before_mount do
31+
# force boom to be on the observing list during the current rendering cycle
32+
StateTest.boom! !StateTest.boom
33+
# this is automatically called by after_mount / after_update, but we don't want
34+
# to have to setup a complicated async test, so we just force it now.
35+
# if we don't do this, then updating boom will have no effect on the first render
36+
React::State.update_states_to_observe
37+
end
38+
def render
39+
(StateTest.boom ? "Boom" : "No Boom").tap { StateTest.boom! !StateTest.boom }
40+
end
41+
end
42+
%x{
43+
var log = [];
44+
var org_warn_console = window.console.warn;
45+
var org_error_console = window.console.error;
46+
window.console.warn = window.console.error = function(str){log.push(str)}
47+
}
48+
markup = React.render_to_static_markup(React.create_element(StateTest))
49+
`window.console.warn = org_warn_console; window.console.error = org_error_console;`
50+
expect(markup).to eq('<span>Boom</span>')
51+
expect(StateTest.boom).to be_falsy
52+
expect(`log`).to eq([])
53+
end
2554
end
2655
end

0 commit comments

Comments
 (0)