Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use FormikApi + FormikState Subscriptions #3089

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9f8765e
Started to reimplement subscriber.
johnrom Mar 8, 2021
4dc160b
Grab types from other PR, to simplify later.
johnrom Mar 9, 2021
ef66e38
Grab types from other PR, to simplify later.
johnrom Mar 9, 2021
b77d371
Merge branch 'johnrom/subscriber' of github.com:johnrom/formik into j…
johnrom Mar 9, 2021
3e6e20c
Set up dev environment and implement Field.
johnrom Mar 10, 2021
881d7e6
Memoize Formik API and make it rain optimized states.
johnrom Mar 11, 2021
b8e7fcd
Roll back unnecessary tsconfig change.
johnrom Mar 11, 2021
feaee0e
Fix Sign In App Page.
johnrom Mar 11, 2021
a5a2f47
Roll back unnecessary tsconfig change.
johnrom Mar 11, 2021
4037473
Wire up FormikConsumer.
johnrom Mar 11, 2021
e2c496c
Remove implementation details from deprecated comments.
johnrom Mar 11, 2021
a78459b
Don't expose internal useOptimizedSelector.
johnrom Mar 11, 2021
0d1cbfd
Move useFormikComputedState to api.useComputedState, useIsDirty and u…
johnrom Mar 17, 2021
0c18ebb
Accidental import change.
johnrom Mar 17, 2021
3ab46cd
Remove TSConfig path aliases during build.
johnrom Mar 17, 2021
5011168
If we're calling our own reducer, we don't need to useReducer! setSta…
johnrom Mar 18, 2021
a5f4643
Optimize field meta and computed state with shallow equals.
johnrom Mar 18, 2021
c9b9579
Move Computed State into normal state helpers so that FormikState = F…
jawnrom Mar 22, 2021
611432a
Merge pull request #3 from jawnstreams/johnrom/downstream
johnrom Mar 22, 2021
dc2e2fa
Clarify RenderState vs GetState. Fix Test.
johnrom Mar 22, 2021
0764e12
Fix tests with updated /app structure.
johnrom Mar 22, 2021
0079948
Remove unused helper components from app/
johnrom Mar 23, 2021
9c4cd01
Ignore TypeScript Build Errors during app/ build.
johnrom Mar 23, 2021
ba1fdb9
Add Changeset
johnrom Mar 23, 2021
dfde363
Isolate subscriptions logic from useFormik to enforce correct use of …
johnrom Apr 12, 2021
46a6e17
Fix useFormikState documentation, fix getState stability.
johnrom Apr 12, 2021
ee43bb2
Get rid of unnecessary, autocompleted import.
johnrom Apr 12, 2021
7949c3d
React.SFC -> FC
johnrom Apr 12, 2021
94b8f22
Null coalesced errors and touched during resetForm.
johnrom Apr 12, 2021
f3bd303
fixed SetValues type, adding functional variant
johnrom Apr 16, 2021
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: 10 additions & 0 deletions .changeset/fifty-shrimps-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"formik-native": major
"formik": major
"app": major
---

Use FormikApi + FormikState Subscriptions

Switch underlying state implementation to use a subscription, and access that subscription via `useFormik().useState()` or its alias, `useFormikState()`.
useFormikContext() only contains a stable API as well as some config props which should eventually be moved to a separate FormikConfigContext for further optimization.
49 changes: 49 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run NPM Dev",
"request": "launch",
"type": "node",
"runtimeArgs": [
"run",
"start:app"
],
"runtimeExecutable": "npm",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
},
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/lerna",
"args": [
"run",
"test",
"--",
"--runInBand",
// when filtering, every package not matching will have no tests
"--passWithNoTests",
"--testTimeout=9999"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"protocol": "inspector",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true
},
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/app"
}
]
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"jest.pathToJest": "npm run test:vscode --",
}
21 changes: 21 additions & 0 deletions app/components/debugging/Collapse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';

export const Collapse: React.FC = props => {
const [collapsed, setCollapsed] = React.useState(false);

return (
<div>
<button type="button" onClick={() => setCollapsed(!collapsed)}>
Collapse
</button>
<div
style={{
overflow: 'hidden',
height: collapsed ? 0 : 'auto',
}}
>
{props.children}
</div>
</div>
);
};
19 changes: 19 additions & 0 deletions app/components/debugging/DebugProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';

export const DebugProps = (props?: any) => {
const renderCount = React.useRef(0);
return (
<div style={{ margin: '1rem 0' }}>
<pre
style={{
background: '#f6f8fa',
fontSize: '.65rem',
padding: '.5rem',
}}
>
<strong>props</strong> = {JSON.stringify(props, null, 2)}
<strong>renders</strong> = {renderCount.current++}
</pre>
</div>
);
};
9 changes: 9 additions & 0 deletions app/helpers/array-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { selectRandomInt } from './random-helpers';

export const selectRange = (count: number) => Array.from(Array(count).keys());

export const selectRandomArrayItem = <T extends any>(array: T[]) => {
const index = selectRandomInt(array.length);

return array[index];
};
89 changes: 89 additions & 0 deletions app/helpers/chaos-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { FormikApi } from 'formik';
import { useMemo, useEffect } from 'react';
import { selectRandomInt } from './random-helpers';

export type DynamicValues = Record<string, string>;

export const useChaosHelpers = (
formik: FormikApi<DynamicValues>,
array: number[]
) => {
return useMemo(
() => [
() =>
formik.setValues(
array.reduce<Record<string, string>>((prev, id) => {
prev[`Input ${id}`] = selectRandomInt(500).toString();

if (prev[`Input ${id}`]) {
}

return prev;
}, {})
),
() =>
formik.setErrors(
array.reduce<Record<string, string>>((prev, id) => {
const error = selectRandomInt(500);

// leave some errors empty
prev[`Input ${id}`] = error % 5 === 0 ? '' : error.toString();

return prev;
}, {})
),
() =>
formik.setTouched(
array.reduce<Record<string, boolean>>((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
}
}
};
9 changes: 9 additions & 0 deletions app/helpers/random-helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
};
41 changes: 41 additions & 0 deletions app/helpers/tearing-helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
};
50 changes: 50 additions & 0 deletions app/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// Use Next Loaders on Linked Packages
// https://github.com/vercel/next.js/pull/13542#issuecomment-679085557
//
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');

module.exports = {
webpack: (config, { defaultLoaders, webpack }) => {
if (config.mode === 'development') {
config.module.rules = [
...config.module.rules,
{
test: /\.(tsx|ts|js|mjs|jsx)$/,
include: [path.resolve(config.context, '../')],
use: defaultLoaders.babel,
exclude: excludePath => {
return /node_modules/.test(excludePath);
},
},
];

// tsdx uses __DEV__
config.plugins.push(
new webpack.DefinePlugin({
__DEV__: process.env.NODE_ENV === 'development',
})
);
} else {
// Remove TSConfigPath aliases.
// We should use a tool which supports tsconfig.build.json
config.resolve.plugins = config.resolve.plugins.filter(
plugin => plugin.constructor.name !== 'JsConfigPathsPlugin'
);
}

return config;
},

onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},

// we don't need to break on TS errors since app/ is not production code.
// in development we alias /app -> /packages/formik/src via TSConfig.paths
// then during build we remove the JsConfigPathsPlugin to remove that alias
// but there is no way to remove the link with Next + TypeScript, like using tsconfig.build.json
typescript: { ignoreBuildErrors: true }
};
8 changes: 4 additions & 4 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"start": "next start"
},
"dependencies": {
"formik": "^2.1.5",
"formik": "^3.0.0-refs1",
"next": "9.5.3",
"react": "16.13.1",
"react-dom": "16.13.1",
"yup": "^0.29.3"
"react": "^17.0.1",
"react-dom": "^17.0.1",
"yup": "^0.28.1"
}
}
56 changes: 56 additions & 0 deletions app/pages/fixtures/components.tsx
Original file line number Diff line number Diff line change
@@ -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<string, typeof initialValues>) => (
<>
<input data-testid="child" {...props.field} />
<DebugProps {...props} />
</>
);
const ComponentComponent = (
props: FieldProps<string, typeof initialValues>
) => (
<>
<input data-testid="child" {...props.field} />
<DebugProps {...props} />
</>
);
const AsComponent = (
props: FieldProps<string, typeof initialValues>['field']
) => (
<>
<input data-testid="child" {...props} />
<DebugProps {...props} />
</>
);

const ComponentsPage = () => (
<div>
<h1>Test Components</h1>
<Formik
initialValues={initialValues}
validate={values => {
console.log(values);
}}
onSubmit={async values => {
await new Promise(r => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
<Form>
<Field name="name" children={RenderComponent} />
<Field name="name" render={RenderComponent} />
<Field name="name" component={ComponentComponent} />
<Field name="name" as={AsComponent} />
<button type="submit">Submit</button>
</Form>
</Formik>
</div>
);

export default ComponentsPage;
Loading