-
-
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
Separate Formik State and API Surface #2931
Conversation
|
@jawnrom is attempting to deploy a commit to the Formium Team on Vercel. A member of the Team first needs to authorize it. |
…ausing inputs to render each other. More to come!
Updated the description to include a heck of a lot of details on how the Core API will work, and how my experimental refs implementation will work. |
…pment app for building Formik by using tsconfig aliases and enabling recompilation via the nextjs webpack config. No more yarn link.
I just optimized a few things and now the performance is close to v3. @jaredpalmer how did you get timing metrics for testing performance? In my last set of changes, I rewired the /app so that it compiles the Formik packages while running, and breakpoints etc are accessible via a vs code Chrome launch task. No more yarn link. I didn't touch the tsdx build process. To try it out, pull this PR, run I added 2 pages, one that tests 500 inputs using v3 and one that tests 500 inputs using @formik/reducer-refs. I couldn't tell which was faster by feeling alone. |
Set up unit test environment for vscode. Set up dev environment for vscode. Write example unit test for @formik/core.
Johnrom/formik core 2
Switched |
This comment has been minimized.
This comment has been minimized.
Force `@types/react@16.x` dependency resolution in root package.json
Resolved typescript issue with |
It was challenging to get TypeScript to play nicely with this system, BUT! Alas, it is done. I've simplified the Subscriptions API in to more manageable chunks, using a new useSubscriptions API and keeping the functionality in there and separate from the main formik component. The new API is like so: import { isEqual } from 'lodash';
const MySubscriber = () => {
const { createSubscriber, createSelector } = useFormikApi<FormValues>();
/**
* Create a subscriber. Memoizing it is the most important thing you can do.
* We might want to use a `useSubscriber` hook instead, so we can make sure these get memoized.
* Or, we can handle the memoizing in `useFormikStateSubscription`. But we'd lose some configurability.
*/
const subscriber = React.useMemo(
() =>
createSubscriber(
createSelector(selectFieldMetaByName, [fieldName]),
isEqual
),
[createSelector, createSubscriber, fieldName]
);
// GEE it was hard to get `fieldMeta` automatically typed.
const fieldMeta = useFormikStateSubscription(subscriber);
} I think the footprint can get even smaller, but I'm pretty tired of battling typescript at this point, lol. Similar to what is discussed, batched updates just do this: unstable_batchedUpdates(() => {
subscriptionsRef.current.forEach(subscription => {
const prevState = subscription.prevStateRef.current;
const newState = subscription.selector(state);
if (!subscription.subscriber.comparer(prevState, newState)) {
subscription.prevStateRef.current = newState;
subscription.listeners.forEach(listener => listener(newState));
}
});
}); I abandoned the Map since I couldn't determine a great way of establishing identity with selector, args + comparers. The refs within Formik cannot be modified during render. Open to any ideas, but for now I'm just using an array. Performance is comparable to the useContextSelector version at 500 inputs, though the initial render is somewhat more expensive since each field has to search for an existing subscription. |
The other alternative I can think of to find existing Subscriptions is the following. Not sure exactly how performance would differ without testing. // as exists in the PR
const subscriptions: SubscriptionsRef = useRef([]);
// complex mapping object to keep track of what selectors / comparers / args map to which subscriptions
const subscriptionMap = new Map();
subscriptionMap.push(
selectIsSubmitting, // selector
new Map(
Object.is, // comparer
{
subscription: subscriptions[0] // for example
nextArgs: undefined,
}
)
);
subscriptionMap.push(
selectFieldMetaByName, // selector
new Map(
_.isEqual, // comparer
{
subscription: undefined, // there should be no version of this without args
nextArg: new Map(
"firstName", // first arg
{
subscription: subscriptions[1],
nextArg: undefined, // unless there are more args passed in createSelector
}
)
}
)
); It might not even really be faster, so I wouldn't implement this unless we really wanted to do a performance audit on this part. |
…an we do `useFormikState(selector: [Selector<State, Args, Return>, ...Args] | Selector<State, Return>, comparer: Comparer<Return>`, removing the implementation details and having it automatically memoized?
Johnrom/formik core 2
Fix resetForm so it always generates a new `values` to test against i…
Fix import paths.
More import fixes.
…n of concerns. Change the default formik state hook to `useFormikState(selector | [selector, ...args], comparer)` with auto-memoization. Stop exposing the useFormikSubscription hook. `useFormikState` is not quite auto-inferrable from TypeScript.
Simplifying API.
I've simplified the API to look like this:
|
Upgrade to TS4 (tsdx still uses TS3.x)
Finish the other hooks + ErrorMessage component.
The reducer-refs API enables easy optional subscriptions, such as the This could open up an easy implementation of the age old "form-level change event". // this one automatically subscribes to values and triggers an effect when it changes
<Form onChange={values => { console.log('changed!', values); }} />
// this one doesn't, and never re-renders
<Form /> |
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 #3089 |
Replaces #2828
Creates a @formik/core package which contains the central Formik API
useFormikCore
, then creates a new Ref-loving version of Formik which uses that API. Isolating the Core API will allow us to experiment with switching between useContextSelector, useMutableSource, refs, etc. It will also let us unit test the individual bits of Formik instead of using integration tests as unit tests.Up next:
The new Core API
@formik/core
The new Formik Core API is being used in the background by all Formik wrappers. This Formik Core API is meant to be separate from the actual React bindings (though it still has a dependency on React, for now), and be interchangeable between different implementations (useMutableSource, useContextSelector, etc). It takes four parameters,
getState
,dispatch
,formikProps
andisMounted
, and from only those four parameters, it generates the following API surface.Anywhere the API needs to get a value from Formik's state, it calls getState, and anytime it needs to dispatch an update, it calls dispatch. This is a redux-like inversion of control.
The new implementation framework
Any implementation of the Formik API will expose the FormikCore API as well as the following helpers:
I've added a new Formik implementation,
@formik/reducer-refs
which uses a redux-like state machine to dispatch changes to its internal state (via dispatch) to React's UI state (via useState). As of right now, that implementation isn't complete, BUT! it provides a great example of how I will modify the originalformik
to use the new@formik/core
API. Basically, Formik would just pass getState() as a callback() => state
from useReducer, and the dispatch as is.The implementation now exposes a stable
useFormikApi
which doesn't trigger UI updates (or it won't, when I'm done!), anduseFormikContext
decorates that API with state which will trigger UI updates. I hope eventually to deprecate useFormikContext in favor ofconst [state, api] = useFormikState();
because it is more React-like.Great question! As described #2846 , the new reduxy API works like this:
Form effects
Field effects
The useField implementation is one example of how we can optimize things and also let users optimize things their own queries to Formik's parameters. For example, if a user only needs to re-render if
isValid
changes, they can do this. Consequently, this would be the exact definition ofuseIsValid
from the new useContextSelector-based API.We will also provide the full set of hooks added by v3 in the useContextSelector changes, which will use this method.