Practical Redux, Part 7: Form Change Handling, Data Editing, and Feature Reducers

This is a post in the Practical Redux series.


Techniques for managing form events, data editing capabilities, and reducer structures for features

Intro 🔗︎

In Part 6, we connected our lists directly to Redux, set up basic editing for the UnitInfo form, and added the ability to toggle "editing" mode for a selected Pilot. This time, we'll look at some advanced techniques for managing form change events, implement editing for model entries, and use a custom reducer structure to handle feature logic.

The code for this project is on Github at github.com/markerikson/project-minimek. Since this post was split off from Part 6, the original WIP commits I made for this post can be seen in PR #6: Practical Redux Part 6 WIP, and the final "clean" commits can be seen in in PR #7: Practical Redux Part 7 Final.

I'll be linking to each "final" commit as I go through the post, as well as specific files in those commits. I won't paste every changed file in here or show every single changed line, to save space, but rather try to show the most relevant changes for each commit as appropriate.

Table of Contents 🔗︎

Cleaning Up Some Code 🔗︎

Before we dive in to the real work for this section, we've got a few bits of cleanup that will help us later.

Handling Data Reloads 🔗︎

There's a problem with the current entitiesReducer logic. If we click the "Load Unit Info" button twice in a session, the reducer will try to load the same sample data again, and will throw errors because data with those IDs already exists in state. We can fix this by having the case reducer delete any existing entries before it loads in the new data.

Commit 36324ea: Clear out existing models on load to avoid conflicts when reloading data

app/reducers/entitiesReducer.js

    const {pilots, designs, mechs} = payload;

+   // Clear out any existing models from state so that we can avoid
+   // conflicts from the new data coming in if data is reloaded
+   [Pilot, Mech, MechDesign].forEach(modelType => {
+       modelType.all().withModels.forEach(model => model.delete());
+       session.state = session.reduce();
+   });

Note that we use the session.state = session.reduce() trick mentioned in Part 2 to create an intermediate updated version of the state after queueing up the delete commands for each model class.

Cleaning Up the PilotDetails Component 🔗︎

Since we're going to eventually add callback functions to handle the inputs in <PilotDetails>, now is a good time to convert it from a functional component to a class component.

Commit e1325ac: Convert PilotDetails to a class component

We're also going to rework the <PilotDetails> form a bit. The "gunnery" and "piloting" fields are going to have a limited range of values, so we might as well make them dropdowns. Also, while working on this part, I found out that the Semantic-UI-React <Form.Field> component supports some useful shorthand syntax. You can pass a label string and a component type as a prop, and it will render them. It will also forward any unknown props onwards to the input component it's rendering. here's an example:

-<Form.Field name="rank" width={16}>
-   <label>Rank</label>
-   <Dropdown
-       fluid
-       selection
-       options={RANKS}
-       value={rank}
-       disabled={!canStopEditing}
-   />
-</Form.Field>
+<Form.Field
+   name="rank"
+   label="Rank"
+   width={16}
+   control={Dropdown}
+   fluid
+   selection
+   options={RANKS}
+   value={rank}
+   disabled={!canStopEditing}
+/>

The name, label, and width props are handled by the <Form.Field> component. It renders an instance of <Dropdown>, and passes it the remaining props. (Order doesn't matter here, I just wrote them with the Field's props first and the Dropdown's props last.)

I suppose it's a tossup which formatting you prefer. I can see arguments both ways. But, given that I already did the work, we'll go with the latter approach :)

Commit dcdc0ec: Rework PilotDetails form layout for consistency

Investigating Form Actions 🔗︎

With that cleanup done, back to work on our input forms. In Part 6, I mentioned that "there's one problem with our text input that we need to address", and promised we'd come back to that later. Later is now :)

Let's type the letters "test" into the end of the "Unit Name" field, and look at the DevTools actions log:

We can see four different UNIT_INFO_UPDATE actions in that list from typing "test" into the name input. If we look at the next-to-last action, we can see that after typing the 's', the dispatched name was "Black Widow Companytes".

Right now, every single time we type a keystroke into the name input, we dispatch another Redux action. Every time we dispatch an action, every connected component gets notified, and has its mapState function run. As an example, that means that for every keystroke, all the <PilotsListRow> components are busy looking up their Pilot entries. But... all we really wanted to do was update the name input! None of the Pilots data is going to change from us typing here, so all that is wasted effort. The dispatched actions are also kind of cluttering up the actions log.

As mentioned previously, Project Mini-Mek really isn't a performance-critical application, and especially not at this stage of development. Still, it would be nice if we could cut down on the number of actual Redux actions dispatched.

Buffering Form Changes and Dispatches 🔗︎

I noted some of the issues with dispatching actions from form inputs while working on early prototypes for my own application. I needed a solution that could do several things for me:

  • Handle onChange events from input components
  • Respond to those change events by immediately passing the updated values back down to the input, for fast UI response
  • If several changes happened close together, only dispatch a single Redux action after they were all done, with the final values
  • Be generic enough that I could reuse it in a variety of different places.

I started playing around with some ideas, and eventually came up with a component I dubbed the "FormEditWrapper". (As you may have noticed, nothing is more permanent in programming than a temporary name. We'll call it FEW for short.)

My FormEditWrapper is a reusable, generic component that can buffer input changes in its own component state, combine those changes with incoming props, and debounce changes to minimize the number of dispatched Redux actions.. Let's look at the source, and I'll explain how it works.

Commit c321c35: Add FormEditWrapper component

common/components/FormEditWrapper.jsx

import React, {Component, PropTypes} from "react";
import {noop, debounce, defaults, values} from "lodash";

import {getValueFromEvent} from "common/utils/clientUtils";

class FormEditWrapper extends Component {
    static propTypes = {
        value : PropTypes.object.isRequired,
        isEditing : PropTypes.bool,
        onChange : PropTypes.func,
        valuePropName : PropTypes.string,
        onChangePropName : PropTypes.string,
        singleValue : PropTypes.bool,
        passIsEditing : PropTypes.bool,
        dispatchDelay : PropTypes.number,
    }
    
    static defaultProps = {
        isEditing : true,
        onChange : noop,
        valuePropName : "value",
        onChangePropName : "onChange",
        singleValue : false,
        passIsEditing : true,
        dispatchDelay : 250,
    }

    constructor(props) {
        super(props);
        const boundDispatchAttributes = this.dispatchAttributeChange.bind(this);
        this.dispatchAttributeChange = debounce(boundDispatchAttributes, props.dispatchDelay);

        this.state = {
            value : {},
        };
    }

    componentWillReceiveProps() {
        // Reset any component-local changes  Note that the incoming props
        // SHOULD match the changes we just had in local state.
        this.setState({value : {}});
    }

    onChildChange = (e) => {
        const {isEditing} = this.props;

        if(isEditing) {
            const newValues = getValueFromEvent(e);

            if(newValues) {
                const change = {
                    ...this.state.value,
                    ...newValues
                };

                // Update our component-local state with these changes, so that the child components
                // will re-render with the new values right away
                this.setState({value : change});

                // Because this is debounced, we will only call the passed-in props.onChange
                // once there is a pause in changes (like letting go of a held-down key)
                this.dispatchAttributeChange(change);
            }
        }
    }

    dispatchAttributeChange(change) {
        this.props.onChange(change);
    }

    render() {
        const {value : propsValue, children} = this.props;
        const {isEditing, passIsEditing, valuePropName, onChangePropName, singleValue} = this.props;
        const {value : stateValue = {}} = this.state;

        // Use incoming values from props IF there is no corresponding value
        // in local component state.  This allows local changes to win out.
        const currentValues = defaults({}, stateValue, propsValue);

        let valueToPassDown = currentValues;

        if(singleValue) {
            valueToPassDown = values(currentValues)[0];
        }

        const editingValue = passIsEditing ? {isEditing} : {};

        // Force the child form to re-render itself with these values
        const child = React.Children.only(children);

        const updatedChild = React.cloneElement(child, {
            [valuePropName] : valueToPassDown,
            [onChangePropName] : this.onChildChange,
            ...editingValue
        });

        return updatedChild;
    }
}

export default FormEditWrapper;

There's a lot of stuff going on in there. Let's break it down:

Passing Down Props to Children 🔗︎

First, FEW expects a prop named value, which should be an object. It also uses the React.Children API to ensure that it only has a single child. The most basic usage would look like:

<FormEditWrapper value={someObject}>
    <SomeChildComponent />
</FormEditWrapper>

FEW also uses another of React's top-level APIs: React.cloneElement(). Since the output of a React render function is just an object, and React exposes the render output of child components as props.children, it's possible for a component to return different children than what it was given. In this case, FEW is going to make a copy of the child render output it was given, and pass down a couple new or different props. One prop is the data value object, and the other prop is an onChange callback. So, in that minimal example, it's as if we had written SomeChildComponent value={someObject} onChange={someOnChangeCallback} />.

The second thing to note is that FEW supplies its own onChildChanged method as the onChange prop for its child. Whenever onChildChanged is called, FEW will update its internal state with the values passed up by the child, merging them with whatever values are already buffered in the state. Calling this.setState() always forces a re-render, and that leads into the next really interesting part.

Let's use the <PilotDetails> form as an example. That form has two main text inputs (name and age). Assume we have this setup:

const name = "Original Name";
const age = "42";

<FormEditWrapper value={ {name, age} }>
    <PilotDetails />
</FormEditWrapper>

If we type "newname" at the end of the name field, FEW's internal state would look like { value : { name : "Original Namenewname"}}. Meanwhile it's still being given the original value={ {name, age} } prop. We need to combine these two so that the name value we just typed overrides the name value coming in as a prop. The key is this line here:

// Use incoming values from props IF there is no corresponding value
// in local component state.  This allows local changes to win out.
const currentValues = defaults({}, stateValue, propsValue);

If we were to inspect currentValues after that line, we would see {name : "Original Namenewname", age : 42}. Since we didn't have an age prop in FEW's state, we copy that over. Since we do have a name field in state, we don't copy over the name field we were given as a prop. This means that any values from the child component will override the equivalent values from props, and the resulting object is passed back down to the child component, allowing it to re-render itself with the correct data.

Finally, when FEW receives new props, it clears out its internal state buffer. The assumption is that the new props will now be a "saved" version of the changes it's keeping inside, so it can clear those out and go back to just passing down the data it's getting as a prop.

Flexibility and Customization 🔗︎

Perhaps the child input or form component is different than most others. Maybe it needs its value as a prop named data (or in the case of <PilotDetails>, as a prop named pilot). Maybe it needs the callback prop to be named something other than onChange. FEW accepts some props that tell it to change its default behavior: valuePropName and onChangePropName. Going back to our earlier example, we could do:

<FormEditWrapper value={ {name, age} }  valuePropName="pilot">
    <PilotDetails />
</FormEditWrapper>

And now <PilotDetails> will be given props.pilot instead of props.value.

Another customization issue is that actual <input> components only want a specific value like a string, instead of an object. In that case, passing the singleValue={true} flag to FEW will tell it to assume there's only one field it needs to pass down, and to pass that down directly instead of the entire value object.

In my own application, a number of components also need to be given an isEditing flag. Originally I passed that through all the time, but when I added the singleValue option, I realized that inputs wouldn't need that prop, so I added a passIsEditing prop to control whether that gets passed down or not.

Buffering Dispatches 🔗︎

FEW's onChildChanged method will immediately call this.setState() with the new values from the child, and that queues up a re-render. That method also calls this.dispatchAttributeChanged(), which in turn calls this.props.onChange(). However, in the constructor, we create a debounced version of dispatchAttributeChanged, which will only actually run if enough time has elapsed since the last time it was called (by default, 250ms). That means that if the user quickly types several characters into a text input, dispatchAttributeChanged will be called repeatedly, but won't actually run for real until after the user is done typing. As a result, when it does run, it will call this.props.onChange() with the final value. So, instead of seeing "n", "ne", "new", and so on, you'd see a single "newname" value passed back up. The onChange prop doesn't have to be a Redux action creator, but it usually will be. That means that most of the time, only a single Redux action will be dispatched for a series of keystrokes.

FormEditWrapper in Action 🔗︎

With all that in mind, let's use FEW to buffer the "name" input in our <UnitInfo> component.

Commit 76e48af: Use FormEditWrapper with UnitInfo name input

features/unitInfo/UnitInfo/UnitInfo.jsx

+import FormEditWrapper from "common/components/FormEditWrapper";

// Omit irrelevant code
    render() {
-       const {unitInfo} = this.props;
+       const {unitInfo, updateUnitInfo} = this.props;
        const {name, affiliation} = unitInfo;

// Omit rendering code
                    <Form.Field name="name" width={6}>
                        <label>Unit Name</label>
+                       <FormEditWrapper
+                           singleValue={true}
+                           value={ {name} }
+                           onChange={updateUnitInfo}
+                           passIsEditing={false}
+                       >
                            <input
                                placeholder="Name"
                                name="name"
-                           value={name}
-                           onChange={this.onNameChanged}
                            />
+                       </FormEditWrapper>
                    <Form.Field name="affiliation" width={6}>

So now, if we type "test" at the end of the name input and look in the DevTools, we should see this:

Success! Four keystrokes, the input updated immediately as we typed, and only a single Redux action was dispatched.

Structuring Reducer Logic for Features 🔗︎

As mentioned in Part 4, we're using a "feature-first" folder structure. There's tradeoffs with both "file-type-first" and "feature-first" approaches.

With "file-type-first", you know where to look for a certain type of code, and it's really easy to pull together all the slice reducers into a root reducer file. However, each feature gets split across several folders, and if multiple features need to interact with a specific slice of state, the files or folders for that slice of reducer logic will start to get awfully crowded.

With "feature-first", you know exactly where all the files for a given feature are, but the process of combining the reducer logic together can be a bit tricker, especially if you're using the standard approach of combineReducers. In particular, what happens when multiple features need to interact with the same slice of state?

In our case, we've created an entities slice containing the "tables" for our relational data entries. Because this is a central data location for our application, several different features are going to need to make updates to the entities slice. We could put all the reducer logic from the different features together into one giant entitiesReducer, but that would get ugly rather quickly. What we really need is a way for each individual feature to separately apply updates to the entities slice.

Looking Beyond combineReducers 🔗︎

I'll paraphrase some of the key concepts from the Structuring Reducers section of the Redux docs:

It's important to understand that your entire application really only has one single reducer function: the function that you've passed into createStore as the first argument. It's good programming practice to take pieces of code that are very long or do many different things, and break them into smaller pieces that are easier to understand. Since a Redux reducer is just a function, the same concept applies. You can split some of your reducer logic out into another function, and call that new function from the parent function.

In Redux it is very common to structure reducer logic by delegating to other functions based on slice of state. Redux refers to this concept as "reducer composition", and it is by far the most widely-used approach to structuring reducer logic. In fact, it's so common that Redux includes a utility function called combineReducers(), which specifically abstracts the process of delegating work to other reducer functions based on slices of state. However, it's important to note that it is not the only pattern that can be used.

Once you go past the core use case for combineReducers, it's time to use more "custom" reducer logic, whether it be specific logic for a one-off use case, or a reusable function that could be widely shared. There's third-party reducer utilities that are available, or if none of the published utilities solve your use case, you can always write a function yourself that does just exactly what you need.

It's time to put some of these ideas into practice.

Using reduceReducers 🔗︎

If you happened to look at the reducerUtils.js file previously, you may have noted a function in there I haven't talked about yet: reduceReducers(). Here it is:

export function reduceReducers(...reducers) {
    return (previous, current) =>
        reducers.reduce(
            (p, r) => r(p, current),
            previous
        );
}

This was actually originally written by Andrew Clark, and is available as a package at https://github.com/acdlite/reduce-reducers . I swiped it and pasted it into reducerUtils.js to skip taking on another dependency.

reduceReducers is a nifty little utility. It lets us supply multiple reducer functions as arguments and effectively forms a pipeline out of those functions, then returns a new reducer function. If we call that new reducer with the top-level state, it will call the first input reducer with the state, pass the output of that to the second input reducer, and so on. (If I were more Functional-Programming-minded, I'd guess that there's probably a lot of similarities with compose(), but this is about as far as my mind goes in that direction. I'm sure someone will be happy to correct me or clarify things in the comments.)

Let's rework our root reducer to make use of reduceReducers:

Commit 9d6b443: Update root reducer to wrap around combined reducer

app/reducers/rootReducer.js

import {reduceReducers} from "common/utils/reducerUtils";

const combinedReducer = combineReducers({
    entities : entitiesReducer,
    unitInfo : unitInfoReducer,
    pilots : pilotsReducer,
    mechs : mechsReducer,
    tabs : tabReducer,
});


const rootReducer = reduceReducers(
    combinedReducer,
);

export default rootReducer;

Our old "root reducer" is now the combinedReducer, and we pass that into reduceReducers to get the new root reducer.

It's important to note that the combined reducer should be first in the pipeline, because that defines the shape of the state. (See the Initializing State section of the Redux docs for more info on why and how the combined reducer defines the state shape.)

Writing a Higher Order Reducer 🔗︎

A "higher order reducer" is any function that takes a reducer as an argument, and returns a new reducer. That includes combineReducers and reduceReducers. Now, we're going to write one of our own.

If all the reducer functions given to reduceReducers are called with the top-level state object, but a given feature only needs to apply updates to one piece of the state, we're going to have a lot of repetitive logic as each feature reducer tries to apply its updates. In addition, reduceReducers will be calling these feature reducers will be called on every dispatched action, so it would be nice to ensure the feature reducers only respond if the action is something they care about.

Putting these ideas together, we can build a higher order reducer function called createConditionalSliceReducer:

Commit 3b5b2bf: Add createConditionalSliceReducer utility

common/utils/reducerUtils.js


export function createConditionalSliceReducer(sliceName, fnMap) {
    // Create a reducer that knows how to handle one slice of state, with these action types
    const sliceReducer = createReducer({}, fnMap);

    // Create a new wrapping reducer
    return (state, action) => {
        // Check to see if this slice reducer knows how to handle this action
        if(fnMap[action.type]) {
            // If it does, pass the slice to the slice reducer, and update the slice
            return {
                ...state,
                [sliceName] : sliceReducer(state[sliceName], action),
            };
        }

        // Otherwise, return the existing state unchanged
        return state;
    }
}

We've already seen that our createReducer utility takes a lookup table mapping action types to case handler reducer functions. Now, createConditionalSliceReducer takes the same lookup table, plus the name of a state slice to update. It uses createReducer to create a reducer that can handle a single slice, and returns a new reducer that checks incoming actions and only responds if any of the given case reducers know how to handle that action type.

Creating a Generic Entity Update Reducer 🔗︎

Looking at our Redux-ORM models, we can see that the basic process for updating a given model's values will be identical no matter what type of model it is. We need to create a Session instance, retrieve the right Model class for the item, look up the specific Model instance by ID, and then tell it to queue an update for the given fields. We can write a generic reducer to update any model instance by its type and ID.

We're going to create a new feature folder, features/entities/, and add the reducer logic there. We then need to create a top-level "feature reducer" function to execute that logic, and add the feature reducer to our root reducer.

Commit 1d5ccae6: Add a "entity feature reducer" and a generic "entity update reducer"

features/entities/entityReducer

import {ENTITY_UPDATE} from "./entityConstants";

import {createConditionalSliceReducer} from "common/utils/reducerUtils";

import schema from "app/schema";

export function updateEntity(state, payload) {
    const {itemType, itemID, newItemAttributes} = payload;

    const session = schema.from(state);
    const ModelClass = session[itemType];

    let newState = state;

    if(ModelClass.hasId(itemID)) {
        const modelInstance = ModelClass.withId(itemID);

        modelInstance.update(newItemAttributes);

        newState = session.reduce();
    }

    return newState;
}

const entityHandlers = {
    [ENTITY_UPDATE] : updateEntity,
};

const entityCrudFeatureReducer = createConditionalSliceReducer("entities", entityHandlers);

export default entityCrudFeatureReducer;

The basic updateEntity() function should be straightforward. We extract the itemType, itemID, and newItemAttributes fields from the action payload. The state parameter in this case should be just the entities slice of our root state. We create a Redux-ORM Session instance from our "tables", retrieve the right Model class by name, look up the specific Model instance by ID, and create a new state object with the updates applied.

From there, we create a lookup table for the action types this module knows how to handle, and use the createConditionalSliceReducer utility to generate a new reducer that will only respond to the actions in the lookup table, and ensure that only the entities slice is updated.

app/reducers/rootReducer.js

import mechsReducer from "features/mechs/mechsReducer";

+import entityCrudReducer from "features/entities/entityReducer";

// Omit combined reducer

const rootReducer = reduceReducers(
    combinedReducer,
+   entityCrudReducer,
);

We then add the top-level entity "feature reducer" as another argument to reduceReducers, and make sure that it's after the combined reducer. Now, we should be able to dispatch actions to update the contents of any one specific model instance in our state.

Editing Pilot Entries 🔗︎

Now that we have our generic entity update reducer in place, we can implement the ability to edit Pilot entries. We already set up the "start/stop editing" toggle last time, so we just need to hook up the event handlers and dispatch the right actions.

Hooking Up Pilot Inputs 🔗︎

We'll start with the Pilot's name field:

Commit 762a820: Hook up editing of pilot name field

features/pilots/PilotDetails/PilotDetails.jsx


+import {updateEntity} from "features/entities/entityActions";
+import {getValueFromEvent} from "common/utils/clientUtils";

const actions = {
    startEditingPilot,
    stopEditingPilot,
+   updateEntity,
}

export class PilotDetails  extends Component {
+   onNameChanged = (e) => {
+       const newValues = getValueFromEvent(e);
+       const {id} = this.props.pilot;
+
+       this.props.updateEntity("Pilot", id, newValues);
+   }

// Omit most rendering code

                <Form.Field
                    name="name"
                    label="Name"
                    width={16}
                    placeholder="Name"
                    value={name}
                    disabled={!canStopEditing}
+                   onChange={this.onNameChanged}
                    control="input"
                />

We import the updateEntity action creator we just wrote, and add it to the actions that will be bound up to auto-dispatch when called. We then add an onNameChanged handler, extract the new values from the change event, and dispatch updateEntity() by passing in the type of item ("Pilot") and the pilot's ID.

Let's try this out by editing one of the pilots. If we select the pilot named "Miklos Delius", click "Start Editing", and type "test" at the end of his name, we should see some actions dispatched:

And there we go! The dispatched action reached our entities feature reducer, and the updateEntity() reducer applied the updates to the right data object in the state. If we look at the screen, we should now see:

Because both the form and the list item are displaying the values from the same item in state, both of them have been updated. It's important to note that we are directly editing the values for this Pilot entry in our state. Much of the time, that behavior is not something we want. It's very likely that some parts of the application would still need to display the original data, at the same time that we are making edits to an item. We'll look at one way to handle the "draft/editing data" concept in the next post.

Next up is the "Rank" dropdown:

Commit af7e46e: Hook up Pilot "rank" dropdown

features/pilots/PilotDetails/PilotDetails.jsx


+   onRankChanged = (e, result) => {
+       const newValues = {rank : result.value};
+       const {id} = this.props.pilot;
+
+       this.props.updateEntity("Pilot", id, newValues);
+   }

// Omit rendering code

                <Form.Field
                    name="rank"
                    label="Rank"
                    width={16}
                    control={Dropdown}
                    fluid
                    selection
                    options={RANKS}
                    value={rank}
+                   onChange={this.onRankChanged}
                    disabled={!canStopEditing}
                />

Easy enough, and now we can promote Corporal Delius to be a Sergeant instead.

Using Generic Change Handlers 🔗︎

We've got three more fields that still need to be hooked up (we'll leave the "Mech" field alone for now). The "Age" field is another text input, and the "Gunnery" and "Piloting" fields are dropdowns. We could write three more individual change handlers for those three fields, but looking at the two we have already, things are pretty simple. In fact, onNameChanged doesn't actually refer to "name" anywhere specifically, and onRankChanged just references "rank" once as a key. We could turn those into a generic "text input" handler and a generic "SUI-React Dropdown" handler, and reuse them for all five inputs:

Commit dad0aa7: Use more generic onChange handlers for PilotDetails dropdowns and inputs

features/pilots/PilotDetails/PilotDetails.jsx

export class PilotDetails  extends Component {
-   onNameChanged = (e) => {
+   onInputChanged = (e) => {
        const newValues = getValueFromEvent(e);
        const {id} = this.props.pilot;

        this.props.updateEntity("Pilot", id, newValues);
    }

-   onRankChanged = (e, result) => {
-       const newValues = {rank : result.value};
+    onDropdownChanged = (e, result) => {
+       const {name, value} = result;
+       const newValues = { [name] : value};
        const {id} = this.props.pilot;

        this.props.updateEntity("Pilot", id, newValues);
    }

We then pass the appropriate change handler to each input field, and bam! All the inputs should now be editable:

Buffering Pilot Inputs 🔗︎

We've got one last improvement to make. The "Name" and "Age" fields are currently unbuffered, and dispatching an ENTITY_UPDATE action every time we type a key. We can use our <FormEditWrapper> component to buffer both of those inputs:

Commit bee1c96: Use FormEditWrapper to buffer changes from Pilot inputs

features/pilots/PilotDetails/PilotDetails.jsx

+import FormEditWrapper from "common/components/FormEditWrapper";

// Omit component and rendering code

+               <FormEditWrapper
+                   singleValue={true}
+                   value={ {name} }
+                   onChange={this.onInputChanged}
+                   passIsEditing={false}
+               >
                    <Form.Field
                        name="name"
                        label="Name"
                        width={16}
                        placeholder="Name"
-                    value={name}
                        disabled={!canStopEditing}
-                    onChange={this.onInputChanged}
                        control="input"
                    />
+               </FormEditWrapper>

+               <FormEditWrapper
+                   singleValue={true}
+                   value={ {age} }
+                   onChange={this.onInputChanged}
+                   passIsEditing={false}
+               >
                    <Form.Field
                        name="age"
                        width={6}
                        label="Age"
                        placeholder="Age"
                        control="input"
-                   value={age}
-                   onChange={this.onInputChanged}
                        disabled={!canStopEditing}
                    />
+                </FormEditWrapper>

Now, if we edit the name, we should only see a single ENTITY_UPDATE action get dispatched.

Final Thoughts 🔗︎

We're now making some significant progress. The application is starting to become usable, and we've used some fairly advanced capabilities and concepts in the process.

Next time, we'll deal with the issue of "draft/editing data" for forms, and see how to display the "current" values for an item at the same time that we edit the "work-in-progress" values for that same item.

Further Information 🔗︎


This is a post in the Practical Redux series. Other posts in this series: