-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Conversation
Added Ref State. Added useSelectorComparer. Starting to build subscriptions.
use-subscriptions might not work in React 17? Seems returning the previous value doesn't bail out the render.
Sync up formik-native and formik for v3.
🦋 Changeset detectedLatest commit: f3bd303 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@johnrom is attempting to deploy a commit to the Formium Team on Vercel. A member of the Team first needs to authorize it. |
initialValues: state.initialValues, | ||
}); | ||
|
||
export const useFormikApiComputedState = <Values = any>(formikApi: FormikApi<Values>) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider removing? or replacing with just useIsValid
useIsDirty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved computed state to api.useComputedState()
so it is accessible from anywhere, and only expose the hooks useIsValid
and useIsDirty
.
|
||
export const selectFullState = <T>(value: T) => value; | ||
|
||
export const useFullFormikState = <Values>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
full, computed, api, state, selectFull, need to reduce the surface area.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useFormikApi
is needed, as it gets the Context, which is now only the APIuseFormikState
is an alias to(selector, comparer) => useFormikApi().useState(selector, comparer)
, which seemed a little tedious to make users use. However, we have to expose that for any of this to work, so it's up to you whether we keep this hook or make users useuseFormikApi().useState()
useFormikApiState(api, ...etc)
can probably go, since it is justapi.useState(selector, comparer)
;selectFullState
is not something we should expose, it's just something we use internally instead of memoizing a new selector. I can go through and mark stuff like this @internal.useFullFormikState
is used by backwards compatibleuseFormikContext
which users expect to load the legacy "formikBag". It's also used internally to subscribe to the full state in the case of Formik and Fieldrender
/child fn
s which do not have access to hooks to get Formik's state. it doesn't need to be exposed -- I can mark it @internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can useFormikState become useFormikStateSelector() or just useFormikSelector()?
I would also like to suggest changing useFormikApi() to useFormikActions() or useFormikMethods(). Mentally, easier to think about state or actions/methods versus state or api I think. api could be anything and it isn’t clear whether or it holds state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: I now see that these mean different things and my prior comment is wrong.
I still think API is a meaningless word and would like to see it changed. useFormikApiState vs. useFormikApi vs. useFormik vs. useFormikState isn’t intuitive for end users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formik API is the only word here that makes sense to me.
- A getter is not an action
- A method is a function belonging to an object in OOP, which isn't really applicable to functional programming like Formik whereas it might apply more in other form APIs since they use classes under the hood.
- The type returned by
useFormik
isFormikApi
and the type exposed by Context is alsoFormikApi
, souseFormikApi
seems to be the best naming convention for the context since we can't useuseFormikContext
.
If you do not believe that useFormik
returns an API, that is a whole different story. I don't think it returns Methods, or Actions. Maybe Helpers?? But we already draw a distinction in TypeScript between Helpers, Handlers, and Config.
Let's decide what the TypeScript name for the return value of useFormik
is, and we'll change the name of useFormikApi
to match. Whatever it is, the Context holds that value so the names should match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'll be super clear that "API" is the right word for the API that Formik provides if we document it separately from the various components and hooks that we provide, and separately from the State.
A person who simply uses useFormik()
and builds everything else from scratch is only using the Formik API, and we should document that separately. The current documentation uses API Reference to mean "all the things" whereas the API Formik provides is just what is returned by useFormik
and every other component or hook we provide is a shortcut for using that API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can always break backwards compatibility and switch useFormikContext
to just have the API, and then it'll be useFormikContext
and useFormikState
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did the above, switched back to useFormikContext and useFormikState, abandoning backwards compatibility for clarity.
@@ -103,6 +103,7 @@ describe('withFormik()', () => { | |||
setSubmitting: expect.any(Function), | |||
setTouched: expect.any(Function), | |||
setValues: expect.any(Function), | |||
isFormValid: expect.any(Function), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isValid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isFormValid is a function passed by Formik API which is needed to calculate whether the form is valid. Otherwise, we need to pass a bunch of config props from Formik through the API, and if a user doesn't memoize them we lose all of the performance gains.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed isFormValid in favor of exposing formikApi.useComputedState().
This currently passes the following props through the API which I think we should remove, because they are likely to cause re-renders if a user doesn't optimize them. {
validationSchema?: any | (() => any);
validate?: (values: Values) => void | object | ValidateFn<Values>;
} |
…te will do. If we call our own reducer then use useReducer dispatch, `state.values !== getState().values`.
Ignore the above, as this is just how batching works in React. I've been thinking about this for too long. 😂 I'm going to start adding some of the |
…ormikReducerState + FormikComputedState. Add Fixtures and Tutorial code to /app.
Consolidate State and Add Tutorial + Fixtures.
I figured out how to remove |
@jaredpalmer this is ready for review and I believe I've resolved all the items above. I did the following:
You can test the branch locally by pulling the branch in VS Code, running |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there...
packages/formik/src/Formik.tsx
Outdated
Selector, | ||
useOptimizedSelector, | ||
} from './hooks/useOptimizedSelector'; | ||
import { unstable_batchedUpdates } from 'react-dom'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does this work with react native?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to test it out, but I think you're right this needs a different import for react native. I've never actually used React Native (except to play with the default expo starter), but maybe we should have an expo version of app/ for testing?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to manually batch updates here anymore as I moved this into an effect. I'll remove and test.
if (__DEV__) { | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useEffect(() => { | ||
invariant( | ||
typeof isInitialValid === 'undefined', | ||
typeof props.isInitialValid === 'undefined', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we actually break the rules of hooks here? (i believe the rationale is that we only ever want to show this error message once) If so, let's add a comment about it or fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I didn't make any changes to this except to update the variable, but I believe you wrote it so that it would only print the warning once in development on the first use.
I would create a separate issue for this since it's unrelated to this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #3145 for this, as I wasn't involved in the DEV checks.
packages/formik/src/Formik.tsx
Outdated
/** | ||
* This is the true test of spacetime. Every method | ||
* Formik uses must carefully consider whether it | ||
* needs to use the ref or the render snapshot. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Elaborate more here in comments for outside contributors as to why we need this optimization. Include mental model for deciding whether to use ref and or snapshot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I currently left the code functioning how it does currently even though I created the ability to "fast-forward" via refs. But I need to go and revisit exactly what each bit of Formik uses, and whether it currently uses the right one. I'm pretty sure the answer is always use getState() internally, except for code used inside useState
.
@@ -334,53 +398,54 @@ export function useFormik<Values extends FormikValues = FormikValues>({ | |||
} | |||
); | |||
|
|||
React.useEffect(() => { | |||
const performValidationOnMount = useEventCallback(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
executeValidationOnMount
for consistency?
packages/formik/src/Formik.tsx
Outdated
isMounted.current === true && | ||
!isEqual(state.initialTouched, nextInitialTouchedProp) | ||
) { | ||
const touched = nextInitialTouchedProp || emptyTouched; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const touched = nextInitialTouchedProp || emptyTouched; | |
const touched = nextInitialTouchedProp ?? emptyTouched; |
packages/formik/src/hooks/hooks.ts
Outdated
import { isShallowEqual } from '../utils'; | ||
|
||
/** | ||
* Returns @see FieldMetaProps<Value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve comment. This will be shown in intellisense.
import { FormikApi, FormikReducerState } from '../types'; | ||
|
||
/** | ||
* @see {@link FormikApi['useState']} for info on using Formik's State. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve comment
export type Comparer<Return> = (prev: Return, next: Return) => boolean; | ||
|
||
const UNINITIALIZED_VALUE = Symbol(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved this package to its own unit tested npm package in another PR. see #3124.
@@ -1,3 +1,5 @@ | |||
export * from './hooks/useFormikState'; | |||
export * from './hooks/hooks'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are exporting useOptimizedSelector
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In another PR under this milestone, I move useOptimizedSelector to a separate package with unit testing etc. You should check that out before I modify this.
Also, I don't think I exported this but I'll double check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not exported publicly by index.tsx
packages/formik/src/types.tsx
Outdated
export type SetSubmittingFn = (isSubmitting: boolean) => void; | ||
|
||
export type SetTouchedFn<Values extends FormikValues> = ( | ||
touched: import('./types').FormikTouched<Values>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this import()
here? is this importing itself? that works???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think vs code autocompleted this... And somehow it works I guess?! Lol
…API. Moved FieldHelpers to their own hooks. Removed batchedUpdates since we use an Effect. Some minor type fixes, code and documentation cleanup.
@jaredpalmer I simplified the mental model here by enforcing a boundary between the I moved FieldHelpers to their own hooks as well, to enforce this boundary.
Also fixed various other items and pushed back on items which can be addressed outside of this PR. |
This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days |
closed in favor of #3231 |
This pivots Formik away from using Context for anything but the API itself, and exposes a
formik.useState()
implementation to subscribe to Formik State via a selector and comparer when you need to do specialized equality checks. Implemented with use-subscription.Upgraded Dev Environment
The new Development environment is awesome. Try it out. VS Code will now run your tests automatically with the Jest extension. Running
npm run start:app
, or just Launching from VS Code, will now run theapp/
folder aliased directly to Formik's source at localhost:3000, so you can test and develop Formik directly through /App. You can also launch Chrome against it to attach breakpoints to Formik.