Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 13 additions & 6 deletions src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ const EMPTY_ERRORS: any[] = [];
const EMPTY_WARNINGS: any[] = [];

export type ShouldUpdate<Values = any> =
| boolean
| ((prevValues: Values, nextValues: Values, info: { source?: string }) => boolean);
boolean | ((prevValues: Values, nextValues: Values, info: { source?: string }) => boolean);

function requireUpdate(
shouldUpdate: ShouldUpdate,
Expand Down Expand Up @@ -98,8 +97,10 @@ export interface InternalFieldProps<Values = any> {
fieldContext?: InternalFormInstance;
}

export interface FieldProps<Values = any>
extends Omit<InternalFieldProps<Values>, 'name' | 'fieldContext'> {
export interface FieldProps<Values = any> extends Omit<
InternalFieldProps<Values>,
'name' | 'fieldContext'
> {
name?: NamePath<Values>;
}

Expand Down Expand Up @@ -140,6 +141,8 @@ class Field extends React.PureComponent<InternalFieldProps, FieldState> implemen

private prevValidating: boolean;

private lastRenderedValue: StoreValue;

private errors: string[] = EMPTY_ERRORS;
private warnings: string[] = EMPTY_WARNINGS;

Expand Down Expand Up @@ -167,8 +170,11 @@ class Field extends React.PureComponent<InternalFieldProps, FieldState> implemen
this.cancelRegisterFunc = registerField(this);
}

// One more render for component in case fields not ready
if (shouldUpdate === true) {
// One more render if the store changed before mount, including replay recovery.
if (
shouldUpdate === true ||
(this.getNamePath().length && this.getValue() !== this.lastRenderedValue)
) {
this.reRender();
}
}
Expand Down Expand Up @@ -583,6 +589,7 @@ class Field extends React.PureComponent<InternalFieldProps, FieldState> implemen
const { getInternalHooks, getFieldsValue }: InternalFormInstance = fieldContext;
const { dispatch } = getInternalHooks(HOOK_MARK);
const value = this.getValue();
this.lastRenderedValue = value;
const mergedGetValueProps = getValueProps || ((val: StoreValue) => ({ [valuePropName]: val }));

const originTriggerFunc = childProps[trigger];
Expand Down
1 change: 1 addition & 0 deletions src/FieldContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const Context = React.createContext<InternalFormInstance>({
useSubscribe: warningFunc,
setInitialValues: warningFunc,
destroyForm: warningFunc,
backupStore: warningFunc,
setCallbacks: warningFunc,
registerWatch: warningFunc,
getFields: warningFunc,
Expand Down
31 changes: 31 additions & 0 deletions src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,34 @@ type BaseFormProps = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit'

type RenderProps = (values: Store, form: FormInstance) => React.ReactNode;

class FormStoreSnapshot extends React.Component<{
backupStore: () => () => void;
clearOnDestroy?: boolean;
}> {
private restoreStore?: () => void;
private hasMounted = false;
private clearOnDestroy?: boolean;

public componentDidMount() {
// Match the value captured when Form's unmount effect first mounts.
if (!this.hasMounted) {
this.hasMounted = true;
this.clearOnDestroy = this.props.clearOnDestroy;
}
this.restoreStore?.();
}

public componentWillUnmount() {
if (this.clearOnDestroy) {
this.restoreStore = this.props.backupStore();
}
}

public render() {
return null;
}
}

export interface FormProps<Values = any> extends BaseFormProps {
initialValues?: Store;
form?: FormInstance<Values>;
Expand Down Expand Up @@ -70,6 +98,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (props, ref) =>
setValidateMessages,
setPreserve,
destroyForm,
backupStore,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);

// Pass ref with form instance
Expand Down Expand Up @@ -158,6 +187,8 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (props, ref) =>

const wrapperNode = (
<ListContext.Provider value={null}>
{/* Restore replayed mounts before fields register or read the store in layout effects. */}
<FormStoreSnapshot backupStore={backupStore} clearOnDestroy={clearOnDestroy} />
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
</ListContext.Provider>
);
Expand Down
18 changes: 18 additions & 0 deletions src/hooks/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export class FormStore {

private store: Store = {};

private explicitValueUpdateVersion = 0;

private fieldEntities: FieldEntity[] = [];

private initialValues: Store = {};
Expand Down Expand Up @@ -114,6 +116,7 @@ export class FormStore {
useSubscribe: this.useSubscribe,
setInitialValues: this.setInitialValues,
destroyForm: this.destroyForm,
backupStore: this.backupStore,
setCallbacks: this.setCallbacks,
setValidateMessages: this.setValidateMessages,
getFields: this.getFields,
Expand Down Expand Up @@ -173,6 +176,17 @@ export class FormStore {
}
};

private backupStore = () => {
const store = this.store;
const version = this.explicitValueUpdateVersion;
return () => {
// Explicit writes or resets after cleanup take precedence over replay recovery.
if (this.explicitValueUpdateVersion === version) {
this.updateStore(store);
}
};
};

private getInitialValue = (namePath: InternalNamePath) => {
const initValue = getValue(this.initialValues, namePath);

Expand Down Expand Up @@ -565,6 +579,7 @@ export class FormStore {

private resetFields = (nameList?: NamePath[]) => {
this.warningUnhooked();
this.explicitValueUpdateVersion += 1;

const prevStore = this.store;
if (!nameList) {
Expand Down Expand Up @@ -600,6 +615,7 @@ export class FormStore {

// Value
if ('value' in data) {
this.explicitValueUpdateVersion += 1;
this.updateStore(setValue(this.store, namePath, data.value));
}

Expand Down Expand Up @@ -762,6 +778,7 @@ export class FormStore {
private updateValue = (name: NamePath, value: StoreValue) => {
const namePath = getNamePath(name);
const prevStore = this.store;
this.explicitValueUpdateVersion += 1;
this.updateStore(setValue(this.store, namePath, value));

this.notifyObservers(prevStore, [namePath], {
Expand Down Expand Up @@ -793,6 +810,7 @@ export class FormStore {
const prevStore = this.store;

if (store) {
this.explicitValueUpdateVersion += 1;
const nextStore = merge(this.store, store);
this.updateStore(nextStore);
}
Expand Down
1 change: 1 addition & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export interface InternalHooks {
useSubscribe: (subscribable: boolean) => void;
setInitialValues: (values: Store, init: boolean) => void;
destroyForm: (clearOnDestroy?: boolean) => void;
backupStore: () => () => void;
setCallbacks: (callbacks: Callbacks) => void;
registerWatch: (callback: WatchCallBack) => () => void;
getFields: (namePathList?: InternalNamePath[]) => FieldData[];
Expand Down
Loading