Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 18 additions & 39 deletions docs/examples/debug.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,30 @@
import Form, { Field } from 'rc-field-form';
import React from 'react';

function WatchCom({ name }: { name: number }) {
const data = Form.useWatch(name);
return data;
}
import Input from './components/Input';

export default function App() {
const [form] = Form.useForm();
const [open, setOpen] = React.useState<boolean>(true);
const [keyName, setKeyName] = React.useState(true);

const data = React.useMemo(() => {
return Array.from({ length: 1 * 500 }).map((_, i) => ({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
}));
}, []);
// const val = Form.useWatch(keyName ? 'name' : 'age', form);
const val = Form.useWatch(values => values[keyName ? 'name' : 'age'], form);

return (
<Form form={form}>
<div className="App">
{/* When I made the switch, it was very laggy */}
<button
onClick={() => {
setOpen(!open);
}}
>
Switch
</button>
<WatchCom name={0} />
<WatchCom name={1} />
{open ? (
<div className="flex gap-[5px] flex-wrap">
{data?.map(item => {
return (
<Field key={item.name} name={item.name}>
<input />
</Field>
);
})}
</div>
) : (
<h2>some thing</h2>
)}
</div>
<button
onClick={() => {
setKeyName(!keyName);
}}
>
Switch {String(keyName)}
</button>
<Field name="name" initialValue="bamboo">
<Input />
</Field>
<Field name="age" initialValue="light">
<Input />
</Field>
{val}
</Form>
);
}
114 changes: 52 additions & 62 deletions src/useWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import type {
InternalNamePath,
NamePath,
Store,
WatchCallBack,
WatchOptions,
} from './interface';
import { isFormInstance } from './utils/typeUtil';
import { getNamePath, getValue } from './utils/valueUtil';
import { useEvent } from '@rc-component/util';

type ReturnPromise<T> = T extends Promise<infer ValueType> ? ValueType : never;
type GetGeneric<TForm extends FormInstance> = ReturnPromise<ReturnType<TForm['validateFields']>>;
Expand All @@ -23,19 +25,6 @@ export function stringify(value: any) {
}
}

const useWatchWarning =
process.env.NODE_ENV !== 'production'
? (namePath: InternalNamePath) => {
const fullyStr = namePath.join('__RC_FIELD_FORM_SPLIT__');
const nameStrRef = useRef(fullyStr);

warning(
nameStrRef.current === fullyStr,
'`useWatch` is not support dynamic `namePath`. Please provide static instead.',
);
}
: () => {};

function useWatch<
TDependencies1 extends keyof GetGeneric<TForm>,
TForm extends FormInstance,
Expand Down Expand Up @@ -123,56 +112,57 @@ function useWatch(
);
}

const namePath = getNamePath(dependencies);
const namePathRef = useRef(namePath);
namePathRef.current = namePath;

useWatchWarning(namePath);

useEffect(
() => {
// Skip if not exist form instance
if (!isValidForm) {
return;
}

const { getFieldsValue, getInternalHooks } = formInstance;
const { registerWatch } = getInternalHooks(HOOK_MARK);

const getWatchValue = (values: any, allValues: any) => {
const watchValue = options.preserve ? allValues : values;
return typeof dependencies === 'function'
? dependencies(watchValue)
: getValue(watchValue, namePathRef.current);
};

const cancelRegister = registerWatch((values, allValues) => {
const newValue = getWatchValue(values, allValues);
const nextValueStr = stringify(newValue);

// Compare stringify in case it's nest object
if (valueStrRef.current !== nextValueStr) {
valueStrRef.current = nextValueStr;
setValue(newValue);
}
});

// TODO: We can improve this perf in future
const initialValue = getWatchValue(getFieldsValue(), getFieldsValue(true));

// React 18 has the bug that will queue update twice even the value is not changed
// ref: https://github.com/facebook/react/issues/27213
if (value !== initialValue) {
setValue(initialValue);
}

return cancelRegister;
},

// We do not need re-register since namePath content is the same
// ============================== Form ==============================
const { getFieldsValue, getInternalHooks } = formInstance;
const { registerWatch } = getInternalHooks(HOOK_MARK);

// ============================= Update =============================
const triggerUpdate = useEvent((values?: any, allValues?: any) => {
const watchValue = options.preserve
? (allValues ?? getFieldsValue(true))
: (values ?? getFieldsValue());

const nextValue =
typeof dependencies === 'function'
? dependencies(watchValue)
: getValue(watchValue, getNamePath(dependencies));

if (stringify(value) !== stringify(nextValue)) {
setValue(nextValue);
}
});

// ============================= Effect =============================
const flattenDeps =
typeof dependencies === 'function' ? dependencies : JSON.stringify(dependencies);

// Deps changed
useEffect(() => {
// Skip if not exist form instance
if (!isValidForm) {
return;
}

triggerUpdate();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isValidForm, flattenDeps]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

方法的话,那每次 render 就是新的方法吧?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我想的是,如果 dependencies 是方法,那判断返回的值,stringify处理,如果改变了就 render

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我想的是,如果 dependencies 是方法,那判断返回的值,stringify处理,如果改变了就 render

就是这么做的。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

现在是只要 render 了,flattenDeps 就变了

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

哦,只是 triggerUpdate 执行了,但是 setValue 还是判断变没变

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Value changed
useEffect(() => {
// Skip if not exist form instance
if (!isValidForm) {
return;
}

const cancelRegister = registerWatch((values, allValues) => {
triggerUpdate(values, allValues);
});

return cancelRegister;

// eslint-disable-next-line react-hooks/exhaustive-deps
[isValidForm],
);
}, [isValidForm]);

return value;
}
Expand Down
38 changes: 25 additions & 13 deletions tests/useWatch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,25 +426,37 @@ describe('useWatch', () => {
errorSpy.mockRestore();
});

it('dynamic change warning', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
it('dynamic change', () => {
const Demo: React.FC = () => {
const [form] = Form.useForm();
const [watchPath, setWatchPath] = React.useState('light');
Form.useWatch(watchPath, form);

React.useEffect(() => {
setWatchPath('bamboo');
}, []);
const value = Form.useWatch(watchPath, form);

return <Form form={form} />;
return (
<Form form={form} initialValues={{ light: 1128, bamboo: 903 }}>
<Field name="light">
<Input />
</Field>
<Field name="bamboo">
<Input />
</Field>
<button
onClick={() => {
setWatchPath('bamboo');
}}
>
{value}
</button>
</Form>
);
};
render(<Demo />);

expect(errorSpy).toHaveBeenCalledWith(
'Warning: `useWatch` is not support dynamic `namePath`. Please provide static instead.',
);
errorSpy.mockRestore();
const { container } = render(<Demo />);
const btn = container.querySelector('button')!;
expect(btn.textContent).toEqual('1128');

fireEvent.click(btn);
expect(btn.textContent).toEqual('903');
});

it('useWatch with preserve option', async () => {
Expand Down
Loading