From 1b4a85f87b237ff8dd9838e463900f1879827f17 Mon Sep 17 00:00:00 2001 From: Andres Galindo Date: Tue, 11 Feb 2025 12:11:19 -0800 Subject: [PATCH] Use Local State for Immediate UI Updates with Deferred Global Store Sync (#3193) closes https://github.com/zesty-io/manager-ui/issues/3178 This approach mimics the [useDeferredValue](https://react.dev/reference/react/useDeferredValue ) hook value method that is only available on react 19 - Introduces a local state to handle immediate UI updates for text based controlled inputs. - Ensures smoother user experience by eliminating lag caused by global store updates. - Global state updates are now deferred, preventing excessive store updates on key clicks in order to keep the UI update process free and responsive. - Syncs local state with external value changes to maintain consistency. **This approach ensures real-time user feedback with zero lag** An alternative but much larger scope approach would be to restructure the component tree and optimize the use of selectors to minimize re-renders caused the global store and prop drilling. However it could potentially not reach the zero lag achieved by the deferring approach as it guarantees UI update calls have the highest priority --- .../src/app/components/Editor/Field/Field.tsx | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx b/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx index 5d1bfd130..754624bb7 100644 --- a/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx +++ b/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx @@ -71,6 +71,7 @@ import { import { ResolvedOption } from "./ResolvedOption"; import { LinkOption } from "./LinkOption"; import { FieldTypeMedia } from "../../FieldTypeMedia"; +import { debounce } from "lodash"; const AIFieldShell = withAI(FieldShell); @@ -203,6 +204,22 @@ export const Field = ({ const value = item?.data?.[name]; const version = item?.meta?.version; const fieldData = fields?.find((field) => field.ZUID === ZUID); + const [inputValue, setInputValue] = useState(value || ""); + + const debouncedOnChange = useMemo(() => debounce(onChange, 300), [onChange]); + + const deferredChange = useCallback( + (value, name) => { + setInputValue(value); + debouncedOnChange(value, name); + }, + [debouncedOnChange] + ); + + // Keep local input value in sync with global field value + useEffect(() => { + setInputValue(value || ""); + }, [value]); useEffect(() => { if (datatype !== "date" && datatype !== "datetime") { @@ -288,7 +305,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name || name} label={fieldData?.label || label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={ fieldData || { name: name, @@ -304,11 +321,11 @@ export const Field = ({ minLength={minLength} errors={errors} aiType="text" - value={value} + value={inputValue} > onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth inputProps={{ name: fieldData?.name || name, @@ -322,12 +339,12 @@ export const Field = ({ return ( onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth error={errors && Object.values(errors)?.some((error) => !!error)} /> @@ -338,14 +355,14 @@ export const Field = ({ return ( onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth type="url" error={errors && Object.values(errors)?.some((error) => !!error)} @@ -377,7 +394,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name} label={fieldData?.label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={fieldData} onChange={(evt: ChangeEvent) => onChange(evt.target.value, name) @@ -387,11 +404,11 @@ export const Field = ({ aiType="word" maxLength={maxLength} minLength={minLength} - value={value} + value={inputValue} > onChange(evt.target.value, name)} + value={inputValue} + onChange={(evt) => deferredChange(evt.target.value, name)} fullWidth multiline rows={6} @@ -424,7 +441,7 @@ export const Field = ({ name={name} value={value} version={version} - onChange={onChange} + onChange={deferredChange} onSave={onSave} onCharacterCountChange={(charCount: number) => setCharacterCount(charCount) @@ -448,7 +465,7 @@ export const Field = ({ ZUID={fieldData?.ZUID} name={fieldData?.name} label={fieldData?.label} - valueLength={(value as string)?.length ?? 0} + valueLength={(inputValue as string)?.length ?? 0} settings={fieldData} onChange={onChange} errors={errors} @@ -456,14 +473,14 @@ export const Field = ({ datatype={fieldData?.datatype} editorType={editorType} onEditorChange={(value: EditorType) => setEditorType(value)} - value={value} + value={inputValue} > { setImageModal(opts); @@ -918,10 +935,10 @@ export const Field = ({ return ( !!error)} /> @@ -937,8 +954,8 @@ export const Field = ({ !!error)} />