将模型传递给组件

Passing models to components

使用 hyperstack.org 框架,如何在改变正在渲染的模型时减少渲染周期?

当将正在渲染的模型传递给会改变该模型的组件时,渲染该模型的所有组件都会在发生任何改变时重新渲染。这很好,除非突变是每次按键,因为这意味着所有组件都会在每次按键时重新呈现。

例如,如果我们有这个 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

而这个组件(用于编辑和新建):

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

基础 table(来自第一个代码示例)在每次按键时重新呈现,原因是:

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

由于重新渲染的数量,这导致打字显得迟缓。

我是否应该为每个字段保留一个本地状态变量,然后只在保存时改变模型字段?

看起来您正在使用 Material UI,它将动态调整 table 的大小以最适合内容。所以我怀疑正在发生的事情是您在编辑对话框中的值时在 MUI table 中显示 first_namelast_name 的值。

所以 MUI 在键入每个字符时不断地重新计算 MUI table 列的大小。

这不仅会减慢速度,还会让人类用户感到不安。这会给人一种印象,即他们所做的更改甚至在您保存之前就已经生效了。

所以是的,我认为最好的方法是在用户键入时不直接更新记录的状态,而是更新本地状态变量。然后只有当用户保存时,你才更新实际记录。

我确实注意到您有 defaultValue,这表示 "uncontrolled" 输入。但是您正在对输入的每个变化做出反应,这就是 "controlled" 行为。我认为您可以将 defaultValue 更改为 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

事实证明,导致性能问题的原因是我没有将唯一键传递给列表中的项目。 React 对此非常讲究,但您不会收到警告。

我需要改变的是:

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

收件人:

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

通过上述更改,两个示例中的一切都完美运行(第一个是基础 table 随着用户输入而更新,第二个由@catmando 提供,更改仅在保存时应用.