Skip to content

Commit

Permalink
refactor: react 18 internal upgrade (#3817)
Browse files Browse the repository at this point in the history
`useReducer().dispatch` appears to be async now where it used to be synchronous, leading to many broken tests. I have replaced usage of it with equivalent synchronous code.
  • Loading branch information
quantizor committed Jun 14, 2023
1 parent 274c306 commit 96280d3
Show file tree
Hide file tree
Showing 17 changed files with 156 additions and 219 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-lions-tickle.md
@@ -0,0 +1,5 @@
---
'formik': patch
---

Updated internal types to support React 18.
6 changes: 5 additions & 1 deletion package.json
@@ -1,11 +1,15 @@
{
"name": "formik-project",
"private": true,
"resolutions": {
"shelljs": "0.8.5",
"typescript": "^4.0.0"
},
"devDependencies": {
"@changesets/changelog-github": "^0.2.7",
"@changesets/cli": "^2.10.3",
"@playwright/test": "^1.34.3",
"@types/jest": "^26.0.14",
"@types/jest": "^25.0.0",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/formik-native/package.json
Expand Up @@ -34,7 +34,7 @@
},
"devDependencies": {
"@react-native-community/eslint-config": "^0.0.5",
"@types/react": "^16.9.55",
"@types/react": "^18.2.7",
"@types/react-native": "^0.63.32",
"react": "^18.2.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz",
Expand Down
12 changes: 6 additions & 6 deletions packages/formik/package.json
Expand Up @@ -48,19 +48,19 @@
"lodash-es": "^4.17.21",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^1.10.0"
"tslib": "^2.0.0"
},
"devDependencies": {
"@testing-library/react": "^11.1.0",
"@testing-library/react": "^14.0.0",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/lodash": "^4.14.119",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@types/warning": "^3.0.0",
"@types/yup": "^0.24.9",
"just-debounce-it": "^1.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tsdx": "^0.14.1",
"typescript": "^4.0.3",
"yup": "^0.28.1"
Expand Down
5 changes: 5 additions & 0 deletions packages/formik/src/Field.tsx
Expand Up @@ -52,6 +52,11 @@ export interface FieldConfig<V = any> {
*/
validate?: FieldValidator;

/**
* Used for 'select' and related input types.
*/
multiple?: boolean;

/**
* Field name
*/
Expand Down
20 changes: 7 additions & 13 deletions packages/formik/src/FieldArray.tsx
Expand Up @@ -178,6 +178,7 @@ class FieldArrayInner<Values = {}> extends React.Component<

formik: { setFormikState },
} = this.props;

setFormikState((prevState: FormikState<any>) => {
let updateErrors = createAlterationHandler(alterErrors, fn);
let updateTouched = createAlterationHandler(alterTouched, fn);
Expand Down Expand Up @@ -268,26 +269,19 @@ class FieldArrayInner<Values = {}> extends React.Component<
this.updateArrayField(
(array: any[]) => {
const arr = array ? [value, ...array] : [value];
if (length < 0) {
length = arr.length;
}

length = arr.length;

return arr;
},
(array: any[]) => {
const arr = array ? [null, ...array] : [null];
if (length < 0) {
length = arr.length;
}
return arr;
return array ? [null, ...array] : [null];
},
(array: any[]) => {
const arr = array ? [null, ...array] : [null];
if (length < 0) {
length = arr.length;
}
return arr;
return array ? [null, ...array] : [null];
}
);

return length;
};

Expand Down
55 changes: 34 additions & 21 deletions packages/formik/src/Formik.tsx
@@ -1,33 +1,34 @@
import * as React from 'react';
import isEqual from 'react-fast-compare';
import deepmerge from 'deepmerge';
import isPlainObject from 'lodash/isPlainObject';
import * as React from 'react';
import isEqual from 'react-fast-compare';
import invariant from 'tiny-warning';
import { FieldConfig } from './Field';
import { FormikProvider } from './FormikContext';
import {
FieldHelperProps,
FieldInputProps,
FieldMetaProps,
FormikConfig,
FormikErrors,
FormikHandlers,
FormikHelpers,
FormikProps,
FormikState,
FormikTouched,
FormikValues,
FormikProps,
FieldMetaProps,
FieldHelperProps,
FieldInputProps,
FormikHelpers,
FormikHandlers,
} from './types';
import {
getActiveElement,
getIn,
isEmptyChildren,
isFunction,
isObject,
isPromise,
isString,
setIn,
isEmptyChildren,
isPromise,
setNestedObjectValues,
getActiveElement,
getIn,
isObject,
} from './utils';
import { FormikProvider } from './FormikContext';
import invariant from 'tiny-warning';

type FormikMessage<Values> =
| { type: 'SUBMIT_ATTEMPT' }
Expand Down Expand Up @@ -170,9 +171,8 @@ export function useFormik<Values extends FormikValues = FormikValues>({
};
}, []);

const [state, dispatch] = React.useReducer<
React.Reducer<FormikState<Values>, FormikMessage<Values>>
>(formikReducer, {
const [, setIteration] = React.useState(0);
const stateRef = React.useRef<FormikState<Values>>({
values: props.initialValues,
errors: props.initialErrors || emptyErrors,
touched: props.initialTouched || emptyTouched,
Expand All @@ -182,6 +182,17 @@ export function useFormik<Values extends FormikValues = FormikValues>({
submitCount: 0,
});

const state = stateRef.current;

const dispatch = React.useCallback((action: FormikMessage<Values>) => {
const prev = stateRef.current;

stateRef.current = formikReducer(prev, action);

// force rerender
if (prev !== stateRef.current) setIteration(x => x + 1);
}, []);

const runValidateHandler = React.useCallback(
(values: Values, field?: string): Promise<FormikErrors<Values>> => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -888,9 +899,11 @@ export function useFormik<Values extends FormikValues = FormikValues>({
);

const getFieldProps = React.useCallback(
(nameOrOptions): FieldInputProps<any> => {
(nameOrOptions: string | FieldConfig<any>): FieldInputProps<any> => {
const isAnObject = isObject(nameOrOptions);
const name = isAnObject ? nameOrOptions.name : nameOrOptions;
const name = isAnObject
? (nameOrOptions as FieldConfig<any>).name
: nameOrOptions;
const valueState = getIn(state.values, name);

const field: FieldInputProps<any> = {
Expand All @@ -905,7 +918,7 @@ export function useFormik<Values extends FormikValues = FormikValues>({
value: valueProp, // value is special for checkboxes
as: is,
multiple,
} = nameOrOptions;
} = nameOrOptions as FieldConfig<any>;

if (type === 'checkbox') {
if (valueProp === undefined) {
Expand Down
7 changes: 4 additions & 3 deletions packages/formik/src/connect.tsx
Expand Up @@ -12,7 +12,7 @@ import invariant from 'tiny-warning';
export function connect<OuterProps, Values = {}>(
Comp: React.ComponentType<OuterProps & { formik: FormikContextType<Values> }>
) {
const C: React.FC<OuterProps> = (props: OuterProps) => (
const C: React.FC<OuterProps> = props => (
<FormikConsumer>
{formik => {
invariant(
Expand All @@ -23,6 +23,7 @@ export function connect<OuterProps, Values = {}>(
}}
</FormikConsumer>
);

const componentDisplayName =
Comp.displayName ||
Comp.name ||
Expand All @@ -32,7 +33,7 @@ export function connect<OuterProps, Values = {}>(
// Assign Comp to C.WrappedComponent so we can access the inner component in tests
// For example, <Field.WrappedComponent /> gets us <FieldInner/>
(C as React.FC<OuterProps> & {
WrappedComponent: React.ReactNode;
WrappedComponent: typeof Comp;
}).WrappedComponent = Comp;

C.displayName = `FormikConnect(${componentDisplayName})`;
Expand All @@ -42,5 +43,5 @@ export function connect<OuterProps, Values = {}>(
Comp as React.ComponentClass<
OuterProps & { formik: FormikContextType<Values> }
> // cast type to ComponentClass (even if SFC)
) as React.ComponentType<OuterProps>;
);
}
9 changes: 6 additions & 3 deletions packages/formik/src/types.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
import { FieldConfig } from './Field';
/**
* Values of fields in the form
*/
Expand Down Expand Up @@ -149,7 +150,9 @@ export interface FormikHandlers {
: (e: string | React.ChangeEvent<any>) => void;
};

getFieldProps: <Value = any>(props: any) => FieldInputProps<Value>;
getFieldProps: <Value = any>(
props: string | FieldConfig<Value>
) => FieldInputProps<Value>;
getFieldMeta: <Value>(name: string) => FieldMetaProps<Value>;
getFieldHelpers: <Value = any>(name: string) => FieldHelperProps<Value>;
}
Expand Down Expand Up @@ -177,7 +180,7 @@ export interface FormikConfig<Values> extends FormikSharedConfig {
/**
* Form component to render
*/
component?: React.ComponentType<FormikProps<Values>> | React.ReactNode;
component?: React.ComponentType<FormikProps<Values>>;

/**
* Render prop (works like React router's <Route render={props =>} />)
Expand Down Expand Up @@ -262,7 +265,7 @@ export interface SharedRenderProps<T> {
/**
* Field component to render. Can either be a string like 'select' or a component.
*/
component?: string | React.ComponentType<T | void>;
component?: keyof JSX.IntrinsicElements | React.ComponentType<T | void>;

/**
* Render prop (works like React router's <Route render={props =>} />)
Expand Down
2 changes: 1 addition & 1 deletion packages/formik/src/withFormik.tsx
Expand Up @@ -81,7 +81,7 @@ export interface WithFormikConfig<

export type CompositeComponent<P> =
| React.ComponentClass<P>
| React.StatelessComponent<P>;
| React.FunctionComponent<P>;

export interface ComponentDecorator<TOwnProps, TMergedProps> {
(component: CompositeComponent<TMergedProps>): React.ComponentType<TOwnProps>;
Expand Down
1 change: 1 addition & 0 deletions packages/formik/test/ErrorMessage.test.tsx
Expand Up @@ -45,6 +45,7 @@ fdescribe('<ErrorMessage />', () => {

await act(async () => {
await actualFProps.setFieldTouched('email');
await actualFProps.setFieldError('email', message);
});

// Renders after being visited with an error.
Expand Down
22 changes: 11 additions & 11 deletions packages/formik/test/FieldArray.test.tsx
Expand Up @@ -2,7 +2,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';
import * as Yup from 'yup';

import { FieldArray, Formik, isFunction } from '../src';
import { FieldArray, FieldArrayRenderProps, Formik, isFunction } from '../src';

const noop = () => {};

Expand Down Expand Up @@ -80,7 +80,7 @@ describe('<FieldArray />', () => {
describe('props.push()', () => {
it('should add a value to the end of the field array', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('<FieldArray />', () => {
it('should push clone not actual reference', () => {
let personTemplate = { firstName: '', lastName: '' };
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm initialValues={{ people: [] }}>
{(props: any) => {
Expand Down Expand Up @@ -187,7 +187,7 @@ describe('<FieldArray />', () => {
describe('props.pop()', () => {
it('should remove and return the last value from the field array', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -217,7 +217,7 @@ describe('<FieldArray />', () => {
describe('props.swap()', () => {
it('should swap two values in field array', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -246,7 +246,7 @@ describe('<FieldArray />', () => {
describe('props.insert()', () => {
it('should insert a value at given index of field array', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -275,7 +275,7 @@ describe('<FieldArray />', () => {
describe('props.replace()', () => {
it('should replace a value at given index of field array', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -304,7 +304,7 @@ describe('<FieldArray />', () => {
describe('props.unshift()', () => {
it('should add a value to start of field array and return its length', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -334,7 +334,7 @@ describe('<FieldArray />', () => {

describe('props.remove()', () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;

beforeEach(() => {
render(
Expand Down Expand Up @@ -396,7 +396,7 @@ describe('<FieldArray />', () => {
describe('given array-like object representing errors', () => {
it('should run arrayHelpers successfully', async () => {
let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;
render(
<TestForm>
{(props: any) => {
Expand Down Expand Up @@ -440,7 +440,7 @@ describe('<FieldArray />', () => {
});

let formikBag: any;
let arrayHelpers: any;
let arrayHelpers: FieldArrayRenderProps;

beforeEach(() => {
render(
Expand Down

1 comment on commit 96280d3

@vercel
Copy link

@vercel vercel bot commented on 96280d3 Jun 14, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

formik-docs – ./website

formik-docs-git-main-formik.vercel.app
www.formik.org
formik-docs.vercel.app
formik-docs-formik.vercel.app
formik.org

Please sign in to comment.