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

Commit 04f7f44

Browse files
catmandozetachang
authored andcommitted
closes #155
closes #155 deprecates shallow compare per #156 improved fix for #155 # Conflicts: # lib/react/state.rb # spec/spec_helper.rb
1 parent 1933a3a commit 04f7f44

File tree

10 files changed

+296
-90
lines changed

10 files changed

+296
-90
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,3 +1154,6 @@ Style/WordArray:
11541154
Description: 'Use %w or %W for arrays of words.'
11551155
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
11561156
Enabled: false
1157+
1158+
Style/CommandLiteral:
1159+
EnforcedStyle: mixed

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: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def self.included(base)
1717
base.include(Callbacks)
1818
base.include(Tags)
1919
base.include(DslInstanceMethods)
20+
base.include(ShouldComponentUpdate)
2021
base.class_eval do
2122
class_attribute :initial_state
2223
define_callback :before_mount
@@ -42,34 +43,22 @@ def initialize(native_element)
4243
@native = native_element
4344
end
4445

45-
def render
46-
raise "no render defined"
47-
end unless method_defined?(:render)
48-
49-
def update_react_js_state(object, name, value)
50-
if object
51-
set_state({"***_state_updated_at-***" => Time.now.to_f, "#{object.class.to_s+'.' unless object == self}#{name}" => value})
52-
else
53-
set_state({name => value})
54-
end rescue nil
55-
end
56-
5746
def emit(event_name, *args)
58-
self.params["_on#{event_name.to_s.event_camelize}"].call(*args)
47+
params["_on#{event_name.to_s.event_camelize}"].call(*args)
5948
end
6049

6150
def component_will_mount
6251
IsomorphicHelpers.load_context(true) if IsomorphicHelpers.on_opal_client?
6352
set_state! initial_state if initial_state
6453
State.initialize_states(self, initial_state)
65-
State.set_state_context_to(self) { self.run_callback(:before_mount) }
54+
State.set_state_context_to(self) { run_callback(:before_mount) }
6655
rescue Exception => e
6756
self.class.process_exception(e, self)
6857
end
6958

7059
def component_did_mount
7160
State.set_state_context_to(self) do
72-
self.run_callback(:after_mount)
61+
run_callback(:after_mount)
7362
State.update_states_to_observe
7463
end
7564
rescue Exception => e
@@ -84,32 +73,6 @@ def component_will_receive_props(next_props)
8473
self.class.process_exception(e, self)
8574
end
8675

87-
def props_changed?(next_props)
88-
return true unless props.keys.sort == next_props.keys.sort
89-
props.detect { |k, v| `#{next_props[k]} != #{params[k]}`}
90-
end
91-
92-
def should_component_update?(next_props, next_state)
93-
State.set_state_context_to(self) do
94-
next_props = Hash.new(next_props)
95-
if self.respond_to?(:needs_update?)
96-
!!self.needs_update?(next_props, Hash.new(next_state))
97-
elsif false # switch to true to force updates per standard react
98-
true
99-
elsif props_changed? next_props
100-
true
101-
elsif `!next_state != !#{@native}.state`
102-
true
103-
elsif `!next_state && !#{@native}.state`
104-
false
105-
elsif `next_state["***_state_updated_at-***"] != #{@native}.state["***_state_updated_at-***"]`
106-
true
107-
else
108-
false
109-
end.to_n
110-
end
111-
end
112-
11376
def component_will_update(next_props, next_state)
11477
State.set_state_context_to(self) { self.run_callback(:before_update, Hash.new(next_props), Hash.new(next_state)) }
11578
rescue Exception => e
@@ -136,11 +99,32 @@ def component_will_unmount
13699

137100
attr_reader :waiting_on_resources
138101

102+
def update_react_js_state(object, name, value)
103+
if object
104+
name = "#{object.class}.#{name}" unless object == self
105+
set_state(
106+
'***_state_updated_at-***' => Time.now.to_f,
107+
name => value
108+
)
109+
else
110+
set_state name => value
111+
end
112+
end
113+
114+
def render
115+
raise 'no render defined'
116+
end unless method_defined?(:render)
117+
139118
def _render_wrapper
140-
State.set_state_context_to(self) do
141-
React::RenderingContext.render(nil) {render || ""}.tap { |element| @waiting_on_resources = element.waiting_on_resources if element.respond_to? :waiting_on_resources }
119+
State.set_state_context_to(self, true) do
120+
element = React::RenderingContext.render(nil) { render || '' }
121+
@waiting_on_resources =
122+
element.waiting_on_resources if element.respond_to? :waiting_on_resources
123+
element
142124
end
125+
# rubocop:disable Lint/RescueException # we want to catch all exceptions regardless
143126
rescue Exception => e
127+
# rubocop:enable Lint/RescueException
144128
self.class.process_exception(e, self)
145129
end
146130

@@ -151,6 +135,5 @@ def watch(value, &on_change)
151135
def define_state(*args, &block)
152136
State.initialize_states(self, self.class.define_state(*args, &block))
153137
end
154-
155138
end
156139
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)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
module React
2+
module Component
3+
#
4+
# React assumes all components should update, unless a component explicitly overrides
5+
# the shouldComponentUpdate method. Reactrb does an explicit check doing a shallow
6+
# compare of params, and using a timestamp to determine if state has changed.
7+
8+
# If needed components can provide their own #needs_update? method which will be
9+
# passed the next params and state opal hashes.
10+
11+
# Attached to these hashes is a #changed? method that returns whether the hash contains
12+
# changes as calculated by the base mechanism. This way implementations of #needs_update?
13+
# can use the base comparison mechanism as needed.
14+
15+
# For example
16+
# def needs_update?(next_params, next_state)
17+
# # use a special comparison method
18+
# return false if next_state.changed? || next_params.changed?
19+
# # do some other special checks
20+
# end
21+
22+
# Note that beginning in 0.9 we will use standard ruby compare on all params further reducing
23+
# the need for needs_update?
24+
#
25+
module ShouldComponentUpdate
26+
def should_component_update?(native_next_props, native_next_state)
27+
State.set_state_context_to(self, false) do
28+
next_params = Hash.new(native_next_props)
29+
# rubocop:disable Style/DoubleNegation # we must return true/false to js land
30+
if respond_to?(:needs_update?)
31+
!!call_needs_update(next_params, native_next_state)
32+
else
33+
!!(props_changed?(next_params) || native_state_changed?(native_next_state))
34+
end
35+
# rubocop:enable Style/DoubleNegation
36+
end
37+
end
38+
39+
# create opal hashes for next params and state, and attach
40+
# the changed? method to each hash
41+
42+
def call_needs_update(next_params, native_next_state)
43+
component = self
44+
next_params.define_singleton_method(:changed?) do
45+
component.props_changed?(self)
46+
end
47+
next_state = Hash.new(native_next_state)
48+
next_state.define_singleton_method(:changed?) do
49+
component.native_state_changed?(native_next_state)
50+
end
51+
needs_update?(next_params, next_state)
52+
end
53+
54+
# Whenever state changes, reactrb updates a timestamp on the state object.
55+
# We can rapidly check for state changes comparing the incoming state time_stamp
56+
# with the current time stamp.
57+
58+
# Different versions of react treat empty state differently, so we first
59+
# convert anything that looks like an empty state to "false" for consistency.
60+
61+
# Then we test if one state is empty and the other is not, then we return false.
62+
# Then we test if both states are empty we return true.
63+
# If either state does not have a time stamp then we have to assume a change.
64+
# Otherwise we check time stamps
65+
66+
# rubocop:disable Metrics/MethodLength # for effeciency we want this to be one method
67+
def native_state_changed?(next_state)
68+
%x{
69+
var current_state = #{@native}.state
70+
var normalized_next_state =
71+
!#{next_state} || Object.keys(#{next_state}).length === 0 || #{nil} == #{next_state} ?
72+
false : #{next_state}
73+
var normalized_current_state =
74+
!current_state || Object.keys(current_state).length === 0 || #{nil} == current_state ?
75+
false : current_state
76+
if (!normalized_current_state != !normalized_next_state) return(true)
77+
if (!normalized_current_state && !normalized_next_state) return(false)
78+
if (!normalized_current_state['***_state_updated_at-***'] ||
79+
!normalized_next_state['***_state_updated_at-***']) return(true)
80+
return (normalized_current_state['***_state_updated_at-***'] !=
81+
normalized_next_state['***_state_updated_at-***'])
82+
}
83+
end
84+
# rubocop:enable Metrics/MethodLength
85+
86+
# Do a shallow compare on the two hashes. Starting in 0.9 we will do a deep compare.
87+
88+
def props_changed?(next_params)
89+
Component.deprecation_warning(
90+
"Using shallow incoming params comparison.\n"\
91+
'Do a require "reactrb/deep-compare, to get 0.9 behavior'
92+
)
93+
(props.keys.sort != next_params.keys.sort) ||
94+
next_params.detect { |k, v| `#{v} != #{@native}.props[#{k}]` }
95+
end
96+
end
97+
end
98+
end

lib/react/state.rb

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,34 @@ 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

39-
def initialize_states(object, initial_values)
40-
# initialize objects' name/value pairs
42+
def initialize_states(object, initial_values) # initialize objects' name/value pairs
4143
states[object].merge!(initial_values || {})
4244
end
4345

4446
def get_state(object, name, current_observer = @current_observer)
45-
# get current value of name for object, remember that the current object
46-
# depends on this state, current observer can be overriden with last
47-
# param
48-
new_observers[current_observer][object] << name if current_observer &&
49-
!new_observers[current_observer][object].include?(name)
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
49+
new_observers[current_observer][object] << name if current_observer && !new_observers[current_observer][object].include?(name)
5050
states[object][name]
5151
end
5252

53-
def set_state2(object, name, value)
54-
# set object's name state to value, tell all observers it has changed.
55-
# 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)
5664
object_needs_notification = object.respond_to? :update_react_js_state
5765
observers_by_name[object][name].dup.each do |observer|
5866
observer.update_react_js_state(object, name, value)
@@ -61,23 +69,14 @@ def set_state2(object, name, value)
6169
object.update_react_js_state(nil, name, value) if object_needs_notification
6270
end
6371

64-
def set_state(object, name, value, delay=nil)
65-
states[object][name] = value
66-
if delay
67-
@delayed_updates ||= []
68-
@delayed_updates << [object, name, value]
69-
@delayed_updater ||= after(0.001) do
70-
delayed_updates = @delayed_updates
71-
@delayed_updates = []
72-
@delayed_updater = nil
73-
delayed_updates.each do |object, name, value|
74-
set_state2(object, name, value)
75-
end
76-
end
77-
else
78-
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) }
7979
end
80-
value
8180
end
8281

8382
def will_be_observing?(object, name, current_observer)
@@ -88,9 +87,7 @@ def is_observing?(object, name, current_observer)
8887
current_observer && observers_by_name[object][name].include?(current_observer)
8988
end
9089

91-
# should be called after the last after_render callback, currently called
92-
# after components render method
93-
def update_states_to_observe(current_observer = @current_observer)
90+
def update_states_to_observe(current_observer = @current_observer) # should be called after the last after_render callback, currently called after components render method
9491
raise "update_states_to_observer called outside of watch block" unless current_observer
9592
current_observers[current_observer].each do |object, names|
9693
names.each do |name|
@@ -116,23 +113,21 @@ def remove # call after component is unmounted
116113
current_observers.delete(@current_observer)
117114
end
118115

119-
# wrap all execution that may set or get states in a block so we know
120-
# which observer is executing
121-
def set_state_context_to(observer)
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
122117
if `typeof window.reactive_ruby_timing !== 'undefined'`
123118
@nesting_level = (@nesting_level || 0) + 1
124119
start_time = Time.now.to_f
125120
observer_name = (observer.class.respond_to?(:name) ? observer.class.name : observer.to_s) rescue "object:#{observer.object_id}"
126121
end
127122
saved_current_observer = @current_observer
128123
@current_observer = observer
124+
@rendering_level += 1 if rendering
129125
return_value = yield
130126
return_value
131127
ensure
132128
@current_observer = saved_current_observer
133-
if `typeof window.reactive_ruby_timing !== 'undefined'`
134-
@nesting_level = [0, @nesting_level - 1].max
135-
end
129+
@rendering_level -= 1 if rendering
130+
@nesting_level = [0, @nesting_level - 1].max if `typeof window.reactive_ruby_timing !== 'undefined'`
136131
return_value
137132
end
138133

@@ -142,7 +137,7 @@ def states
142137

143138
[:new_observers, :current_observers, :observers_by_name].each do |method_name|
144139
define_method(method_name) do
145-
instance_variable_get("@#{method_name}") or
140+
instance_variable_get("@#{method_name}") ||
146141
instance_variable_set("@#{method_name}", Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } })
147142
end
148143
end

lib/reactrb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require 'react/observable'
2121
require 'react/component'
2222
require 'react/component/dsl_instance_methods'
23+
require 'react/component/should_component_update'
2324
require 'react/component/tags'
2425
require 'react/component/base'
2526
require 'react/element'

0 commit comments

Comments
 (0)