Controller pattern

Originally published Sep 29, 2017·Tagged web-development, react, patterns

How do you trigger an action in a React component, from within a different React component? This question would be pretty easy to answer if it were asked of a Backbone app. ```js const view = new TweetsView({ user: '@ryaninvents' }); $button.on('click', () => view.refreshTweets()); ``` However, in React-land, it's not so obvious. ```js import Tweets from './Tweets'; function App() { return ( <div> <button /* onClick = ??? */>Refresh tweets</button> <Tweets user="@ryaninvents" /> </div> ); } ``` How can we link the button to the `Tweets` component, so that clicking the button will reload the tweets? It's possible to directly call a `refresh` method on the `Tweets` component directly. That's a bad idea, though; it's not "the React way" of doing things. One core tenet of React development is that your view should be a pure function of your application's state, and if your components can "reach into" each other to change things, you violate this assumption. So how can this be handled? One possible answer takes inspiration from the long-awaited proposal for aborting a `fetch` request. The API for an [abortable fetch](https://developers.google.com/web/updates/2017/09/abortable-fetch "Google Developers: Abortable Fetch") adds a new class called `AbortController`, which you can pass as a parameter to the `fetch()` function. For instance, here's an example of how you might abort a fetch if it takes longer than 5 seconds: ```js const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); fetch(url, { signal: controller.signal }) .then(response => response.text()) .then(text => { console.log(text); }); ``` There are three pieces that need to be implemented for you to do this: 1. You create and control the `AbortController`. 2. When you might want to abort something, you pass it the signal from your controller. 3. When you decide you do want to abort that request, you call `controller.abort()`. Let's take a cue from this and try to design a `RefreshController` class that covers these three user needs. ## Designing `RefreshController` To check item (1) off the list, all we need to do is create an empty class: ```js class RefreshController { } ``` Item (2) is a little trickier. We don't actually know what `signal` does, but it seems to be some sort of event emitter. We can use [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter "Node.js: 'EventEmitter'") from the Node.js [`events`](https://nodejs.org/api/events.html "Node.js: 'Events'") built-in module for this; Browserify and Webpack will polyfill this for us in the browser. ```js import EventEmitter from 'events'; class RefreshController { constructor() { this.signal = new EventEmitter(); } } ``` Now, for part (3), we just need to define the API. Since we're only going to ask the controller to perform a simple refresh, this is pretty easy: ```js import EventEmitter from 'events'; class RefreshController { constructor() { this.signal = new EventEmitter(); } refresh() { this.signal.emit('refresh'); } } ``` ## Redesigning the `Tweets` component Now, we just need to make our `Tweets` component understand what it means when we hand it a refresh signal. Let's say for the sake of discussion that the `Tweets` component looks like this: ```js import React from 'react'; import PropTypes from 'prop-types'; import TwitterApi from './TwitterApi'; class Tweets extends React.Component { static propTypes = { user: PropTypes.string.isRequired, }; state = { tweets: [], loading: false, }; refresh = () => { this.setState({loading: true}); // Assume that `loadTweetsForUser` just returns an array of strings // representing the most recent tweets. TwitterApi.loadTweetsForUser(this.props.user) .then((tweets) => { this.setState({tweets}); }); } render() { if (this.state.loading) { return <em>Loading, please wait...</em>; } return ( <ul> {this.state.tweets.map((tweet, i) => <li key={i}>{tweet}</li>)} </ul> ) } } ``` We'll need it to accept a new prop representing the refresh signal: ```js // ... static propTypes = { user: PropTypes.string.isRequired, refreshSignal: PropTypes.object.isRequired, }; // ... ``` And we'll also need it to respect that refresh signal. To make sure it's hooked up correctly, we'll need to use the React lifecycle methods. ```js // ... componentDidMount() { // When component mounts, start listening for the refresh signal. this.props.refreshSignal.addListener('refresh', this.refresh); } componentWillReceiveProps({refreshSignal}) { // If the refresh signal changes... if (refreshSignal !== this.props.refreshSignal) { // ...stop listening to the old one... this.props.refreshSignal.removeListener('refresh', this.refresh); // ...and start listening to the new one. refreshSignal.addListener('refresh', this.refresh); } } componentWillUnmount() { // Stop listening to the signal when this component is unmounted. this.props.refreshSignal.removeListener('refresh', this.refresh); } // ... ``` ## Using the new signal Now, we're all ready to use the controller in our `App` component: ```js import RefreshController from './RefreshController'; import Tweets from './Tweets'; function App() { const controller = new RefreshController(); return ( <div> <button onClick={() => controller.refresh()}>Refresh tweets</button> <Tweets user="@ryaninvents" refreshSignal={controller.signal} /> </div> ); } ``` When the user clicks the button, it emits a "refresh" event from the signal. The `Tweets` component, which has been listening to the signal, receives the event and reloads the tweets. This approach is flexible, since you can attach data to the events, and also emit multiple types of event if your application needs it. What use cases might you have for this approach?