RFC: Form Reducer / Middleware
See original GitHub issueFeature
Allow users to hook into and reduce state changes.
Current Behavior
N/A
Desired Behavior
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.
Use cases:
- Transforming values.
- Adding state (like warnings, submit counts, soft submits, etc.)
- Testing!
Suggested Solutions
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
}
}
Discussion
- Async. We would need a redux-thunk thing?
- Naming conventions
- Argument order
- Action shape
- binding to dispatch / action creators.
Regarding Action shape, my thoughts…
Sync Actions:
{
type: string;
payload: any;
}
Async actions (inspired by redux-pack)
{
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;
}
}
Why is this better than 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.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:27
- Comments:12 (7 by maintainers)
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.
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.