简体   繁体   中英

Passing models to components

Using the hyperstack.org framework, how can I reduce the rendering cycles when mutating models that are being rendered?

When passing a Model which is being rendered to a Component which mutates that Model, all Components rendering that Model get re-rendered on any mutation. This is fine unless the mutation is per key press as this means that all Components get re-rendered per key press.

For example, if we have this table:

class UserIndex < HyperComponent
  render(DIV) do
    puts "UserIndex render"
    BridgeAppBar()
    UserDialog(user: User.new)
    Table do
      TableHead do
        TableRow do
          TableCell { 'Name' }
          TableCell { 'Gender' }
          TableCell { 'Edit' }
        end
      end
      TableBody do
        user_rows
      end
    end
  end

  def user_rows
    User.each do |user|
      TableRow do
        TableCell { "#{user.first_name} #{user.last_name}" }
        TableCell { user.is_female ? 'Female' : 'Male' }
        TableCell { UserDialog(user: user) }
      end
    end
  end
end

And this Compnent (which is used for edit and new):

class UserDialog < HyperComponent
  param :user

  before_mount do
    @open = false
  end

  render do
    puts "UserDialog render"
    if @open
      render_dialog
    else
      edit_or_new_button.on(:click) { mutate @open = true }
    end
  end

  def render_dialog
    Dialog(open: @open, fullWidth: false) do
      DialogTitle do
        'User'
      end
      DialogContent do
        content
        error_messages if @User.errors.any?
      end
      DialogActions do
        actions
      end
    end
  end

  def edit_or_new_button
    if @User.new?
      Fab(size: :small, color: :primary) { Icon { 'add' } }
    else
      Fab(size: :small, color: :secondary) { Icon { 'settings' } }
    end
  end

  def content
    FormGroup(row: true) do
      TextField(label: 'First Name', defaultValue: @User.first_name.to_s).on(:change) do |e|
        @User.first_name = e.target.value
      end
      TextField(label: 'Last Name', defaultValue: @User.last_name.to_s).on(:change) do |e|
        @User.last_name = e.target.value
      end
    end

    BR()

    FormLabel(component: 'legend') { 'Gender' }
    RadioGroup(row: true) do
      FormControlLabel(label: 'Male',
                       control: Radio(value: false, checked: !@User.is_female).as_node.to_n)
      FormControlLabel(label: 'Female',
                       control: Radio(value: true, checked: @User.is_female).as_node.to_n)
    end.on(:change) do |e|
      @User.is_female = e.target.value
    end
  end

  def actions
    Button { 'Cancel' }.on(:click) { cancel }

    if @User.changed? && validate_content
      Button(color: :primary, variant: :contained, disabled: (@User.saving? ? true : false)) do
        'Save'
      end.on(:click) { save }
    end
  end

  def save
    @User.save(validate: true).then do |result|
      mutate @open = false if result[:success]
    end
  end

  def cancel
    @User.revert
    mutate @open = false
  end

  def error_messages
    @User.errors.full_messages.each do |message|
      Typography(variant: :h6, color: :secondary) { message }
    end
  end

  def validate_content
    return false if @User.first_name.to_s.empty?
    return false if @User.last_name.to_s.empty?
    return false if @User.is_female.nil?

    true
  end

end

The underlying table (from the first code example) is re-rendered on every keypress, caused by:

TextField(label: 'First Name', defaultValue: @User.first_name.to_s)
.on(:change) do |e|
    @User.first_name = e.target.value
end

This is causing typing to appear sluggish due to the amount of re-rendering.

Should I be keeping a local state variable for each field then only mutating the model fields on save?

Looks like you are using Material UI which will dynamically size tables to be best fit the content. So I suspect what is happening is that you are displaying the value of first_name and last_name in the MUI table, while you editing the values in the Dialog box.

So MUI is constantly recalculating the size of the MUI table columns as each character is typed.

Not only is this going to slow things down, but its also going to be disconcerting to the human user. It will give the impression that the changes they are making all already taking effect even before you have saved them.

So yes I think the best approach is to not directly update the state of the record while the user is typing but rather update a local state variable. Then only when the user saves, do you update the actual record.

I do notice that you have defaultValue which indicates an "uncontrolled" input. But you are reacting to every change in the input, which is the "controlled" behavior. I think you can change defaultValue to value .

class UserDialog < HyperComponent
  param :user

  before_mount do
    @open = false
    @first_name = @User.first_name
    @last_name = @User.last_name 
    @is_female = @User.is_female
  end

  render do
    puts "UserDialog render"
    if @open
      render_dialog
    else
      edit_or_new_button.on(:click) { mutate @open = true }
    end
  end

  def render_dialog
    Dialog(open: @open, fullWidth: false) do
      DialogTitle do
        'User'
      end
      DialogContent do
        content
        error_messages if @User.errors.any?
      end
      DialogActions do
        actions
      end
    end
  end

  def edit_or_new_button
    if @User.new?
      Fab(size: :small, color: :primary) { Icon { 'add' } }
    else
      Fab(size: :small, color: :secondary) { Icon { 'settings' } }
    end
  end

  def content
    FormGroup(row: true) do
      TextField(label: 'First Name', value: @first_name).on(:change) do |e|
        mutate @first_name = e.target.value
      end
      TextField(label: 'Last Name', value: @last_name).on(:change) do |e|
        mutate @last_name = e.target.value
      end
    end

    BR()

    FormLabel(component: 'legend') { 'Gender' }
    RadioGroup(row: true) do
      FormControlLabel(label: 'Male',
                       control: Radio(value: false, checked: !@is_female).as_node.to_n)
      FormControlLabel(label: 'Female',
                       control: Radio(value: true, checked: @is_female).as_node.to_n)
    end.on(:change) do |e|
      mutate @is_female = e.target.value
    end
  end

  def actions
    Button { 'Cancel' }.on(:click) { cancel }

    return unless ready_to_save?
    Button(color: :primary, variant: :contained, disabled: (@User.saving? ? true : false)) do
      'Save'
    end.on(:click, &:save)
  end

  def save
    @User.update(first_name: @first_name, last_name: @last_name, is_female: @is_female).then do |result|
      mutate @open = false if result[:success]
    end
  end

  def cancel
    mutate @open = false
  end

  def error_messages
    @User.errors.full_messages.each do |message|
      Typography(variant: :h6, color: :secondary) { message }
    end
  end

  def ready_to_save?
    return false if @first_name.empty?
    return false if @last_name.empty?
    return false if @is_female.nil?
    return true if @first_name != @User.first_name
    return true if @last_name != @User.last_name
    return true if @is_female != @User.is_female
  end

end

As it turned out the things that were causing the performance problem was that I was not passing a unique key to the items in the list. React is very particular about this yet this is not something you get warnings about.

All I had to change was:

User.each do |user|
    TableRow do
        ...
        TableCell { UserDialog(user: user) }
    end
end

To:

User.each do |user|
    TableRow do
        ...
        # this passes a unique key to each Component
        TableCell { UserDialog(user: user, key: user) } 
    end
end

With the above change everything works perfectly in both examples (the first being where the underlying table is updated as the user types and the second, provided by @catmando, where the changes are only applied on save.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM