Advanced Redux Form Pattern: Derived (but Editable) Fields

Subscribe
Advanced Redux Form Pattern: Derived (but Editable) Fields
Blog Feature

UX  |  forms  |  React  |  Redux  |  Technology

Let’s face it, forms can be extremely complicated. There are all sorts of issues you run into with forms to provide a nice user experience. That’s why it’s really handy to have a library to help you out. If using React/Redux, Redux Forms is a great library to help you manage that form state in your Store. Having Redux Forms work with the store also opens up the possibility to customize behavior via your own reducers. This will let you take advantage of all the built-in functionality of the library but also handle your potentially complicating scenarios. I’m going to show you how to leverage this concept to build a specialized form that reflects a requirement we had for a project, a derived but editable field.

Derived But Editable?

This is a field that we want to default for the user based on their current input but then let them change at will. Don’t worry I’ve got an example.

The above is a fork from a simple form example provided by the Redux Form creator @erikras with our more complex form. The basic gist is that we have 3 fields, an Amount, a Discount Percent and a Total Value but want the user to be able to edit all three. Each value affects the others in a defined way.

  1. When changing Amount, update Total (keeping Percent fixed)
  2. When changing Percent, update Total (keeping Amount fixed),
  3. When changing Total, update Amount (keeping Percent fixed).

Of course you can change these rules but they’ll serve for our purposes.

There are two basic ways to handle this in your redux form. The first is to send your own onChange event handler down to the input component, then instead of doing the default action which is to just change the field in question, you would dispatch multiple actions. So something like this:

// in form rendering view:
const CustomInput = (props) => {
const customOnChange =
(evt) => {
console.log( 'custom on change called with: ', props)
props.input.onChange( evt.target.value);
// dispatch another event here to update another field
}

. return (
<input {...props.input} onChange={customonChange}/>
);

}

render() => {
return (

<Field
name="amount"
component={CustomInput}
placeholder="Amount"
/>

);
}

FYI this is also a way to prevent a redux form action from happening. Just override the onChange or on* and don’t call the input’s onChange function if some condition is met

However there are a lot of problems with this approach. First, how do I dispatch another event to update the other field? Well, to follow the convention you’d have to connect some component and provide a custom dispatch event for what you’re doing. In our case, it’d be something like this:

import { connect } from 'react-redux';
import { change } from 'redux-form';

mapStateToProps = state => { /*...*/ }
mapDispatchToProps = dispatch => ({
    updateTotalFromAmount : (amount) => {
        return dispatch(change('formName', 'total', amount));
    }
})
export default connect(mapStateToProps, mapDispatchToProps)(SomeParentComponent);

There’s a lot to be desired in this solution.

  1. Overriding default behavior significantly complicates your views (as you can see above)
  2. Similarly, this is putting business logic in your views, making it more difficult to test
  3. In redux, we’re encouraged to have actions mutate your state to the next viable application state. Dispatching multiple actions to get where we want means that we temporarily have our state in an invalid… state.

A better way, use the reducers

If we think in the ‘redux’ way, we’d realize that what we really need is to hook into the reducers for redux form. When a change action is dispatched, we need to update two values (at least for our case), not just one. Redux Form recently introduced the concept of plugins that let us do just that.

The redux form examples list a case where you can use plugins to respond to your application’s custom actions, but it’s fairly easy to respond to the redux form actions themselves as well. The general gist will be:

  1. Listen for a CHANGE action for our form
  2. Check the field in the payload, if it’s one we need to update a derived field on then:
  3. Update the state to set the derived value as well.

First off, let’s get some simple pure functions defined for updating the amount and the total (we currently don’t need to update the percent).

const calculateTotal = (amount, discount) => {
  const discountAmount = 1 - discount / 100;
  return (amount * discountAmount).toFixed(2);
};

const calculateAmount = (total, discount) => {
  const discountAmount = 1 - discount / 100;
  return (total / discountAmount).toFixed(2);
};

Next, let’s create our reducer:

import { createStore, combineReducers } from 'redux';
import { reducer as reduxFormReducer,actionTypes } from 'redux-form';
import dotProp from 'dot-prop-immutable';

const reducer = combineReducers({
  form: reduxFormReducer.plugin({
    simple: (state, action) => {  // state is the state slice of this form ( form.simple )
      switch (action.type) {
        case actionTypes.CHANGE:
        case actionTypes.BLUR:  // Listen to both CHANGE and BLUR as both can set the value of your field
          const field = action.meta.field;
          const {amount>, discount = 0, total} = state.values;  // get current values of form (including current change)
          if(field === 'amount'){
            return dotProp.set(state, 'values.total', calculateTotal(action.payload , discount));
          }
          if(field === 'discount'){
            return dotProp.set(state, 'values.total', calculateTotal(amount, action.payload));            
          }
          if (field === 'total') {
            return dotProp.set(state, 'values.amount', calculateAmount(action.payload, discount
            ));
          }
          return state;
      }
      return state;
    }
  }),
});

This is code from the above example. It’s all combined here in the single combineReducers but obviously you’d want to extract this to a separate module.

One thing to remember about this method is that your plugin reducer is run after the default actions of the form. That means that when a CHANGE action happens and amount is updated, the state we receive in our custom reducer already has the updated amount. This is nice because now we just have to worry about updating our derived values.

To do that, we grab the field that was changed, the current values of those fields from the state, and then calculate the new values. Here I’m using dot-prop-immutable to make it look cleaner but you can easily use the spread operator if you like.

Advantages and Disadvantages

First and foremost, this pattern allows for easy testing of your application business logic via unit tests. It’s very easy to write a test that reads like a requirements specification. When the Amount is 100 and the discount is 5 then the Total Amount should be 95.

It also simplifies your views to be just that, views of your data. They don’t know or care how things get updated, they just respond to the redux store. This makes a lot of sense. Similarly, having all business logic in a single place helps keep the entire project coherent.

Another advantage is that you’re not limited to the redux form actions, as the redux plugin example shows. This means you can have custom actions that update or remove (or reset) parts of your form. All you have to do is add them to the reducer.

There are some downsides to this approach. The first is that if you have a large, deep or complex form, to get the current state of the object you’re trying to modify you’ll have to use regexes to get to your data based on the meta.field property. More on that in a possible later post.

Also, this applies to all reducers but with this approach comes the ability to shoot yourself in the foot. As mentioned in the docs, this is a powerful tool and with that power comes the ability to corrupt or completely destroy your form state.

We’ve been using this pattern on our project and find it really useful. I hope you do too!

Get the latest Aviturian insights sent straight to your inbox.

Leave a comment:

Ready to uncover solutions that transform your organization?

It’s never too late to build a culture of innovation. First, let’s discuss your vision, then map the journey to get there.

Start talking strategy