From 47d0754ebeea0ab0e10e00e4fe6c0d68249ea643 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Tue, 4 Jun 2019 22:49:54 +0200 Subject: [PATCH 1/2] Strongly typed form values for Flow and Typescript --- package.json | 2 +- src/FormSpy.js | 17 ++++++++++++----- src/ReactFinalForm.js | 24 +++++++++++++----------- src/context.js | 5 ----- src/getContext.js | 14 ++++++++++++++ src/index.js | 7 +++++-- src/index.js.flow | 18 +++++++++++++----- src/types.js.flow | 41 ++++++++++++++++++++++------------------- src/useField.js | 13 +++++++++---- src/useForm.js | 11 +++++++---- src/useFormState.js | 14 +++++++------- typescript/index.d.ts | 42 +++++++++++++++++++++++++----------------- 12 files changed, 128 insertions(+), 80 deletions(-) delete mode 100644 src/context.js create mode 100644 src/getContext.js diff --git a/package.json b/package.json index e2d79209..246131fd 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-react": "^7.13.0", "eslint-plugin-react-hooks": "^1.6.0", "fast-deep-equal": "^2.0.1", - "final-form": "^4.13.0", + "final-form": "file:.yalc/final-form", "flow-bin": "^0.98.1", "glow": "^1.2.2", "husky": "^2.3.0", diff --git a/src/FormSpy.js b/src/FormSpy.js index 720f2c7b..e13a7ac9 100644 --- a/src/FormSpy.js +++ b/src/FormSpy.js @@ -2,13 +2,20 @@ import * as React from 'react' import renderComponent from './renderComponent' import type { FormSpyPropsWithForm as Props, FormSpyRenderProps } from './types' -import type { FormApi } from 'final-form' +import type { FormApi, FormValuesShape } from 'final-form' import isSyntheticEvent from './isSyntheticEvent' import useFormState from './useFormState' -import ReactFinalFormContext from './context' +import getContext from './getContext' -const FormSpy = ({ onChange, subscription, ...rest }: Props) => { - const reactFinalForm: ?FormApi = React.useContext(ReactFinalFormContext) +function FormSpy({ + onChange, + subscription, + ...rest +}: Props) { + const ReactFinalFormContext = getContext() + const reactFinalForm: ?FormApi = React.useContext( + ReactFinalFormContext + ) if (!reactFinalForm) { throw new Error('FormSpy must be used inside of a ReactFinalForm component') } @@ -17,7 +24,7 @@ const FormSpy = ({ onChange, subscription, ...rest }: Props) => { return null } - const renderProps: FormSpyRenderProps = { + const renderProps: FormSpyRenderProps = { form: { ...reactFinalForm, reset: eventOrValues => { diff --git a/src/ReactFinalForm.js b/src/ReactFinalForm.js index 318caa7c..f5517d46 100644 --- a/src/ReactFinalForm.js +++ b/src/ReactFinalForm.js @@ -10,6 +10,7 @@ import type { Config, FormSubscription, FormState, + FormValuesShape, Unsubscribe } from 'final-form' import type { FormProps as Props } from './types' @@ -19,7 +20,7 @@ import useConstant from './useConstant' import shallowEqual from './shallowEqual' import isSyntheticEvent from './isSyntheticEvent' import type { FormRenderProps } from './types.js.flow' -import ReactFinalFormContext from './context' +import getContext from './getContext' import useLatest from './useLatest' export const version = '6.0.1' @@ -37,7 +38,7 @@ export const all: FormSubscription = formSubscriptionItems.reduce( {} ) -const ReactFinalForm = ({ +function ReactFinalForm({ debug, decorators, destroyOnUnregister, @@ -50,8 +51,9 @@ const ReactFinalForm = ({ validate, validateOnBlur, ...rest -}: Props) => { - const config: Config = { +}: Props) { + const ReactFinalFormContext = getContext() + const config: Config = { debug, destroyOnUnregister, initialValues, @@ -62,16 +64,16 @@ const ReactFinalForm = ({ validateOnBlur } - const form: FormApi = useConstant(() => { - const f = createForm(config) + const form: FormApi = useConstant(() => { + const f = createForm(config) f.pauseValidation() return f }) // synchronously register and unregister to query form state for our subscription on first render - const [state, setState] = React.useState( - (): FormState => { - let initialState: FormState = {} + const [state, setState] = React.useState>( + (): FormState => { + let initialState: FormState = {} form.subscribe(state => { initialState = state }, subscription)() @@ -81,7 +83,7 @@ const ReactFinalForm = ({ // save a copy of state that can break through the closure // on the shallowEqual() line below. - const stateRef = useLatest(state) + const stateRef = useLatest>(state) React.useEffect(() => { // We have rendered, so all fields are no registered, so we can unpause validation @@ -170,7 +172,7 @@ const ReactFinalForm = ({ return form.submit() } - const renderProps: FormRenderProps = { + const renderProps: FormRenderProps = { // assign to force Flow check ...state, form: { diff --git a/src/context.js b/src/context.js deleted file mode 100644 index a219aac0..00000000 --- a/src/context.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow -import * as React from 'react' -import type { FormApi } from 'final-form' - -export default React.createContext() diff --git a/src/getContext.js b/src/getContext.js new file mode 100644 index 00000000..fb43d161 --- /dev/null +++ b/src/getContext.js @@ -0,0 +1,14 @@ +// @flow +import * as React from 'react' +import type { FormApi, FormValuesShape } from 'final-form' + +let instance: React.Context> + +export default function getContext< + FormValues: FormValuesShape +>(): React.Context> { + if (!instance) { + instance = React.createContext>() + } + return ((instance: any): React.Context>) +} diff --git a/src/index.js b/src/index.js index 234a4b11..eac6a858 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,12 @@ // @flow +import Form from './ReactFinalForm' +import FormSpy from './FormSpy' export { default as Field } from './Field' export { default as Form, version } from './ReactFinalForm' export { default as FormSpy } from './FormSpy' -export { default as ReactFinalFormContext } from './context' export { default as useField } from './useField' export { default as useFormState } from './useFormState' export { default as useForm } from './useForm' -export { default as context } from './context' +export function withTypes() { + return { Form, FormSpy } +} diff --git a/src/index.js.flow b/src/index.js.flow index b40ad1b0..bf22052a 100644 --- a/src/index.js.flow +++ b/src/index.js.flow @@ -1,6 +1,6 @@ // @flow import * as React from 'react' -import type { FormApi, FormState } from 'final-form' +import type { FormApi, FormState, FormValuesShape } from 'final-form' import type { FieldProps, FieldRenderProps, @@ -20,12 +20,20 @@ export type { } from './types' declare export var Field: React.ComponentType -declare export var Form: React.ComponentType -declare export var FormSpy: React.ComponentType -declare export var useForm: (componentName?: string) => FormApi -declare export var useFormState: UseFormStateParams => ?FormState +declare export var Form: React.ComponentType> +declare export var FormSpy: React.ComponentType> +declare export function useForm( + componentName?: string +): FormApi +declare export function useFormState( + params: UseFormStateParams +): ?FormState declare export var useField: ( name: string, config: UseFieldConfig ) => FieldRenderProps +declare export function withTypes(): { + Form: React.ComponentType>, + FormSpy: React.ComponentType> +} declare export var version: string diff --git a/src/types.js.flow b/src/types.js.flow index 238d97a6..b974c767 100644 --- a/src/types.js.flow +++ b/src/types.js.flow @@ -6,14 +6,15 @@ import type { Decorator, FormState, FormSubscription, + FormValuesShape, FieldSubscription, FieldValidator } from 'final-form' type SupportedInputs = 'input' | 'select' | 'textarea' -export type ReactContext = { - reactFinalForm: FormApi +export type ReactContext = { + reactFinalForm: FormApi } export type FieldInputProps = { @@ -49,14 +50,14 @@ export type FieldRenderProps = { } } -export type FormRenderProps = { +export type FormRenderProps = { handleSubmit: (?SyntheticEvent) => ?Promise, - form: FormApi -} & FormState + form: FormApi +} & FormState -export type FormSpyRenderProps = { - form: FormApi -} & FormState +export type FormSpyRenderProps = { + form: FormApi +} & FormState export type RenderableProps = { component?: React.ComponentType<*> | SupportedInputs, @@ -64,12 +65,12 @@ export type RenderableProps = { render?: (props: T) => React.Node } -export type FormProps = { +export type FormProps = { subscription?: FormSubscription, - decorators?: Decorator[], + decorators?: Decorator[], initialValuesEqual?: (?Object, ?Object) => boolean -} & Config & - RenderableProps +} & Config & + RenderableProps> export type UseFieldConfig = { afterSubmit?: () => void, @@ -95,14 +96,16 @@ export type FieldProps = UseFieldConfig & { name: string } & RenderableProps -export type UseFormStateParams = { - onChange?: (formState: FormState) => void, +export type UseFormStateParams = { + onChange?: (formState: FormState) => void, subscription?: FormSubscription } -export type FormSpyProps = UseFormStateParams & - RenderableProps +export type FormSpyProps< + FormValues: FormValuesShape +> = UseFormStateParams & + RenderableProps> -export type FormSpyPropsWithForm = { - reactFinalForm: FormApi -} & FormSpyProps +export type FormSpyPropsWithForm = { + reactFinalForm: FormApi +} & FormSpyProps diff --git a/src/useField.js b/src/useField.js index 375d0a42..5a6c0233 100644 --- a/src/useField.js +++ b/src/useField.js @@ -1,7 +1,12 @@ // @flow import * as React from 'react' import { fieldSubscriptionItems } from 'final-form' -import type { FieldSubscription, FieldState, FormApi } from 'final-form' +import type { + FieldSubscription, + FieldState, + FormApi, + FormValuesShape +} from 'final-form' import type { UseFieldConfig, FieldInputProps, FieldRenderProps } from './types' import isReactNative from './isReactNative' import getValue from './getValue' @@ -18,7 +23,7 @@ const defaultFormat = (value: ?any, name: string) => const defaultParse = (value: ?any, name: string) => value === '' ? undefined : value -const useField = ( +function useField( name: string, { afterSubmit, @@ -38,8 +43,8 @@ const useField = ( validateFields, value: _value }: UseFieldConfig = {} -): FieldRenderProps => { - const form: FormApi = useForm('useField') +): FieldRenderProps { + const form: FormApi = useForm('useField') const validateRef = useLatest(validate) diff --git a/src/useForm.js b/src/useForm.js index 233f89a1..63e21aef 100644 --- a/src/useForm.js +++ b/src/useForm.js @@ -1,10 +1,13 @@ // @flow import * as React from 'react' -import type { FormApi } from 'final-form' -import ReactFinalFormContext from './context' +import type { FormApi, FormValuesShape } from 'final-form' +import getContext from './getContext' -const useForm = (componentName?: string): FormApi => { - const form: ?FormApi = React.useContext(ReactFinalFormContext) +function useForm( + componentName?: string +): FormApi { + const ReactFinalFormContext = getContext() + const form: ?FormApi = React.useContext(ReactFinalFormContext) if (!form) { throw new Error( `${componentName || 'useForm'} must be used inside of a
component` diff --git a/src/useFormState.js b/src/useFormState.js index 69293aa4..ae5ad5c8 100644 --- a/src/useFormState.js +++ b/src/useFormState.js @@ -1,21 +1,21 @@ // @flow import * as React from 'react' import type { UseFormStateParams } from './types' -import type { FormState, FormApi } from 'final-form' +import type { FormState, FormApi, FormValuesShape } from 'final-form' import { all } from './ReactFinalForm' import useForm from './useForm' -const useFormState = ({ +function useFormState({ onChange, subscription = all -}: UseFormStateParams = {}): FormState => { - const form: FormApi = useForm('useFormState') +}: UseFormStateParams = {}): FormState { + const form: FormApi = useForm('useFormState') const firstRender = React.useRef(true) // synchronously register and unregister to query field state for our subscription on first render - const [state, setState] = React.useState( - (): FormState => { - let initialState: FormState = {} + const [state, setState] = React.useState>( + (): FormState => { + let initialState: FormState = {} form.subscribe(state => { initialState = state }, subscription)() diff --git a/typescript/index.d.ts b/typescript/index.d.ts index d2dc7f99..3194b402 100644 --- a/typescript/index.d.ts +++ b/typescript/index.d.ts @@ -13,8 +13,8 @@ import { Omit } from 'ts-essentials'; type SupportedInputs = 'input' | 'select' | 'textarea'; -export interface ReactContext { - reactFinalForm: FormApi; +export interface ReactContext { + reactFinalForm: FormApi; } export type FieldMetaState = Omit< @@ -38,15 +38,15 @@ export interface FieldRenderProps { meta: FieldMetaState; } -export interface FormRenderProps extends FormState { - form: FormApi; +export interface FormRenderProps extends FormState { + form: FormApi; handleSubmit: ( event?: React.SyntheticEvent ) => Promise | undefined; } -export interface FormSpyRenderProps extends FormState { - form: FormApi; +export interface FormSpyRenderProps extends FormState { + form: FormApi; } export interface RenderableProps { @@ -55,9 +55,9 @@ export interface RenderableProps { render?: (props: T) => React.ReactNode; } -export interface FormProps - extends Config, - RenderableProps { +export interface FormProps + extends Config, + RenderableProps> { subscription?: FormSubscription; decorators?: Decorator[]; initialValuesEqual?: (a?: object, b?: object) => boolean; @@ -88,22 +88,30 @@ export interface FieldProps [otherProp: string]: any; } -export interface UseFormStateParams { - onChange?: (formState: FormState) => void; +export interface UseFormStateParams { + onChange?: (formState: FormState) => void; subscription?: FormSubscription; } -export interface FormSpyProps - extends UseFormStateParams, - RenderableProps {} +export interface FormSpyProps + extends UseFormStateParams, + RenderableProps> {} export const Field: React.FC>; export const Form: React.FC>; -export const FormSpy: React.FC; +export const FormSpy: React.FC>; export function useField( name: string, config?: UseFieldConfig ): FieldRenderProps; -export function useForm(componentName?: string): FormApi; -export function useFormState(params?: UseFormStateParams): FormState; +export function useForm( + componentName?: string +): FormApi; +export function useFormState( + params?: UseFormStateParams +): FormState; +export function withTypes(): { + Form: React.FC>; + FormSpy: React.FC>; +}; export const version: string; From dbe8d81cff42e51b9eaf5201734949f839297f76 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Tue, 11 Jun 2019 09:36:25 +0200 Subject: [PATCH 2/2] Upped version variable and ff version dep --- package-lock.json | 6 +++--- package.json | 4 ++-- src/ReactFinalForm.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb7cd579..615166d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4493,9 +4493,9 @@ } }, "final-form": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.13.0.tgz", - "integrity": "sha512-JD/MPstXejGtQOryhbLHJoOtsvcCV37baoVuwK/+/mgbNir3QZjedhUJoSqgjwMxk/H1j/usdDwy7xfKMAq/dw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.14.0.tgz", + "integrity": "sha512-AaZJxzmbhG4+zdz8gyCxZ73t1SIHG+THklIG5UEQyc57gVePgsQJVUjETao7ySLyX1Z4x/a+SJRw3lOD2dje/g==", "dev": true, "requires": { "@babel/runtime": "^7.3.1" diff --git a/package.json b/package.json index 246131fd..1782c56c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-plugin-react": "^7.13.0", "eslint-plugin-react-hooks": "^1.6.0", "fast-deep-equal": "^2.0.1", - "final-form": "file:.yalc/final-form", + "final-form": "^4.14.0", "flow-bin": "^0.98.1", "glow": "^1.2.2", "husky": "^2.3.0", @@ -88,7 +88,7 @@ "typescript": "^3.4.5" }, "peerDependencies": { - "final-form": "^4.13.0", + "final-form": "^4.14.0", "react": "^16.8.0" }, "lint-staged": { diff --git a/src/ReactFinalForm.js b/src/ReactFinalForm.js index f5517d46..a5865684 100644 --- a/src/ReactFinalForm.js +++ b/src/ReactFinalForm.js @@ -23,7 +23,7 @@ import type { FormRenderProps } from './types.js.flow' import getContext from './getContext' import useLatest from './useLatest' -export const version = '6.0.1' +export const version = '6.1.0' const versions = { 'final-form': ffVersion,