SlackMoji, Debouncing and React Event Pooling

Posted by Niky Morgan on September 9, 2018

The Flatiron staff are frequent users of Bitmoji. We use it on email, GitHub and especially on Slack. One of the difficulties of using Bitmoji on Slack is knowing which commands are connected to Bitmoji comics. We have at times found new and unexpected commands using outside sources like this, but the comics update frequently so it is hard to stay current.

This weekend I found a repo that listed some information about the Bitmoji API. Using that information I was able to find the current Bitmoji commands for Slack at this endpoint. (There are thousands of results, so expect a slow page load if you visit that URL). The API has top level keys of imoji and friends which contain arrays of comics (and their command tags) which support solo and group Bitmojis respectively. There is other data available at that endpoint, but I can create an application for my coworkers to search and preview Slack Bitmoji commands using the imoji and friends data.

The src key for each comic entry has a URL for the comic image, but it is a template URL. In order to use it, you must replace the %s with a Bitmoji id. As one might expect, for the comics that support multiple avatars, there are two occurrences of %s in the URL. I found my Bitmoji id by using the Chrome Extension. I copied the image address for multiple Bitmoji comics featuring my avatar and looked for a common set of characters in the URLs. I isolated a 32-character string which reproduces my avatar when inserted into the comic URLs.

The natural question is, is this string a unique identifier for me as a user or does it encode information about the set of options that constitute my avatar? As an experiment I gave my avatar purple hair and found that the previous string was no longer in my image URLs. This also means that comics at the URLs including the previous string were unchanged and did not sport my avatar’s new purple ‘do.

With this knowledge I was able to ideate an MVP app. I would fetch data from the endpoint and for each result, insert my avatar into the comic and render it to the page. Unfortunately when I started to build I found the API does not support cross-origin requests. This means that using JavaScript, I wouldn’t be able to fetch data from the API via another webpage. Once way around CORS issues is to create a backend which makes the request. Browsers restrict JavaScript requests for security reasons, but the request can be made outside of the browser (like from a remote server).

Since I wasn’t persisting any information in a database, Rails seemed like too powerful a tool for this job. I initially created a Sinatra application to request the data from Bitmoji in Ruby. When my React application mounted, it would request information from the Sinatra app which would request information from the Bitmoji API. The API returned duplicate comics, so I filtered this data by comic_id to avoid duplicates and then served it from my Sinatra app to the React app.

Even filtered there were thousands of results. I used a radio button to toggle between comics for solo avatars and comics for multiple avatars. I also created a searchbar which would search through the multiple tags for each comic and return partial matches. Following React convention, this search input was a controlled component. That means that every time the input field changed value (when a new character was typed or deleted), this would update the state of the application. However, I did not want each state update to trigger a new search of the comics.

Let’s look at the example of searching for skywriter (which has two results). If the app searched for every change to the search input, it would run nine searches (one for each letter of skywriter). Each of the 2,000 records has somewhere between two and ten text tags which could potentially match the search term. If say there are an average of five tags per record, that means each search is running 10,000 string comparisons. Doing comparisons for each of the nine letters makes close to 100,000 comparisons. This made for a noticeable lag in the application. Nine re-renders and searching 100,000 times takes longer than the few seconds it takes me to type skywriter.

My solution was to debounce the state changes so that there were fewer state changes triggering the filter and rerender. I gave my search input a local state to control the input value, and then had a state in a parent container that would trigger the refiltering of all the records on change. My local state needed to update on every input change, but my container search state could potentially skip some of the updates. If a user typed skywriter fast enough, they might in fact only need the container state to update at the very end. The app only needs to search on the most up-to-date information.

I wrote a higher order debounce function which accepts a callback function and a debounce time in milliseconds. (After experimenting I found that a debounce time of 500 milliseconds provided me with the best user experience.) The debounce function creates a closure and returns a wrapped version of the callback function which has reference to a timeout variable in a higher scope. The closure encapsulates this timeout variable so that our debounced function can read it and update it every time it is executed.

Executing this debounced function resets the timeout variable to a new timeout it creates to execute the callback in 500 ms. If we repeatedly execute the debounced function quickly enough (in under 500 ms), it will keep deferring the execution of the callback function by clearing the old timeout and creating a new one.

When the search input changes, it updates the local state and executes a debounced function to update the parent container state. If the user types quickly enough, the previous timeouts keep getting removed and the callback does not execute. The callback to update the parent container state only executes if the timeout does not get cleared in 500 ms (if 500 ms pass between executions of the debounced function).

// debounce function
const debounce = (func, time) => {
	let timeout
	return function() {
		const later = () => {
      timeout = null
			func.apply(null, args)
		}
		clearTimeout(timeout)
		timeout = setTimeout(later, time)
	}
}

// callback function
changeDisplay = ({target: {name, value}}) => {
    this.setState({[name]: value})
  }

Debouncing the update of the parent container state meant my app had to filter much less frequently and therefore more quickly. While holding similar state (of a search term) in multiple components is not usually best practice, in this case it makes sense because that state is functioning differently in each component. In the child search component, having a local state for the search term is good React practice. It allows React to control the value of the input element and use React data flow instead of relying on the DOM to store the value. Likewise keeping a debounced version of that search state in the parent component also follows proper React data flow. In the parent, that search term truly reflects the state of the component because it stores information about what data needs to be rendered.

There was one more slight complication with the debouncing. As you can see, the callback takes an event and pulls the name and value of the target to dynamically update state in the parent component. It is a dynamic function so that I can use it as a change-handler for both my radio buttons and my search input. The radio buttons use the undebounced changeDisplay function because they don’t trigger the same amount of filtering. However, the search input calls the debounced version of changeDisplay on change (after updating local state).

If you have worked with React’s synthetic events before, you know that they are reused. In order to maintain consistency across browsers, React wraps native browser events with their own event. These synthetic events are reused for performance reasons, which means their properties must be used quickly before they are nullified. If the events are used asynchronously, they will not keep reference to the previous event data. By the time our timeout finishes and our callback is executed (500 ms after the event occurs), the event data is gone.

There are two ways to keep event data. One is to save references to the properties as variables. Just declare a constant with the target, name or value. Once you have saved that information to a variable, you can use it later. It is also possible to prevent an event from being wiped clean and returning to the React pool by calling the persist method on it. I usually prefer the first solution because I would rather let React manage events how it wants to manage them. However, in this case I had a changeDisplay function that was being passed as a reference to an onChange in a functional component, I wanted my changeDisplay to be set up to accept an event as an argument. This made my functional component look much cleaner than if I had written a separate function to save references to the target and name, just to pass it in later. This meant I needed to persist the event produced when my search input was changed so it could be used after the timeout.

// search input
export default class SearchField extends Component {
  state = {search: ''}

  handleChange = (e) => {
    // telling React not to return this event to the pool
    e.persist()
    const search = e.target.value
    this.setState({search})
    // the debounced callback was passed to this component as the below prop
    this.props.changeDisplay(e)
  }

  render() {
    return(
      <input
        type='text'
        name='search'
        onChange={this.handleChange}
        placeholder='Search for keywords'
      />
    )
  }
}

The result is a fun app which my coworkers and I can use regularly to send each other ‘mojis. To learn how I used an AWS to serve data for this app, read my blog Using an AWS Lambda as a Simple Server.

Slackmoji