Skip to content

Dumping exceptions in live editor #26

@barriehadfield

Description

@barriehadfield

The live editor can crash the whole page if the user adds one of the lifecycle methods (before_mount, etc). The only fix so far is the patch below which catches and dumps an exception. Each change has comment #barrie

We need to find a better solution and this needs to be removed before the rename.

module React
find_and_render_component
  class NativeLibrary
    class << self
      def imports(native_name)
        @native_prefix = "#{native_name}."
        self
      end

      def rename(rename_list)
        # rename_list is a hash in the form: native_name => ruby_name, native_name => ruby_name
        rename_list.each do |js_name, ruby_name|
          native_name = lookup_native_name(js_name)
          if lookup_native_name(js_name)
            create_component_wrapper(self, native_name, ruby_name) ||
              create_library_wrapper(self, native_name, ruby_name)
          else
            raise "class #{name} < React::NativeLibrary could not import #{js_name}. "\
            "Native value #{scope_native_name(js_name)} is undefined."
          end
        end
      end

      def import_const_from_native(klass, const_name, create_library)
        begin
          native_name = lookup_native_name(const_name) ||
                        lookup_native_name(const_name[0].downcase + const_name[1..-1])
          native_name && (
            create_component_wrapper(klass, native_name, const_name) || (
              create_library &&
                create_library_wrapper(klass, native_name, const_name)))
        rescue Exception
          # barrie
        end
      end

      def const_missing(const_name)
        import_const_from_native(self, const_name, true) || super
      end

      def method_missing(method, *args, &block)
        component_class = const_get(method) if const_defined?(method, false)
        component_class ||= import_const_from_native(self, method, false)
        raise 'could not import a react component named: '\
              "#{scope_native_name method}" unless component_class
        React::RenderingContext.render(component_class, *args, &block)
      end

      private

      def lookup_native_name(js_name)
        native_name = scope_native_name(js_name)
        `eval(#{native_name}) !== undefined && native_name`
      # rubocop:disable Lint/RescueException  # that is what eval raises in Opal >= 0.10.
      rescue Exception
        nil
        # rubocop:enable Lint/RescueException
      end

      def scope_native_name(js_name)
        "#{@native_prefix}#{js_name}"
      end

      def create_component_wrapper(klass, native_name, ruby_name)
        if React::API.native_react_component?(native_name)
          new_klass = klass.const_set ruby_name, Class.new
          new_klass.class_eval do
            include Hyperloop::Component::Mixin
            imports native_name
          end
          new_klass
        end
      end

      def create_library_wrapper(klass, native_name, ruby_name)
        klass.const_set ruby_name, Class.new(React::NativeLibrary).imports(native_name)
      end
    end
  end
end

module Hyperloop
  class Component
    class << self
      def mounted_components
        @mounted_components ||= Set.new
      end

      def force_update!
        components = mounted_components.to_a
        components.each do |comp|
          next unless mounted_components.include? comp
          comp.force_update!
        end
      end
    end

    module Mixin
      def self.included(base)
        base.include(Hyperloop::Store::Mixin)
        base.include(React::Component::API)
        base.include(React::Callbacks)
        base.include(React::Component::Tags)
        base.include(React::Component::DslInstanceMethods)
        base.include(React::Component::ShouldComponentUpdate)
        base.class_eval do
          class_attribute :initial_state
          define_callback :before_mount
          define_callback :after_mount
          define_callback :before_receive_props
          define_callback :before_update
          define_callback :after_update
          define_callback :before_unmount
          define_callback(:after_error) { React::API.add_after_error_hook(base) }
        end
        base.extend(React::Component::ClassMethods)
      end

      def self.deprecation_warning(message)
        React::Component.deprecation_warning(name, message)
      end

      def deprecation_warning(message)
        React::Component.deprecation_warning(self.class.name, message)
      end

      def initialize(native_element)
        @native = native_element
        init_store
      end

      def emit(event_name, *args)
        if React::Event::BUILT_IN_EVENTS.include?(built_in_event_name = "on#{event_name.to_s.event_camelize}")
          params[built_in_event_name].call(*args)
        else
          params["on_#{event_name}"].call(*args)
        end
      end

      def component_will_mount
        begin
          React::IsomorphicHelpers.load_context(true) if React::IsomorphicHelpers.on_opal_client?
          React::State.set_state_context_to(self) do
            Hyperloop::Component.mounted_components << self
            run_callback(:before_mount)
          end
        rescue Exception
          # barrie
        end
      end

      def component_did_mount
        begin
          React::State.set_state_context_to(self) do
            run_callback(:after_mount)
            React::State.update_states_to_observe
          end
        rescue Exception
          # barrie
        end
      end

      def component_will_receive_props(next_props)
        # need to rethink how this works in opal-react, or if its actually that useful within the react.rb environment
        # for now we are just using it to clear processed_params
        begin
          React::State.set_state_context_to(self) { run_callback(:before_receive_props, next_props) }
          @_receiving_props = true
        rescue Exception
          # barrie
        end
      end

      def component_will_update(next_props, next_state)
        begin
          React::State.set_state_context_to(self) { run_callback(:before_update, next_props, next_state) }
          params._reset_all_others_cache if @_receiving_props
          @_receiving_props = false
        rescue Exception
          # barrie
        end
      end

      def component_did_update(prev_props, prev_state)
        begin
          React::State.set_state_context_to(self) do
            run_callback(:after_update, prev_props, prev_state)
            React::State.update_states_to_observe
          end
        rescue Exception
          # barrie
        end
      end

      def component_will_unmount
        begin
          React::State.set_state_context_to(self) do
            run_callback(:before_unmount)
            React::State.remove
            Hyperloop::Component.mounted_components.delete self
          end
        rescue Exception
          # barrie
        end
      end

      def component_did_catch(error, info)
        React::State.set_state_context_to(self) do
          run_callback(:after_error, error, info)
        end
      end

      attr_reader :waiting_on_resources

      def update_react_js_state(object, name, value)
        if object
          name = "#{object.class}.#{name}" unless object == self
          # Date.now() has only millisecond precision, if several notifications of
          # observer happen within a millisecond, updates may get lost.
          # to mitigate this the Math.random() appends some random number
          # this way notifactions will happen as expected by the rest of hyperloop
          set_state(
            '***_state_updated_at-***' => `Date.now() + Math.random()`,
            name => value
          )
        else
          set_state name => value
        end
      end

      def set_state_synchronously?
        @native.JS[:__opalInstanceSyncSetState]
      end

      def render
        raise 'no render defined'
      end unless method_defined?(:render)

      def _render_wrapper
        React::State.set_state_context_to(self, true) do
          element = React::RenderingContext.render(nil) { render || '' }
          @waiting_on_resources =
            element.waiting_on_resources if element.respond_to? :waiting_on_resources
          element
        end
      end

      def watch(value, &on_change)
        Observable.new(value, on_change)
      end

      def define_state(*args, &block)
        React::State.initialize_states(self, self.class.define_state(*args, &block))
      end
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions