Передача моделей на компоненты
Используя каркас hyperstack.org, как я могу уменьшить циклы рендеринга при изменении моделей, которые рендерится?
При передаче модели, которая отображается для компонента, который мутирует эту модель, все компоненты, отображающие эту модель, повторно отображаются при любой мутации. Это нормально, если мутация не происходит при нажатии клавиши, так как это означает, что все компоненты будут перерисованы при нажатии клавиши.
Например, если у нас есть эта таблица:
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
Базовая таблица (из первого примера кода) перерисовывается при каждом нажатии клавиши, вызванная:
TextField(label: 'First Name', defaultValue: @User.first_name.to_s)
.on(:change) do |e|
@User.first_name = e.target.value
end
Это приводит к тому, что набор текста выглядит вялым из-за повторного рендеринга.
Должен ли я хранить локальную переменную состояния для каждого поля, а затем изменять только поля модели при сохранении?
2 ответа
Как выяснилось, проблема производительности была в том, что я не передавал уникальный ключ элементам в списке. Реагируйте очень внимательно по этому поводу, но вы не получаете предупреждений.
Все, что я должен был изменить, было:
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
С указанным выше изменением все отлично работает в обоих примерах (первый, где базовая таблица обновляется по типу пользователя, и второй, предоставленный @catmando, где изменения применяются только при сохранении.
Похоже, вы используете Material UI, который будет динамически изменять размер таблиц, чтобы наилучшим образом соответствовать содержанию. Поэтому я подозреваю, что происходит то, что вы отображаете значение first_name
а также last_name
в таблице многоязыкового интерфейса пользователя при редактировании значений в диалоговом окне.
Таким образом, MUI постоянно пересчитывает размер столбцов таблицы MUI при вводе каждого символа.
Это не только замедлит ход событий, но и приведет в замешательство человека-пользователя. Создается впечатление, что все изменения, которые они вносят, уже вступают в силу еще до того, как вы их сохранили.
Так что да, я думаю, что лучший подход - не обновлять состояние записи напрямую, пока пользователь печатает, а обновлять локальную переменную состояния. Тогда только когда пользователь сохранит, вы обновите фактическую запись.
Я заметил, что у вас есть defaultValue
что указывает на "неконтролируемый" вход. Но вы реагируете на каждое изменение ввода, которое является "контролируемым" поведением. Я думаю, что вы можете изменить 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