Allow users to hook into and reduce state changes.
N/A
const MY_CUSTOM_FORMIK_ACTION = 'MY_CUSTOM_FORMIK_ACTION'
<Formik
reducer={(prevState, nextState, action) =>{
if (action.type === FormikActions.SET_VALUES) {
// do something
return nextState
}
if (action.type === MY_CUSTOM_FORMIK_ACTION) {
// since this action does not exist inside of formik's internal reducer,
// nextState === prevState.
//
return {...prevState, warning: action.payload }
}
return prevState;
})
render=(({ dispatch, warning }) =>
<Form>
<button onClick={() => dispatch({ type: MY_CUSTOM_FORMIK_ACTION, payload: 'covfefe' }>
Set a warning
</button>
{warning && warning}
[/** .. */}
</Form>
}/>
/>
This would run on every state update. This approach would allow user's to add custom reducers and middleware on top of formik. For example, here's some pseudo code of a logger and quick and dirty formik-persist feature:
const logger = (prevState, nextState, action) => {
console.log(`--- FORMIK ${action.type} ---`)
console.log('previous form state', prevState)
console.log('action', action.type, action)
console.log('next form state', nextState)
console.log(`-----------------------------`)
return nextState;
}
const persist = (prevState, nextState) => {
// would probably debounce this. but this is core idea.
window.localStorage.setItem('state', JSON.stringify(nextState));
return nextState;
}
// Usage
<Formik
onSubmit={...}
initialValues={JSON.parse(window.localStorage.getItem('state')) || { ... }}
reducer={compose(persist, logger)}
render={props => ... }
/>
The reason this is better than just using regular react state, is that the reducers should be highly reusable across your forms. Furthermore, if you don't like a specific feature of Formik, you can now safely extend or modify Formik.
We would need to rewrite Formik's internals to use a "reducer" pattern. This is quite simple actually.
export const FormikActions = {
SET_VALUES: 'SET_VALUES',
SET_ERRORS: 'SET_ERRORS',
SET_TOUCHED: 'SET_TOUCHED',
}
export class Formik extends React.Component {
static defaultProps = {
reducer: (_prevState, nextState) => nextState // default
}
reducer = (state, props, action) => {
switch (action.type) {
case FormikActions.SET_VALUES:
return { ...state,
values: action.payload
}
case FormikActions.SET_ERRORS:
return { ...state,
errors: action.payload
}
case FormikActions.SET_TOUCHED:
return { ...state,
touched: action.payload
}
default:
return state
}
}
dispatch = (action) => {
this.setState((prevState, props) => this.props.reducer(prevState, this.reducer(prevState, props, action)))
}
setValues = (payload) => this.dispatch({
type: FormikAction.SET_VALUES,
payload
})
setErrors = (payload) => this.dispatch({
type: FormikAction.SET_ERRORS,
payload
})
setTouched = (payload) => this.dispatch({
type: FormikAction.SET_TOUCHED,
payload
})
// and on and on
render() {
// ... same same
}
}
Regarding Action shape, my thoughts....
{
type: string;
payload: any;
}
{
type: string;
promise: Promise<any>,
meta: {
onStart: (state: FormikState<Values>, props: Props) => void;
onSuccess: (state: FormikState<Values>, props: Props) => void;
onFailure: (state: FormikState<Values>, props: Props) => void;
finally: (state: FormikState<Values>, props: Props) => void;
always: (state: FormikState<Values>, props: Props) => void;
}
}
status and setStatus or the new (yet undocumented) setFormikState?setFormikState is a code smell IMHO, which is why we didn't ship it for so long. A reducer/middleware pattern lends keeps Formik uncontrolled from the user's perspective, and yet allows Formik's internals to be augmented.
Really like this approach. It would be helpful when a change of one field should cause another field to change automatically (e.g. in dynamic forms). We actually such use-cases in our app 👍
Regarding the technical approach to the reducer itself: I've found the middleware pattern of redux quite useful.
In the provided code the previous and the next values are already provided. This means the reducer given by the formik-user doesn't have a chance to manipulate the values before Formik handles them. There's also no way to hide an update from Formik.
Instead, I'd suggest passing a function down which calls the "next" reducer in the reducer chain, middleware style.
// old code
reducer={(prevState, nextState, action) => {
if (action.type === FormikActions.SET_VALUES) {
return nextState;
}
if (action.type === MY_CUSTOM_FORMIK_ACTION) {
return { ...prevState, warning: action.payload };
}
return prevState;
}}
// adapted code
// the reducer needs to return the next state for formik
// the reducer can alter the action it received before passing it to formik's reducer
// the reducer can skip formik's reducer by returning null
// the reducer can amend the state formik would generate (by manipulating next())
reducer={next => (prevState, action) => {
// here we manipulate the action passed to Formik's reducer
if (action.type === FormikActions.SET_VALUES)
return next({ ...action, foo: true });
// here we hide the update from formik completely
if (action.type === MY_CUSTOM_FORMIK_ACTION) return null;
// here we manipulate the state formik would generate
if (action.type === 'foo') {
const nextState = next(action);
return { ...nextState, foo: true };
}
// here we basically do nothing
return next(action);
}}
Is there any progress on this subject? I would like to see it in production because this is something what's really missing from redux-form/react-redux-form. Dynamic forms are the primary forms in our company and I think that redux shouldn't be used for this kind of forms because it doesn't need to know state on every input change. (it's really heavy and you still need some component's state so that you can use debounce, immediate error validators.. etc.)
Btw thanks for your great work! 👍
Will hopefully have time this weekend to publish the work I've done. It's a fully overhaul of the internals as you can imagine.
Please please! This would help in so many ways !
why is this closed? no progress?
They tend to close things a lot
Sent from my iPhone
On Jun 29, 2018, at 2:10 AM, Matan Assa <[email protected]notifications@github.com> wrote:
why is this closed? no progress?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHubhttps://nam05.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fjaredpalmer%2Fformik%2Fissues%2F401%23issuecomment-401281971&data=02%7C01%7C%7C058f157856b84f7d446f08d5dd97b97e%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C636658566071752877&sdata=t%2B5sshRc46OpKfu3gOztCaNW2pMcv29H6A6BHqkCcO8%3D&reserved=0, or mute the threadhttps://nam05.safelinks.protection.outlook.com/?url=https%3A%2F%2Fgithub.com%2Fnotifications%2Funsubscribe-auth%2FAYM5QdNWVnfwxGK08pAtgN5R5Fe8os5Vks5uBeDdgaJpZM4R4S7E&data=02%7C01%7C%7C058f157856b84f7d446f08d5dd97b97e%7C84df9e7fe9f640afb435aaaaaaaaaaaa%7C1%7C0%7C636658566071752877&sdata=Dfbu1EGjpg%2BHuFQhcd9RnGufdg69oHHYfxmHE5Yi2Cw%3D&reserved=0.
I made progress on this. I decided that, while we may end up using this technique internally for logging, there is already a great way to extend Formik functionality: plain ol’ React components. After back and forth, we actually couldn’t come up with a single example where a state reducer solves something that can’t be done with just components. If you have one, let me know.
Where's the back and forth discussion at? I don't see examples being solved in this PR that would explain alternative ways of getting notified for change.
Here's a concrete use case: See https://github.com/jaredpalmer/formik/issues/485#issuecomment-407590206
It certainly doesn't seem unreasonable to have an onChange and to want to be able to react to that like with a filter form as mentioned in this comment linked above.
There were many issues that were depending on this being implemented. If it's not going to be exposed to the public then we should re-open one of the others and at least have some simple onChange callback which provides all the current values.
If nothing else, we should at least have something like react-final-form where you can easily attach an onChange to an input that gets called after the supplied handleChange.
@jeffbski I just discovered this for being notified of onChange. Haven't used it yet: https://github.com/jaredpalmer/formik-effect
Awesome! thanks @morgs32 I'll try it out.
After back and forth, we actually couldn’t come up with a single example where a state reducer solves something that can’t be done with just components. If you have one, let me know.
Often, I face problem of dependant form fields, e.g. county/city selector, where one value should be reset/set depending on value of other field. Here is example.
In my opinion, instead of putting login on field level, logic should be present on some higher level, in one place. State reducer pattern would be perfect fit for such use case.
cc @jaredpalmer
I agree with it, I often have this use case and I needed to implement custom formik fields which not only call setFieldValue in onChange like any custom formik field should do, but also allow additional onChange callback passed to Field, which then is called in Field. It looks more or less like this based on simplified Material UI component:
const Input = ({
field,
form,
onChange,
}) => (
<TextField
{...field}
id={field.name}
value={field.value}
onBlur={() => form.setFieldTouched(field.name, true)}
onChange={e => {
form.setFieldValue(field.name, e.target.value);
if (onChange) {
setTimeout(() => {
onChange(e.target.value);
});
}
}}
/>
);
Then this onChange needs to be passed to Field, while it would be indeed much better to have centralized place for such transformations, like global reducer. Also notice ugly timeout which is needed in recent formik.
Downshift which also uses render prop pattern also have such a reducer implemented: https://github.com/downshift-js/downshift#statereducer
Most helpful comment
Where's the back and forth discussion at? I don't see examples being solved in this PR that would explain alternative ways of getting notified for change.
Here's a concrete use case: See https://github.com/jaredpalmer/formik/issues/485#issuecomment-407590206
It certainly doesn't seem unreasonable to have an onChange and to want to be able to react to that like with a filter form as mentioned in this comment linked above.
There were many issues that were depending on this being implemented. If it's not going to be exposed to the public then we should re-open one of the others and at least have some simple onChange callback which provides all the current values.
If nothing else, we should at least have something like react-final-form where you can easily attach an onChange to an input that gets called after the supplied handleChange.