From c9b9579545d5a7f3ccd1b86c3624d5c5fcff27ff Mon Sep 17 00:00:00 2001
From: "johnrom (streaming)" <74513402+jawnrom@users.noreply.github.com>
Date: Mon, 22 Mar 2021 17:35:13 -0400
Subject: [PATCH] Move Computed State into normal state helpers so that
FormikState = FormikReducerState + FormikComputedState. Add Fixtures and
Tutorial code to /app.
---
app/components/debugging/Collapse.tsx | 21 +++
app/components/debugging/DebugFieldState.tsx | 9 ++
app/components/debugging/DebugFormikState.tsx | 9 ++
app/components/debugging/DebugProps.tsx | 19 +++
app/helpers/array-helpers.ts | 9 ++
app/helpers/chaos-helpers.ts | 89 ++++++++++++
app/helpers/random-helpers.ts | 9 ++
app/helpers/tearing-helpers.ts | 41 ++++++
app/pages/{ => fixtures}/basic.js | 0
app/pages/fixtures/components.tsx | 56 ++++++++
app/pages/fixtures/perf.tsx | 123 ++++++++++++++++
app/pages/fixtures/perf500-same.tsx | 69 +++++++++
app/pages/fixtures/perf500.tsx | 65 +++++++++
app/pages/fixtures/tearing.tsx | 133 ++++++++++++++++++
app/pages/index.tsx | 59 ++++++--
.../{sign-in.js => tutorial/sign-in.tsx} | 61 ++++----
packages/formik/src/Field.tsx | 7 +-
packages/formik/src/FieldArray.tsx | 4 +-
packages/formik/src/Formik.tsx | 60 ++++----
packages/formik/src/FormikContext.tsx | 4 +-
packages/formik/src/connect.tsx | 8 +-
packages/formik/src/helpers/field-helpers.ts | 4 +-
packages/formik/src/helpers/form-helpers.ts | 11 +-
packages/formik/src/hooks/hooks.ts | 8 +-
packages/formik/src/hooks/useFormikState.ts | 4 +-
.../formik/src/hooks/useFullFormikState.ts | 31 ----
packages/formik/src/types.tsx | 21 ++-
27 files changed, 803 insertions(+), 131 deletions(-)
create mode 100644 app/components/debugging/Collapse.tsx
create mode 100644 app/components/debugging/DebugFieldState.tsx
create mode 100644 app/components/debugging/DebugFormikState.tsx
create mode 100644 app/components/debugging/DebugProps.tsx
create mode 100644 app/helpers/array-helpers.ts
create mode 100644 app/helpers/chaos-helpers.ts
create mode 100644 app/helpers/random-helpers.ts
create mode 100644 app/helpers/tearing-helpers.ts
rename app/pages/{ => fixtures}/basic.js (100%)
create mode 100644 app/pages/fixtures/components.tsx
create mode 100644 app/pages/fixtures/perf.tsx
create mode 100644 app/pages/fixtures/perf500-same.tsx
create mode 100644 app/pages/fixtures/perf500.tsx
create mode 100644 app/pages/fixtures/tearing.tsx
rename app/pages/{sign-in.js => tutorial/sign-in.tsx} (62%)
delete mode 100644 packages/formik/src/hooks/useFullFormikState.ts
diff --git a/app/components/debugging/Collapse.tsx b/app/components/debugging/Collapse.tsx
new file mode 100644
index 000000000..e4b52a93d
--- /dev/null
+++ b/app/components/debugging/Collapse.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react';
+
+export const Collapse: React.FC = props => {
+ const [collapsed, setCollapsed] = React.useState(false);
+
+ return (
+
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/app/components/debugging/DebugFieldState.tsx b/app/components/debugging/DebugFieldState.tsx
new file mode 100644
index 000000000..b8cd4784b
--- /dev/null
+++ b/app/components/debugging/DebugFieldState.tsx
@@ -0,0 +1,9 @@
+import * as React from 'react';
+import { UseFieldProps, useField } from 'formik';
+import { DebugProps } from './DebugProps';
+
+export const DebugFieldState = (props: UseFieldProps) => {
+ const [field, meta, helpers] = useField(props);
+
+ return ;
+};
diff --git a/app/components/debugging/DebugFormikState.tsx b/app/components/debugging/DebugFormikState.tsx
new file mode 100644
index 000000000..a4c46c1fa
--- /dev/null
+++ b/app/components/debugging/DebugFormikState.tsx
@@ -0,0 +1,9 @@
+import * as React from 'react';
+import { useFormikContext } from 'formik';
+import { DebugProps } from './DebugProps';
+
+export const DebugFormikState = () => {
+ const formikState = useFormikContext();
+
+ return ;
+};
diff --git a/app/components/debugging/DebugProps.tsx b/app/components/debugging/DebugProps.tsx
new file mode 100644
index 000000000..82cad4781
--- /dev/null
+++ b/app/components/debugging/DebugProps.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+
+export const DebugProps = (props?: any) => {
+ const renderCount = React.useRef(0);
+ return (
+
+
+ props = {JSON.stringify(props, null, 2)}
+ renders = {renderCount.current++}
+
+
+ );
+};
diff --git a/app/helpers/array-helpers.ts b/app/helpers/array-helpers.ts
new file mode 100644
index 000000000..97c9c06b1
--- /dev/null
+++ b/app/helpers/array-helpers.ts
@@ -0,0 +1,9 @@
+import { selectRandomInt } from './random-helpers';
+
+export const selectRange = (count: number) => Array.from(Array(count).keys());
+
+export const selectRandomArrayItem = (array: T[]) => {
+ const index = selectRandomInt(array.length);
+
+ return array[index];
+};
diff --git a/app/helpers/chaos-helpers.ts b/app/helpers/chaos-helpers.ts
new file mode 100644
index 000000000..1a89ed920
--- /dev/null
+++ b/app/helpers/chaos-helpers.ts
@@ -0,0 +1,89 @@
+import { FormikApi } from 'formik';
+import { useMemo, useEffect } from 'react';
+import { selectRandomInt } from './random-helpers';
+
+export type DynamicValues = Record;
+
+export const useChaosHelpers = (
+ formik: FormikApi,
+ array: number[]
+) => {
+ return useMemo(
+ () => [
+ () =>
+ formik.setValues(
+ array.reduce>((prev, id) => {
+ prev[`Input ${id}`] = selectRandomInt(500).toString();
+
+ if (prev[`Input ${id}`]) {
+ }
+
+ return prev;
+ }, {})
+ ),
+ () =>
+ formik.setErrors(
+ array.reduce>((prev, id) => {
+ const error = selectRandomInt(500);
+
+ // leave some errors empty
+ prev[`Input ${id}`] = error % 5 === 0 ? '' : error.toString();
+
+ return prev;
+ }, {})
+ ),
+ () =>
+ formik.setTouched(
+ array.reduce>((prev, id) => {
+ prev[`Input ${id}`] = selectRandomInt(500) % 2 === 0;
+
+ return prev;
+ }, {})
+ ),
+ () => formik.submitForm(),
+ () =>
+ formik.setFieldValue(
+ `Input ${selectRandomInt(array.length)}`,
+ selectRandomInt(500).toString()
+ ),
+ () =>
+ formik.setFieldError(
+ `Input ${selectRandomInt(array.length)}`,
+ selectRandomInt(500).toString()
+ ),
+ () =>
+ formik.setFieldTouched(
+ `Input ${selectRandomInt(array.length)}`,
+ selectRandomInt(2) % 2 === 0
+ ),
+ () => formik.setStatus(selectRandomInt(500).toString()),
+ () => formik.resetForm(),
+ ],
+ [array, formik]
+ );
+};
+
+let skipCount = 0;
+
+/**
+ * https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode
+ */
+export const useAutoUpdate = () => {
+ useEffect(() => {
+ if (typeof document !== 'undefined') {
+ skipCount += 1;
+
+ if (skipCount % 10 === 0) {
+ document.getElementById('update-without-transition')?.click();
+ }
+ }
+ }, []);
+
+ // SSR
+ if (typeof performance !== 'undefined') {
+ const start = performance?.now();
+ while (performance?.now() - start < 2) {
+ // empty
+ }
+ }
+};
diff --git a/app/helpers/random-helpers.ts b/app/helpers/random-helpers.ts
new file mode 100644
index 000000000..71aa26d63
--- /dev/null
+++ b/app/helpers/random-helpers.ts
@@ -0,0 +1,9 @@
+/**
+ * @param minOrMax // The maximum is exclusive and the minimum is inclusive
+ * @param max
+ */
+export const selectRandomInt = (minOrMax: number, max?: number) => {
+ const min = max ? minOrMax : 0;
+ max = max ? max : minOrMax;
+ return Math.floor(Math.random() * (max - min)) + min;
+};
diff --git a/app/helpers/tearing-helpers.ts b/app/helpers/tearing-helpers.ts
new file mode 100644
index 000000000..882491f04
--- /dev/null
+++ b/app/helpers/tearing-helpers.ts
@@ -0,0 +1,41 @@
+import { selectRange } from './array-helpers';
+import { useState, useCallback, useMemo } from 'react';
+import { useEffect } from 'react';
+
+/**
+ * Check if all elements show the same number.
+ * https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode
+ */
+export const useCheckTearing = (elementCount: number, skip = 0) => {
+ const ids = useMemo(() => selectRange(elementCount).slice(skip), [
+ elementCount,
+ skip,
+ ]);
+ const checkMatches = useCallback(() => {
+ const [first, ...rest] = ids;
+ const firstValue = document.querySelector(`#input-${first} code`)
+ ?.innerHTML;
+ return rest.every(id => {
+ const thisValue = document.querySelector(`#input-${id} code`)?.innerHTML;
+ const tore = thisValue !== firstValue;
+ if (tore) {
+ console.log('useCheckTearing: tore');
+ console.log(thisValue);
+ console.log(firstValue);
+ }
+ return !tore;
+ });
+ }, [ids]);
+ const [didTear, setDidTear] = useState(false);
+
+ // We won't create an infinite loop switching this boolean once, I promise.
+ // (famous last words)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(() => {
+ if (!didTear && !checkMatches()) {
+ setDidTear(true);
+ }
+ });
+
+ return didTear;
+};
diff --git a/app/pages/basic.js b/app/pages/fixtures/basic.js
similarity index 100%
rename from app/pages/basic.js
rename to app/pages/fixtures/basic.js
diff --git a/app/pages/fixtures/components.tsx b/app/pages/fixtures/components.tsx
new file mode 100644
index 000000000..2095894ef
--- /dev/null
+++ b/app/pages/fixtures/components.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+import { Formik, Field, Form, FieldProps } from 'formik';
+import { DebugProps } from '../../components/debugging/DebugProps';
+
+const initialValues = {
+ name: '',
+};
+
+const RenderComponent = (props: FieldProps) => (
+ <>
+
+
+ >
+);
+const ComponentComponent = (
+ props: FieldProps
+) => (
+ <>
+
+
+ >
+);
+const AsComponent = (
+ props: FieldProps['field']
+) => (
+ <>
+
+
+ >
+);
+
+const ComponentsPage = () => (
+
+
Test Components
+ {
+ console.log(values);
+ }}
+ onSubmit={async values => {
+ await new Promise(r => setTimeout(r, 500));
+ alert(JSON.stringify(values, null, 2));
+ }}
+ >
+
+
+
+);
+
+export default ComponentsPage;
diff --git a/app/pages/fixtures/perf.tsx b/app/pages/fixtures/perf.tsx
new file mode 100644
index 000000000..629ecf02e
--- /dev/null
+++ b/app/pages/fixtures/perf.tsx
@@ -0,0 +1,123 @@
+import * as React from 'react';
+import { Formik, Field, Form, ErrorMessage } from 'formik';
+import * as Yup from 'yup';
+
+let renderCount = 0;
+
+const PerfPage = () => (
+
+
Sign Up
+
{
+ await new Promise(r => setTimeout(r, 500));
+ alert(JSON.stringify(values, null, 2));
+ }}
+ >
+
+
+
+);
+
+export default PerfPage;
diff --git a/app/pages/fixtures/perf500-same.tsx b/app/pages/fixtures/perf500-same.tsx
new file mode 100644
index 000000000..c92b1a9d9
--- /dev/null
+++ b/app/pages/fixtures/perf500-same.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import { Formik, Form, useField, FieldConfig } from 'formik';
+import { Collapse } from '../../components/debugging/Collapse';
+import { selectRange } from '../../helpers/array-helpers';
+
+const Input = (p: FieldConfig) => {
+ const [field, meta] = useField(p);
+ const renders = React.useRef(0);
+ const committedRenders = React.useRef(0);
+ React.useLayoutEffect(() => {
+ committedRenders.current++;
+ });
+ return (
+ <>
+
+
+
+ {renders.current++}, {committedRenders.current}
+
+ {meta.touched && meta.error ? {meta.error.toString()}
: null}
+
+ {JSON.stringify(meta, null, 2)}
+
+ >
+ );
+};
+
+const isRequired = (v: string) => {
+ return v && v.trim() !== '' ? undefined : 'Required';
+};
+
+const fieldsArray = selectRange(500);
+const initialValues = fieldsArray.reduce>((prev, id) => {
+ prev[`Input ${id}`] = '';
+
+ return prev;
+}, {});
+
+const onSubmit = async (values: typeof initialValues) => {
+ await new Promise(r => setTimeout(r, 500));
+ alert(JSON.stringify(values, null, 2));
+};
+
+const Perf500SamePage = () => {
+ return (
+
+
+
500 of the same controlled field
+
+ #, # = number of renders, number of committed renders
+
+
+
+
+
+
+ );
+}
+
+export default Perf500SamePage;
diff --git a/app/pages/fixtures/perf500.tsx b/app/pages/fixtures/perf500.tsx
new file mode 100644
index 000000000..b0d67aec9
--- /dev/null
+++ b/app/pages/fixtures/perf500.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import { Formik, Form, useField, FieldConfig } from 'formik';
+import { selectRange } from '../../helpers/array-helpers';
+
+const Input = (p: FieldConfig) => {
+ const [field, meta] = useField(p);
+ const renders = React.useRef(0);
+ const committedRenders = React.useRef(0);
+ React.useLayoutEffect(() => {
+ committedRenders.current++;
+ });
+ return (
+ <>
+
+
+
+ {renders.current++}, {committedRenders.current}
+
+ {meta.touched && meta.error ? {meta.error.toString()}
: null}
+
+ {JSON.stringify(meta, null, 2)}
+
+ >
+ );
+};
+
+const isRequired = (v: string) => {
+ return v && v.trim() !== '' ? undefined : 'Required';
+};
+
+const array = selectRange(500);
+const initialValues = array.reduce>((prev, id) => {
+ prev[`Input ${id}`] = '';
+ return prev;
+}, {});
+
+const onSubmit = async (values: typeof initialValues) => {
+ await new Promise(r => setTimeout(r, 500));
+ alert(JSON.stringify(values, null, 2));
+};
+
+const kids = array.map(id => (
+
+));
+
+const Perf500Page = () => {
+ return (
+
+
+
Formik v3 with 500 controlled fields
+
+ #, # = number of renders, number of committed renders
+
+
+
+
+
+
+ );
+}
+
+export default Perf500Page;
\ No newline at end of file
diff --git a/app/pages/fixtures/tearing.tsx b/app/pages/fixtures/tearing.tsx
new file mode 100644
index 000000000..f2afbd9e7
--- /dev/null
+++ b/app/pages/fixtures/tearing.tsx
@@ -0,0 +1,133 @@
+import * as React from 'react';
+import {
+ FieldConfig,
+ Form,
+ FormikProvider,
+ useField,
+ useFormik,
+ useFormikContext,
+} from 'formik';
+import { selectRandomArrayItem, selectRange } from '../../helpers/array-helpers';
+import { useCheckTearing } from '../../helpers/tearing-helpers';
+import {
+ DynamicValues,
+ useChaosHelpers,
+} from '../../helpers/chaos-helpers';
+
+const selectFullState = (state: T) => state
+
+const Input = (p: FieldConfig) => {
+ useField(p);
+ const api = useFormikContext();
+ const childState = api.useState(selectFullState);
+
+ return (
+
+
+ {JSON.stringify(childState, null, 2)}
+
+
+ );
+};
+
+const isRequired = (v: string) => {
+ return v && v.trim() !== '' ? undefined : 'Required';
+};
+
+const array = selectRange(50);
+const initialValues = array.reduce>((prev, id) => {
+ prev[`Input ${id}`] = '';
+ return prev;
+}, {});
+
+const onSubmit = async (values: DynamicValues) => {
+ await new Promise(r => setTimeout(r, 500));
+ console.log(JSON.stringify(values, null, 2));
+};
+
+const [parentId, lastId, ...inputIDs] = array;
+
+const kids = inputIDs.map(id => (
+
+));
+
+const TearingPage = () => {
+ const formik = useFormik({ onSubmit, initialValues });
+ const parentState = formik.useState(selectFullState);
+
+ const chaosHelpers = useChaosHelpers(formik, array);
+
+ const handleClickWithoutTransition = React.useCallback(() => {
+ selectRandomArrayItem(chaosHelpers)();
+ }, [chaosHelpers]);
+
+ // skip form-level state to check inputs
+ const didInputsTear = useCheckTearing(array.length - 1, 1);
+
+ // check form-level against inputs
+ const didFormStateTearWithInputs = useCheckTearing(array.length);
+
+ return (
+
+
+
+
Formik Tearing Tests
+
+ Did inputs tear amongst themselves? {didInputsTear ? 'Yes' : 'No'}
+
+
+ Did form-level state tear with inputs?{' '}
+ {didFormStateTearWithInputs ? 'Yes' : 'No'}
+
+
+
+
+
+
+
+ );
+}
+
+export default TearingPage;
diff --git a/app/pages/index.tsx b/app/pages/index.tsx
index 30daffeed..abc0c42f4 100644
--- a/app/pages/index.tsx
+++ b/app/pages/index.tsx
@@ -4,19 +4,52 @@ import Link from 'next/link';
function Home() {
return (
- Formik Examples and Fixtures
-
+ Formik Tutorial and Fixtures
+
+