Debouncing forms in React with Redux

Basic react form

Before we jump into debouncing and what it means I want to present you a simple react form. It looks like this:

Form
Form

I made this using awesome Tailwind CSS. The code for this form sits mainly in two components - App.js:

class App extends Component {
constructor(props) {
super(props);
this.state = { typedWords: [] };
}

handleChange = (event) => {
const { value } = event.target;
let typedWords = [...this.state.typedWords, value];
this.setState({ typedWords });
};
render() {
return (
<div className="flex flex-col items-center min-h-screen w-full bg-teal-lighter bg-repeat">
<div className="container md:max-w-sm md:mx-auto">
<h1 className="block w-full text-center text-grey-darkest mb-6">Debounce in React</h1>
<SearchInput handleChange={this.handleChange} />
</div>
{this.state.typedWords.map((word, key) => (
<SearchResult text={word} key={key} />
))}
</div>
);
}
}

and SearchInput:

class SearchInput extends Component {
render() {
const { handleChange } = this.props;
return (
<form className="mb-4" onChange={handleChange}>
<div className="flex flex-col mb-4 md:w-full">
<label
className="mb-2 uppercase font-bold text-lg text-grey-darkest"
htmlFor="search-input"
>

Search input:
</label>
<input className="field" name="search" type="text" id="search" />
</div>
</form>
);
}
}

How it works

In my App component I define a handleChange function which then will be used inside SearchInput as a callback. In handleChange, I extract typed character from html input. Then I make a copy of state and insert a new value from SearchInput component.

SearchInput is representing html form so I treat it as a representational component.

You may notice another component - SearchResult which looks like this:

function SearchResult(props) {
const { text } = props;
return (
<div className="container md:max-w-sm md:mx-auto">
<span>{text}</span>
</div>
);
}

it is still only representing html.

I have my components working so let's type something into a search input:

Form without debouncing from Noaal on Vimeo.

Whoa! What is happening here?

onChange event handler fired up every time I typed something into an input. That's not exactly what I wanted - I want my handler to capture only full typed words. How to do it?

What is debounce

As you saw in a previous blog post my handleChange event is firing up every time I type the letter. I don't want that. I want it to be called when a user stops typing. One way of doing this will be using debounce.

Debounce is limiting a rate which given function will be called. Thanks to that I can tell my app to run handleChange every 250ms. It is very useful when we have event handlers that are attached to the e.g scroll of change events.

Debounce in react

I will be using lodash.debounce as it is widely used and battle-tested library.

My App component will look like this after a change:

class App extends Component {
constructor() {
super();
this.state = { typedWords: [] };

this.emitChangeDebounced = debounce(this.emitChange, 250);
}

componentWillUnmount() {
this.emitChangeDebounced.cancel();
}

handleChange = (event) => {
this.emitChangeDebounced(event.target.value);
};

emitChange = (value) => {
if (value !== "") {
let typedWords = [...this.state.typedWords, value];
this.setState({ typedWords });
}
};
// render method here
}

Let's start here with handleChange. Right now it calls emitChangeDebounced. This emit is debounced function that lodash will fire every 250ms after the user changes the input. My main logic lays inside emitChange where I set my state based on a value from the event. You may ask why do you pass event.target.value instead of the whole event?

It is because of how React works. In React all events are wrapped into SyntheticEvent. This event is reused by all events inside react. To let garbage collector take it after debounce has ended I have to either provide only value to my function or call event.persist() to have my event persisted.

With event.persist() my handleChange event will look like this:

handleChange = (event) => {
event.persist();
this.emitChangeDebounced(event);
};

If I wanted to pass entire event without persist I will get an error:

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property `type` on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().

Testing debounce

Ok, I have my component working but how to test it? This is one of the solutions - in my App.test.js I have the following test:

it("should set state when input has changed", () => {
const wrapper = mount(<App />);
const searchInputWrapper = wrapper.find("#search");
searchInputWrapper.simulate("change", {
target: { value: "Fake Name" },
});

setTimeout(() => {
expect(wrapper.state().typedWords).toEqual(["Fake Name"]);
}, 200);
});

The first few lines are component setup using enzyme. Right after that, I simulate change event on my search input. Then I use setTimeout to wait with the assertion - I will be executed when debounce stops.

Adding redux

It may seem like overkill for this simple example but I decided to add redux to show how this debounced form can be used in the more realistic scenario.

So, after adding redux and react-redux to my application I started by creating actions & actions creators under src/actions/index.js:

export const ADD_WORD = "ADD_WORD";

export const addWord = (word) => ({
type: ADD_WORD,
word,
});

To explain how those two can be used I added a small test in 'actions.test.js`:

import { ADD_WORD, addWord } from "./index";

describe("Actions", () => {
it("should create action to add word", () => {
const expectedAction = {
type: ADD_WORD,
word: "fake",
};

expect(addWord("fake")).toEqual(expectedAction);
});
});

As you can see calling addWord with some string should dispatch action with ADD_WORD type and typed a word.

Next step was to add src/reducers/index.js:

import { combineReducers } from "redux";

import { ADD_WORD } from "../actions/index";

export const words = (state = [], action) => {
switch (action.type) {
case ADD_WORD:
return [action.word, ...state];
default:
return state;
}
};

const rootReducer = combineReducers({ words });
export default rootReducer;

Where I have my pure function words which is getting its own piece of state to work with. In this case, I want my typed words to be first in a state of my application. I also added tests:

import { words } from "./index";
import { ADD_WORD } from "../actions/index";

describe("Words reducer", () => {
it("should return initial state", () => {
expect(words(undefined, {})).toEqual([]);
});

it("should handle ADD_WORD on initial state", () => {
expect(words([], { type: ADD_WORD, word: "tom" })).toEqual(["tom"]);
});

it("should handle ADD_WORD on existing state", () => {
expect(words(["tim"], { type: ADD_WORD, word: "tom" })).toEqual(["tom", "tim"]);
});
});

The last thing is to set up my store and connect it with react application. The first step is happening in store.js:

import { createStore, compose } from "redux";

import rootReducer from "./reducers";

const store = createStore(
rootReducer,
compose(window.devToolsExtension ? window.devToolsExtension() : (f) => f)
);

export default store;

I create here my store with rootReducer which in this case is only words reducer and I also added redux dev tools which help me debug my redux code.

The second step is to modify my index.js so redux can be injected into my application:

import { Provider } from "react-redux";
import AppContainer from "./components/App/AppContainer";
import store from "./store";

const root = (
<Provider store={store}>
<AppContainer />
</Provider>
);
ReactDOM.render(root, document.getElementById("root"));

Using redux with react applications

You may notice that Provider component is wrapping a new one - AppContainer. This is a nice pattern to use when using redux & react applications. It boils down to two concepts: component and container. A component is responsible only for rendering html. A container is a way to get your data from the redux store.

That's why I created AppContainer:

import React from "react";
import { connect } from "react-redux";

import { addWord } from "../../actions/index";

import App from "./App";

export const AppContainer = (props) => <App addWord={props.addWord} words={props.words} />;

const mapDispatchToProps = (dispatch) => ({
addWord: (word) => dispatch(addWord(word)),
});

const mapStateToProps = (state) => ({
words: state.words,
});

export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);

Here I added two typical functions for react applications with redux - mapDispatchToProps & mapStateToProps. In the first one, I tell redux that when I call addWord inside my App component it should dispatch an action from actions/index. The second function is for extracting the data from the store - it will be the best if my component has only access to this part of a state which it is concerned about.

That's all for today! To recap: I've added redux to my application and used Presentational and Container Components and I have my debounced input with react & redux!

Github repo can be found here.