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

Commit

Permalink
✨ better behaviour for <FormState /> initialValue changes (#446)
Browse files Browse the repository at this point in the history
* ✨ customizable state derivation for form state

* 🗒️ fix FAQ
  • Loading branch information
marutypes authored Jan 4, 2019
1 parent 7164efc commit be529d2
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 81 deletions.
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!

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';
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

0 comments on commit be529d2

Please sign in to comment.