Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

✨ better behaviour for <FormState /> initialValue changes #446

Merged
merged 3 commits into from
Jan 4, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 8 additions & 2 deletions packages/react-form-state/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## Unreleased

### Added

- You can control how `<FormState />` reacts to changes in the initialValue prop using onInitialValueChanged.

## [0.5]

## Added
### Added

- `<List />` supports `getChildKey` to provide custom `key`s for it's children. [#387](https://github.com/Shopify/quilt/pull/387)

## [0.4.1]

## Fixed
### Fixed

- `<List />` no longer breaks on name generation.

Expand Down
49 changes: 48 additions & 1 deletion packages/react-form-state/docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ As such the main difference in our solution is the explicit, declarative api. Fo

## I want to invoke all my validators whenever I want, how can I do this?

You can do this by setting a `ref` on your `<FormState />`, and calling `validateForm` on the instance passed in.
You can do this by setting a [`ref`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) on your `<FormState />`, and calling `validateForm` on the instance passed in.

```typescript
// use `createRef` and validate imperatively later
Expand All @@ -38,6 +38,53 @@ class MyComponent extends React.Component {

If you need to do something immediately on mount you could also use old fashioned [callback refs](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs).

## My form keeps resetting for no reason! / My form is resetting whenever I change an input!
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is probably the most asked question I get, so I snuck it in here since it's sorta on theme


By default `<FormState />` resets whenever any value in your `initialValues` changes. If you are basing your initial values on existing state, this lets it update when your state changes (usually this would be the result of submitting).

If this is happening on each rerender, it is likely that you are generating your `initialValues` in some way that is different each time. This can happen when you construct `Date` objects, `UUID`s, or other dynamic values inline. You can solve this by `memoize`ing your initial value creation or creating dates and other dynamic data only once outside of your component's `render` method.

```typescript
// Bad!
function MyForm() {
return (
<FormState
initialValues={
publicationDate: new Date(),
text: '',
}
>
{({fields}) => /* markup*/ }
</FormState>
);
}


// Good!
const today = new Date();

function MyForm() {
return (
<FormState
initialValues={
publicationDate: today,
text: '',
}
>
{({fields}) => /* markup*/ }
</FormState>
);
}
```

## Can I have more control over what happens when initialValues change?

You can control how `<FormState />` reacts to changes in the `initialValue` prop using `onInitialValueChanged`. This prop has three options:

- (default) `reset-all`: Reset the entire form when `initialValues` changes.
- `reset-where-changed`: Reset only the changed field objects when `initialValues` changes.
- `ignore`: Ignore changes to the `initialValues` prop. This option makes `<FormState />` behave like a [fully controlled component](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-controlled-component). You will generally want to accompany this option with a [`key`](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key) or [`ref`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs).

## More questions

Have a question that you think should be included in our FAQ? Please help us by creating an [issue](https://github.com/Shopify/quilt/issues/new?template=ENHANCEMENT.md) or opening a pull request.
82 changes: 62 additions & 20 deletions packages/react-form-state/src/FormState.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-case-declarations */
import * as React from 'react';
import isEqual from 'lodash/isEqual';
import isArray from 'lodash/isArray';
Expand Down Expand Up @@ -52,6 +53,7 @@ interface Props<Fields> {
validators?: Partial<ValidatorDictionary<Fields>>;
onSubmit?: SubmitHandler<Fields>;
validateOnSubmit?: boolean;
onInitialValuesChange?: 'reset-all' | 'reset-where-changed' | 'ignore';
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking it would be helpful/safer to make these values constants or maybe an exported enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For those with typescript supporting editors string unions autocomplete just as well either way, for those without they feel like a more naturalJS API. Personally for component apis I enjoy the convenience provided by string unions.

Copy link
Contributor

Choose a reason for hiding this comment

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

For those with typescript supporting editors string unions autocomplete just as well either way

TIL

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image
:)

children(form: FormDetails<Fields>): React.ReactNode;
}

Expand All @@ -68,21 +70,25 @@ export default class FormState<
static List = List;
static Nested = Nested;

static getDerivedStateFromProps<T>(newProps: Props<T>, oldState?: State<T>) {
const newInitialValues = newProps.initialValues;
static getDerivedStateFromProps<T>(newProps: Props<T>, oldState: State<T>) {
const {initialValues, onInitialValuesChange} = newProps;

if (oldState == null) {
return createFormState(newInitialValues);
}
switch (onInitialValuesChange) {
case 'ignore':
return null;
case 'reset-where-changed':
return reconcileFormState(initialValues, oldState);
case 'reset-all':
default:
const oldInitialValues = initialValuesFromFields(oldState.fields);
const valuesMatch = isEqual(oldInitialValues, initialValues);

const oldInitialValues = initialValuesFromFields(oldState.fields);
const shouldReinitialize = !isEqual(oldInitialValues, newInitialValues);
if (valuesMatch) {
return null;
}

if (shouldReinitialize) {
return createFormState(newInitialValues);
return createFormState(initialValues);
}

return null;
}

state = createFormState(this.props.initialValues);
Expand Down Expand Up @@ -127,6 +133,16 @@ export default class FormState<
});
}

@bind()
public reset() {
return new Promise(resolve => {
this.setState(
(_state, props) => createFormState(props.initialValues),
() => resolve(),
);
});
}

private get dirty() {
return this.state.dirtyFields.length > 0;
}
Expand Down Expand Up @@ -184,22 +200,18 @@ export default class FormState<
}
}

const errors = await onSubmit(formData);
const errors = (await onSubmit(formData)) || [];

if (!this.mounted) {
return;
}

if (errors) {
if (errors.length > 0) {
this.updateRemoteErrors(errors);
this.setState({submitting: false});
} else {
this.setState({submitting: false, errors});
}

this.setState({submitting: false});
}

@bind()
private reset() {
this.setState((_state, props) => createFormState(props.initialValues));
}

@memoize()
Expand Down Expand Up @@ -373,6 +385,36 @@ export default class FormState<
}
}

function reconcileFormState<Fields>(
values: Fields,
oldState: State<Fields>,
): State<Fields> {
const {fields: oldFields} = oldState;
const dirtyFields = new Set(oldState.dirtyFields);

const fields: FieldStates<Fields> = mapObject(values, (value, key) => {
const oldField = oldFields[key];

if (value === oldField.initialValue) {
return oldField;
}

dirtyFields.delete(key);

return {
value,
initialValue: value,
dirty: false,
};
});

return {
...oldState,
dirtyFields: Array.from(dirtyFields),
fields,
};
}

function createFormState<Fields>(values: Fields): State<Fields> {
const fields: FieldStates<Fields> = mapObject(values, value => {
return {
Expand Down
Loading