Skip to content

Commit

Permalink
Use Local State for Immediate UI Updates with Deferred Global Store S…
Browse files Browse the repository at this point in the history
…ync (#3193)

closes #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
  • Loading branch information
agalin920 authored Feb 11, 2025
1 parent f2c9a95 commit 1b4a85f
Showing 1 changed file with 40 additions and 23 deletions.
63 changes: 40 additions & 23 deletions src/apps/content-editor/src/app/components/Editor/Field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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,
Expand All @@ -304,11 +321,11 @@ export const Field = ({
minLength={minLength}
errors={errors}
aiType="text"
value={value}
value={inputValue}
>
<TextField
value={value}
onChange={(evt) => onChange(evt.target.value, name)}
value={inputValue}
onChange={(evt) => deferredChange(evt.target.value, name)}
fullWidth
inputProps={{
name: fieldData?.name || name,
Expand All @@ -322,12 +339,12 @@ export const Field = ({
return (
<FieldShell
settings={fieldData}
valueLength={(value as string)?.length ?? 0}
valueLength={(inputValue as string)?.length ?? 0}
errors={errors}
>
<TextField
value={value}
onChange={(evt) => onChange(evt.target.value, name)}
value={inputValue}
onChange={(evt) => deferredChange(evt.target.value, name)}
fullWidth
error={errors && Object.values(errors)?.some((error) => !!error)}
/>
Expand All @@ -338,14 +355,14 @@ export const Field = ({
return (
<FieldShell
settings={fieldData}
valueLength={(value as string)?.length ?? 0}
valueLength={(inputValue as string)?.length ?? 0}
errors={errors}
maxLength={maxLength}
withLengthCounter
>
<TextField
value={value}
onChange={(evt) => 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)}
Expand Down Expand Up @@ -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<HTMLInputElement>) =>
onChange(evt.target.value, name)
Expand All @@ -387,11 +404,11 @@ export const Field = ({
aiType="word"
maxLength={maxLength}
minLength={minLength}
value={value}
value={inputValue}
>
<TextField
value={value}
onChange={(evt) => onChange(evt.target.value, name)}
value={inputValue}
onChange={(evt) => deferredChange(evt.target.value, name)}
fullWidth
multiline
rows={6}
Expand Down Expand Up @@ -424,7 +441,7 @@ export const Field = ({
name={name}
value={value}
version={version}
onChange={onChange}
onChange={deferredChange}
onSave={onSave}
onCharacterCountChange={(charCount: number) =>
setCharacterCount(charCount)
Expand All @@ -448,22 +465,22 @@ 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}
aiType="word"
datatype={fieldData?.datatype}
editorType={editorType}
onEditorChange={(value: EditorType) => setEditorType(value)}
value={value}
value={inputValue}
>
<FieldTypeEditor
// @ts-ignore component not typed
name={name}
value={value}
value={inputValue}
version={version}
onChange={onChange}
onChange={deferredChange}
datatype={datatype}
mediaBrowser={(opts: any) => {
setImageModal(opts);
Expand Down Expand Up @@ -918,10 +935,10 @@ export const Field = ({
return (
<FieldShell settings={fieldData} errors={errors}>
<FieldTypeNumber
value={+value || 0}
value={+inputValue || 0}
name={name}
required={required}
onChange={onChange}
onChange={deferredChange}
hasError={errors && Object.values(errors)?.some((error) => !!error)}
/>
</FieldShell>
Expand All @@ -937,8 +954,8 @@ export const Field = ({
<FieldTypeCurrency
name={name}
currency={settings?.currency ?? "USD"}
value={String(value)}
onChange={onChange}
value={String(inputValue)}
onChange={deferredChange}
error={errors && Object.values(errors)?.some((error) => !!error)}
/>
</FieldShell>
Expand Down

0 comments on commit 1b4a85f

Please sign in to comment.