Refactor LayerApplicabilityDialog and RulesCard components to utilize useKeyedList for managing lists, improving state handling and performance. Update ScheduleGrid to enhance key assignment for mapped elements, ensuring better React reconciliation. Modify ManualInboxDialog to use unique keys for parts in sample name processing, and improve ToastProvider and ThemeProvider by memoizing context values to prevent unnecessary re-renders.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
export type KeyedRow<T> = { key: string; value: T }
|
||||
|
||||
const toRows = <T,>(values: readonly T[]): KeyedRow<T>[] =>
|
||||
values.map((value) => ({ key: crypto.randomUUID(), value }))
|
||||
|
||||
/**
|
||||
* Редактируемый список со стабильными ключами строк.
|
||||
*
|
||||
* Индекс в качестве `key` не годится там, где строки удаляются из середины или переставляются:
|
||||
* React сопоставит уцелевшие DOM-узлы не с теми значениями, и фокус вместе с внутренним состоянием
|
||||
* полей переедет в соседнюю строку. Ключ существует только на клиенте и наружу не уезжает —
|
||||
* в API отправляется `values`.
|
||||
*
|
||||
* Заодно правки строк перестают быть замыканиями по индексу внутри JSX: `patch`/`remove` работают
|
||||
* по ключу и не зависят от текущей позиции строки в списке.
|
||||
*/
|
||||
export function useKeyedList<T>(initial: readonly T[]) {
|
||||
const [rows, setRows] = useState<KeyedRow<T>[]>(() => toRows(initial))
|
||||
|
||||
const reset = useCallback((values: readonly T[]) => setRows(toRows(values)), [])
|
||||
|
||||
const add = useCallback((value: T) => setRows((c) => [...c, ...toRows([value])]), [])
|
||||
|
||||
const remove = useCallback(
|
||||
(key: string) => setRows((c) => c.filter((row) => row.key !== key)),
|
||||
[],
|
||||
)
|
||||
|
||||
const patch = useCallback(
|
||||
(key: string, next: (value: T) => T) =>
|
||||
setRows((c) =>
|
||||
c.map((row) => (row.key === key ? { key: row.key, value: next(row.value) } : row)),
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
return { rows, values: rows.map((row) => row.value), reset, add, remove, patch }
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
|
||||
type ToastVariant = 'default' | 'success' | 'error'
|
||||
export type ToastItem = { id: number; message: string; variant: ToastVariant }
|
||||
@@ -26,7 +33,11 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
pushImpl = push
|
||||
|
||||
return <ToastContext value={{ toasts, dismiss }}>{children}</ToastContext>
|
||||
// Литерал в value пересоздавался бы на каждый рендер провайдера и перерисовывал всех потребителей
|
||||
// контекста, даже когда список тостов не менялся.
|
||||
const value = useMemo(() => ({ toasts, dismiss }), [toasts, dismiss])
|
||||
|
||||
return <ToastContext value={value}>{children}</ToastContext>
|
||||
}
|
||||
|
||||
export function useToastContext() {
|
||||
|
||||
@@ -36,14 +36,15 @@ function ToastItem({
|
||||
|
||||
// Живой регион остаётся обычным контейнером, а закрытие висит на настоящей кнопке: обработчик
|
||||
// клика на самом сообщении недоступен с клавиатуры, и скринридер о нём никак не сообщает.
|
||||
// <output> — нативный живой регион: роль status у него подразумевается, и поддержка у
|
||||
// скринридеров лучше, чем у div с проставленной ролью.
|
||||
return (
|
||||
<div
|
||||
<output
|
||||
className={cn(
|
||||
'crt-panel pointer-events-auto flex items-start gap-3 rounded-md px-4 py-3 text-sm shadow-lg',
|
||||
variant === 'success' && 'border-primary/60',
|
||||
variant === 'error' && 'border-red-700/60 text-red-400',
|
||||
)}
|
||||
role="status"
|
||||
>
|
||||
<span className="flex-1">{message}</span>
|
||||
<button
|
||||
@@ -54,6 +55,6 @@ function ToastItem({
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</output>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user