CHAPTER 6

image

Architecting React Applications with Flux

As you saw earlier, one core philosophy of React is that data flows in a single direction, from parent to child components as props. When a parent component needs its children to reach back, it can pass callback functions down as props as well.

This one-way data flow leads to clear, explicit, and easy-to-follow code. You can trace a React application from start to finish and see what code is executed on changes.

But while this architectural pattern has many advantages, it also brings some challenges. React applications usually grow to have many nesting levels where top components act as containers and many pure components are like leafs on an interface tree. With state living on the top levels the hierarchy, you end up creating callbacks that needs to get passed down as props, sometimes many levels deep in a repetitive and error-prone task.

Ryan Florence, React Router co-author and prominent community member, uses an analogy to describe the act of passing data and callbacks as props many levels deep: drilling your application. If you have many nested components, you have a lot of drill work going on, and if you want to refactor (move some components around), you must do a whole lot of drilling all over again.

Let me be clear here: using nested React components is a great way to structure UIs. It reduces complexity and leads to separation of concerns, and to code that is easier to extend and maintain. And since React is built around the concept of reactive rendering, for every change on the component’s state or props, React updates the DOM (using its virtual DOM implementation to calculate the minimum necessary mutations). You get a very simple mindset for developing and great performance for free.

What we’re trying to address here is, given the fact that you want to have nested components, how do you bring data and, most importantly, the callbacks to manipulate that data closer to each of these components when the applications grow? That’s exactly where Flux comes in.

What Is Flux?

Flux is an architectural guideline for building web applications. It was created by Facebook, and while it’s not part of React, nor was it built exclusively for React, it pairs exceptionally well with the library.

The main point of Flux is to allow an uni-directional data flow in your application. It is composed of basically three pieces: actions, stores, and a dispatcher. Let’s take a look at these three pieces.

Stores

As mentioned, one of the main points you are trying to address is how to bring data closer to each of the application’s components. Our ideal view of the world looks like Figure 6-1. Data is completely separated from the component, but you want the component to be notified when data changes so it can re-render.

9781484212615_Fig06-01.jpg

Figure 6-1. An ideal view of the world

That’s exactly what stores do. Stores hold all the application state (including data and even UI state) and dispatch events when the state has changed. Views (React components) subscribe to the stores that contain the data they needs and when the data changes, re-render themselves, as shown in Figure 6-2.

9781484212615_Fig06-02.jpg

Figure 6-2. Views re-render themselves

One important characteristic of stores is that they are closed black boxes; they expose public getters to access data, but nobody can insert, update, or change their data in any way, not the views nor any of the other Flux pieces. Only the store itself can mutate its own data.

If you know the MVC paradigm, stores do bear resemblance with models, but again the main difference is that stores only have getters; nobody can set a value on a store.

But if no other part of the application can change the data in a store, what in the system causes stores to update their state? The answer is Actions.

Actions

Actions are loosely defined as “things that happen in your app.” They are created by almost any part of the application, basically from user interactions (such as clicking on a button, leaving a comment, requesting search results, and so on…), but also as results of AJAX requests, timers, web socket events, etc.

Every action contains a type (its unique name) and an option payload. When dispatched, actions reach the stores, and that’s how a store knows it needs to update its own data. See Figure 6-3.

9781484212615_Fig06-03.jpg

Figure 6-3. Store updates its own data

Actually, that’s pretty much all that is to Flux: React components create actions (say, after user types a name in a text field); that action reach the stores; stores that are interested in that particular action update their own data and dispatch change events; finally, the view responds to that store’s event by re-rendering with the latest data. But there’s a missing piece in this diagram, the dispatcher.

Dispatcher

The dispatcher is responsible for coordinating the relaying of the actions to the stores and ensuring that the stores’ action handlers are executed in the correct order. See Figure 6-4.

9781484212615_Fig06-04.jpg

Figure 6-4. Dispatcher workflow

Despite being an essential piece of Flux architecture, you don’t have to think too much about dispatchers. All you need to do is to create an instance to use; the rest is handled by the given dispatcher implementation.

The Unrealistic, Minimal Flux App

When used in complex applications, Flux helps keep the code easier to read, maintain, and grow. It certainly reduces complexity, and as a consequence in many cases it also reduces the number of lines of code in the project (although that’s not an appropriate metric for code complexity). But no such thing will happen in your first sample project, where the use of Flux will actually increase the amount of code necessary to build it. This is because the purpose of this first example is to help you grasp all of the elements in a Flux application. Flux can be tricky to newcomers, so you will start on a very basic project, with almost no UI (or practical use, for that matter) but one that designed to make explicit and well-defined use of all of the elements in a Flux + React application. In the next section, you will move to complete, real-world examples.

The Bank Account Application

The analogy of using a bank account to describe Flux’s actions and stores was first made by Jeremy Morrell in his presentation “Those who forget the past are doomed to debug it” (https://speakerdeck.com/jmorrell/jsconf-uy-flux-those-who-forget-the-past-dot-dot-dot-1), and is used here with his permission.

A bank account is defined by two things: a transaction and a balance. With every transaction, you update the balance, as shown in Tables 6-1 and 6-2.

Table 6-1. The First Transaction Initiates the Balance

Transaction

Amount

Balance

Create Account

$0

$0

Table 6-2. With Every Transaction, You Update the Balance

Transaction

Amount

Balance

Create Account

$0

$0

Deposit

$200

$200

Withdrawal

($50)

$150

Deposit

$100

$250

  

$250

These transactions are how you interact with your bank; they modify the state of your account.

You will recreate this process in a Flux application. In Flux terms, the transactions on the left are your actions, and the balance on the right is a value that you will track in a store. Your sample application structure will include

  • A constants.js file (since all actions should have uniquely identifiable names across the app, you will store these names as constants).
  • The standard AppDispatcher.js.
  • BankActions.js, which will contain three action creators: CreateAccount, depositIntoAccount, and WithdrawFromAccount. We call the methods “action creators” because the actions are really just objects. The methods that create and dispatch these actions, for lack of a better name, are called the action creators.
  • BankBalanceStore.js, which will keep track of the user’s balance.
  • Finally, the App.js file, which contains the single UI component you will use in this project.

Start by creating a new project and installing the Flux library with npm (npm install --save flux). The next sections will walk through each file in the project.

The Application’s Constants

Let’s get started by defining the constants file. You need three constants to uniquely identify your actions across the app for creating an account, depositing in the account, and withdrawing from the account. Listing 6-1 shows the code.

The Dispatcher

Next, let’s define your application dispatcher. As said earlier, you don’t have to think too much about it. For all that matters, your AppDispatcher file could be as simple as just instantiating a Flux dispatcher.

import {Dispatcher} from ’flux’;
export default new Dispatcher();

However, you do have the opportunity to extend the standard dispatcher in your application, and one thing that will help you better comprehend the dispatcher role is making it log every action that gets dispatched, as shown in Listing 6-2.

Action Creators

Moving on in your fake banking application, let’s define some functions that will generate actions in the application. Remember, an action in the context of a Flux application is just an object that contains a type and optional data payload. For lack of a better term, we call the functions that define and dispatch actions as action creators. You create a single JavaScript file with three action creators (creating an account, depositing, and withdrawing), as shown in Listing 6-3.

Store

In the sequence, let’s define your BankBalanceStore file. In a Flux application, the store owns state and registers itself with the dispatcher. Every time an action gets dispatched, all the stores are invoked and can decide if they care for that specific action; if one cares, it changes its internal state and emits an event so that the views can get notified that the store has changed.

To emit events, you need an event emitter package from npm. Node.js that has a default event emitter, but it is not supported on browsers. There are many different packages in npm that reimplement a node’s event system on the browser; even Facebook has an open source one, which is a simple implementation that prioritizes speed and simplicity. Let’s use it: npm install --save fbemitter

Starting with a basic skeleton for your BankAccountStore, you create an event emitter instance and provide an addListener method to subscribe to the store change event. You also import the application dispatcher and register the store, providing a callback that is invoked for every dispatched action. The code is shown in Listing 6-4.

Notice in the code that you invoked the dispatcher’s register method, passing a callback function. This function is called every time a dispatch occurs, and you have the opportunity to decide whether the store does something when certain action types are dispatched.

Additionally, the dispatcher’s register method returns a dispatch token: an identifier that can be used to coordinate the store’s update order, which you will see later on this chapter.

In the sequence, you need to do two more things: create a variable to store the account balance (as well as a getter method to access its value) and make the actual switch statements to respond to the actions CREATE_ACCOUNT, DEPOSIT_INTO_ACCOUNT, and WITHDRAW_FROM_ACCOUNT. Notice that you need to manually emit a change event after changing the internal value of the account balance. The complete code is shown in Listing 6-5.

UI Components

Finally, you need some UI. Your App.js file will import both the store and the actions. It will display the balance that is controlled by the store and call the action creators when the user clicks the withdraw or deposit buttons.

Let’s approach this in parts, starting with the store. As shown in Listing 6-6, in the class constructor you define the local state containing a balance key. The value for this key comes from the BankBalanceStore (BankBalanceStore.getState()). In the sequence, you use the lifecycle methods componentDidMount and componentWillUnmount to manage listening for changes in the BankBalanceStore. Whenever the store changes, the handleStoreChange method is called and the component’s state gets updated (and, as you already know, as the state changes, the component will re-render itself).

In the sequence, let’s implement the render function. It takes a text field and two buttons (withdraw and deposit). You also have two local methods to handle the click of those buttons. The methods simply call the action creators and clear the text field. The complete code for App.js is shown in Listing 6-7, and a complementary CSS file for basic styling is shown in Listing 6-8.

If you are following along, now’s a good time to try the withdraw and deposit operations. Make sure you have the browser console open so you can see the all the actions logged by the dispatcher, as shown in Figure 6-5.

9781484212615_Fig06-05.jpg

Figure 6-5. The fake banking app with actions logged

Flux Utils

Since version 2.1, the Flux library includes base classes for defining stores, as well as a higher order function to use with a container component so it can update its state automatically when relevant stores change. These utilities are valuable because they help reduce the boilerplate in your application.

Flux Utils Stores

The Flux Utils package provides three base classes to implement stores: Store, ReduceStore, and MapStore.

  • Store is the simplest one. It is just a small wrapper around a basic store. It helps in coping with boilerplate code but doesn’t introduce any concepts or new functionalities.
  • ReduceStore is a very special kind of store. Its name comes from the fact that it uses reducing functions to modify its internal state. Reducer is a function that calculates a new state given the previous state and an action, similar to how Array.prototype.reduce works. The state in a ReduceStore must necessarily be treated as immutable, so be careful to only store immutable structures or any of the following:
    • Single primitive values (a string, Boolean value, or a number)
    • An array or primitive values, as in [1,2,3,4]
    • An object of primitive values, as in {name:’cassio’, age:35}
    • An object with nested objects that will be manipulated using React immutable helpers
  • MapStore is a variation of ReduceStore with additional helper methods to store key value pairs instead of a single value.

Another neat characteristic of ReduceStore (and consequentially MapStore) is that you don’t need to manually emit change events: the state is compared before and after each dispatch and changes are emitted automatically.

Let’s use the BankBalanceStore from the previous example to exemplify the Flux Util base stores. For comparison purposes, you will first take a look at how the exact same result can be achieved using Flux Util’s Store. In the sequence, you will make a much slimmer version using ReduceStore.

Starting with the base Store, the implementation is actually almost identical to your current BankBalanceStore, with two main differences:

  • You don’t need to create your own instance of an event emitter.
  • You don’t need to manually register the store with the dispatcher; instead, you create an instance of the store passing the dispatcher as an argument.

This results in a file slightly leaner than your original one, as shown in Listing 6-9.

But the implementation really shines using ReduceStore instead of using the regular Store. Besides being cleaner, its functional roots coupled with the use of an immutable data structure allows for a more declarative programming (just like React) and impacts positively in many other areas (like testing, for example).

Let’s make yet another implementation of your BankBalanceStore, this time using ReduceStore. To extend ReduceStore, your class needs to implement two methods: getInitialState and reduce. In getInitialState you define the initial state of your store, and in reduce you modify this state as result of actions. A default getState method is already defined, so you don’t need to override unless you don’t want to treat the ReduceStore state as immutable (which defeats the purpose of using a ReduceStore in the first place, so for practical purposes you will always treat as immutable).

Listing 6-10 shows the complete code for the BankBalanceStore extending ReduceStore. Notice that there’s no need to emit change events; they are automatically dispatched for you.

Container Component Higher Order Function

You learned about container components in Chapter 3. They are used to separate business logic non-related to UI rendering (such as data fetching) from its corresponding sub-component. By default, containers are pure, meaning they will not re-render when their state does not change.

Image Tip  One note of caution: to use the Flux Util’s higher order function, the container component cannot access any props. This is both for performance reasons, and to ensure that containers are reusable and that props do not have to be threaded throughout a component tree.

Let’s see this in practice. You will change the app component to automatically subscribe to the BankAccountStore and update its state whenever it changes. You start by removing what won’t be needed anymore: you won’t need to declare the component’s initial state on the constructor. The componentDidMount and componentWillUnmount lifecycle methods can also be removed because the higher order component will take care of subscribing to and unsubscribing from the stores for you. For the same reason, you get rid of the handleStoreChange method. To use this higher order function, your container component must implement two class methods: calculateState (which maps store state to local component’s state) and getStores (which returns an array with all the stores the component listens to). Mind that the container higher order function only works with stores that extend Flux Util’s Stores. Listing 6-11 shows the updated App.js component, which is now 15% smaller than the original.

Asynchronous Flux

In any decently complex JavaScript web application, you’ll likely need to deal with asynchronicity. This can come in basically two forms: from coordinating update order between stores to asynchronous data fetching.

waitFor: Coordinating Store Update Order

In big Flux projects dealing with multiple stores, you may come to a situation where one store depends on data from another store. The Flux dispatcher provides a method called waitFor() to manage this kind of dependency; it makes the store wait for the callbacks from the specified stores to be invoked before continuing execution.

Using your fake bank application, let’s say you have a rewards program that is based on the user’s current balance. You could create a new BankRewardsStore to handle the current user’s tier on the program, and since the program is solely based on the user balance, for every operation the BankRewardsStore must wait for the BankBalanceStore to finish updating, and then update itself accordingly. Listing 6-12 shows the finished BankRewardsStore.

The BankRewardsStore responds to both DEPOSIT_INTO_ACCOUNT and WITHDRAW_FROM_ACCOUNT action types in the same way: by getting the current balance and simply assigning a tier depending on the balance amount (Basic tier for a balance amount lower than $5,000; Silver tier for a balance amount between $5,000 and $10,000; Gold tier between $10,000 and $50,000; and Platinum tier for a balance amount bigger than $50,000).

Now you can subscribe to this store on your main component and show the user’s current tier on the Rewards program. Listing 6-13 shows the updated App.js and Figure 6-6 shows the how the application looks with the update.

9781484212615_Fig06-06.jpg

Figure 6-6. The updated fake bank account with a fake rewards program

Asynchronous Data Fetching

As you’ve seen so far, general usage within Flux is straightforward, but the one thing that is not exactly intuitive is where to handle asynchronous requests. Where should you fetch data? How do you make the response go through the Flux data flow?

Although the library does not enforce a specific place to make fetch operations, a best practice that emerged from the community is to create a separate module to wrap all your requests and API calls (a file such as APIutils.js). The API utils can be called from anywhere, but they always make the async requests and then talk to the action creators to dispatch actions (so any store can choose to act on them).

Remember that when you call an asynchronous API, there are two crucial moments in time: the moment you start the call, and the moment when you receive an answer (or a timeout). For that reason, the API utility module will always dispatch at least three different kinds of actions: an action informing the stores that the request began, an action informing the stores that the request finished successfully, and an action informing the stores that the request failed.

Having a separate module wrapping the communication with the API and dispatching different actions in time offers many advantages because it isolates the rest of the system from the asynchronous execution. As soon as the actions get dispatched, the code is executed in a synchronous fashion from the point of view of the stores and the components, and this makes it easier to reason about them.

Let’s exemplify this by creating a new application: a site for airline tickets.

AirCheap Application

The application will fetch a list of airports as soon as it loads, and when the user fills the origin and destination airports, the application will talk to an API to fetch airline ticket prices. Figure 6-7 shows the working application.

9781484212615_Fig06-07.jpg

Figure 6-7. The AirCheap tickets app

Setup: Project Organization and Basic Files

To start the project in an organized way, you’re going to create folders for Flux-related files (action creators, stores, and an API folder for API utility modules) and a folder for React components. The initial project structure will look like Figure 6-8.

9781484212615_Fig06-08.jpg

Figure 6-8. The app folder structure for the AirCheap project

Let’s start creating the project files, beginning with the AppDispatcher. Remember, the AppDispatcher can be simply an instance of the Flux dispatcher, as shown in Listing 6-14. Some developers prefer to create a dispatchers folder, but since a Flux application will always have a single dispatcher, you will just save the AppDispatcher.js file in the root level of the app folder.

Of course, you could extend the dispatcher functionality. If you want to log all dispatcher actions, for example, just overwrite the dispatch method as you did in the Fake Bank account app.

Next, let’s create your constants.js file. When developing an application in a real-world scenario, you would probably start the constants.js file with just a few constants and increase them as needed, but in your case you already know beforehand all the constants you want to use:

  • FETCH_AIRPORTS to name the action you dispatch as the application starts to fetch all the airports. And since this is an async operation, you also create the FETCH_AIRPORTS_SUCCESS and FETCH_AIRPORTS_ERROR constants to represent the success and error on the operation.
  • CHOOSE_AIRPORT to name a synchronous action of the user selection an airport (as both origin OR destination).
  • The FETCH_TICKETS constant to name the action that you dispatch when both an origin and a destination are selected. This is an asynchronous data fetching operation, so you also need constants to represent success and, eventually, an error on the fetch operation: FETCH_TICKETS_SUCCESS and FETCH_TICKETS_ERROR.

Listing 6-15 shows the final constants.js file.

Creating the API Helper and ActionCreators for Fetching Airports

Let’s create an API helper to deal with the airport and ticket fetching. As discussed earlier, creating a segregated helper module to interact with the API will help keep the actions clean and minimal. For the sake of simplicity, in the case of this sample application, the API helper method will load a static json file in your public folder containing a list of the biggest airports in the world instead of using an actual remote API. You can download the airports.json file and the other public assets for this project from the Apress site (www.apress.com) or from this book’s GitHub page (http://pro-react.github.io). In any case, a trimmed down version of the airports.json file is shown in Listing 6-16.

In sequence, let’s create the api/AirCheapAPI.js file. It contains a function called fetchAirports that loads the airports from the remote json file and calls the actioncreators to dispatch a success or error action. Listing 6-17 shows a first draft of the file.

Image Note  As in earlier examples, you’re using the native fetch function to load the json file and importing the whatwg-fetch npm module that provides support for fetch in older browsers. Don’t forget to install it with npm install --save whatwg-fetch.

When the API module is called, it will fetch the remote data and success or errors actions itself by talking to the action creators. You don’t have the AirportActionCreators yet, but assuming it will contain fetchAirportsSuccess and fetchAirportsError functions, you can complete the AirCheapAPI implementation, as shown in Listing 6-18.

Moving to the AirportActionCreators, remember that actions are like messages that get dispatched through all stores: they just communicate what happened to the app. There is no place for business logic or computations on an action. With this knowledge, developing an ActionCreator module is pretty straightforward. Listing 6-19 shows the AirportActionCreators file.

AirportStore

The airport store could act on all the possible dispatched actions. It could act on fetchAirports to set a variable indicating that it is currently loading. It could act on fetchAirportsError to set a variable with an appropriate error message and, obviously, it could act on fetchAirportsSuccess to set its internal state to the list of fetched airports.

Let’s get started by doing the absolutely minimum: you create the AirportStore.js, inheriting from ReduceStore. Its state will contain the list of airports, starting as an empty array and getting populated as the store acts on fetchAirportsSuccess action. Listing 6-20 shows the complete source code.

App Component

Next, let’s implement the interface for the AirCheap Application. The user will interact with the application by filling two text fields (Origin and Destination), and to make things easier for the user, you implement an auto-suggest feature that suggests airports as the user types, as shown in Figure 6-9.

9781484212615_Fig06-09.jpg

Figure 6-9. Component with auto suggestions

There are many auto-suggestion libraries available (as a quick search on npmjs.com reveals). In this example, you use react-auto-suggest, so be sure to install it using NPM (npm install –save react-auto-suggest).

You start by creating a basic structure for your App component. It uses the Flux Util’s Container (to listen for store changes and map the stores state to the local component state) and invokes the AirportActionCreator on the lifecycle method componentDidMount to trigger the async loading of the airports. Listing 6-21 shows the basic structure of App.js.

If you run this application now, the react-auto-suggest library will throw an error. It expects a suggestions function to be passed as props. This function gets called every time the user changes the input value of the text field and should return a list of suggestions to be displayed. The function is shown in Listing 6-22.

The function receives two parameters: the text inputted by the user and a callback function to call with the suggestions.

In the first lines of the function, you clean up the user input by removing trailing spaces and transforming everything to lowercase. In the following line, you create a regular expression with the escaped user input. This regular expression is then used to filter the list of airports (based on the city name).

Besides filtering the airports, you do three other transformations. You sort the airports so that the occurrences where the word is matched at the beginning appear first, you limit the results to a maximum of seven, and you map the output to a specific format of “city name – country initials (airport code).”

The updated code for the App component with the suggestion function passed as props to the Autosuggest components is shown in Listing 6-23. A matching CSS file with the application style is shown in Listing 6-24.

If you test the application right now, you should see the autosuggest fields in action: just start typing a few letters. But the application is not finished. After choosing an origin and a destination, nothing else happens. In the next section, let’s start implementing the ticket loading.

Finishing the AirCheap application: Loading Tickets

You’re fetching the airport data asynchronously as soon as the app component mounts, but there’s one more fetch to be done: you need to fetch the actual ticket list when the user chooses the desired origin and destination.

The process is very similar to what you did for fetching airports. You put all the code that handles the actual data fetching in an API helper module. You create action creators to signal the data-fetching steps (loading initiated, loaded data successfully, or error in loading) and make a new store to keep the loaded tickets in its state. The App component is connected to the store and shows the loaded tickets data.

API Helper

For the sake of simplicity, instead of using a real API to return a list of flights and tickets, you load them from a static json file (flights.json). Obviously this means that whichever airports the user chooses, the loaded tickets will always be the same, but since your focus is on learning the Flux architecture, this will suffice. The flights.json file is shown in Listing 6-25, showing available flight tickets for a trip from São Paulo (GRU) to New York (JFK).

Next, let’s edit the AirCheapApi.js module to add methods to fetch the json file and dispatch the corresponding actions. As you did when you first created the AirCheapAPI file, you again assume that you will later implement some methods in the AirportActionCreators (fetchTicketsSuccess and fetchTicketsError). Listing 6-26 shows the updated file.

ActionCreators

Moving on, let’s edit the AirportActionCreators.js file. Of course you need to add the three necessary action creators for ticket fetching, but let’s start implementing another one, the chooseAirport action creator.

You provide the user with two auto-suggestion fields in the interface for selecting origin and destination airports, but so far nothing happens when the user chooses an airport. The chooseAirport action creator will be used for this purpose: it is invoked when either airport (origin or destination) is selected. Listing 6-27 shows the updated AirportActionCreators.

Stores

You next create two stores. The first store, RouteStore, holds the user selected origin and destination airports. The second store, TicketStore, holds the list of airline tickets that will be fetched when both airports are selected.

Let’s start with the RouteStore. It inherits from MapStore, which allows it to hold multiple key-value pairs. There are only two possible keys, origin and destination, and the store responds to the CHOOSE_AIRPORT action type to update the value of one of these keys with an airport code. Listing 6-28 shows the complete source code.

The TicketStore is very similar to the AirportStore. It inherits from ReduceStore and updates its state when the FETCH_TICKETS_SUCCESS action is dispatched. Listing 6-29 shows the complete source.

Notice that the TicketStore also responds to the FETCH_TICKETS action by resetting its state to an empty array. This way, every time you try to fetch different tickets, the interface can be immediately updated to clear any previous tickets that may exist.

Interface Components

Let’s begin your work on the interface by creating a new component, the TicketItem.js. It receives the component info as a prop and displays a single ticket row. The component’s code is shown in Listing 6-30.

In the sequence, let’s update the main App component. There are a few things you need to do:

  • Make the component listen to updates from the new stores (RouteStore and TicketStore) and calculate its state using both store states. To do this, edit the static methods getStores and calculateState:
    App.getStores = () => ([AirportStore,RouteStore,TicketStore]);
    App.calculateState = (prevState) => ({
      airports: AirportStore.getState(),
      origin: RouteStore.get(’origin’),
      destination: RouteStore.get(’destination’),
      tickets: TicketStore.getState()
    });
  • Invoke the chooseAirport action creator when the user chooses an origin or destination airport. To do this, you pass a callback to the AutoSuggest’s onSuggestionSelected prop. You could have two different callbacks (one for the origin field and other for the destination field), but using JavaScript’s bind function you can have just one callback function and pass a different parameter for each field:
    <Autosuggest id=’origin’
               suggestions={this.getSuggestions.bind(this)}
               onSuggestionSelected={this.handleSelect.bind(this,’origin’)}
               value={this.state.origin}
               inputAttributes={{placeholder:’From’}} />
    <Autosuggest id=’destination’
               suggestions={this.getSuggestions.bind(this)}
               onSuggestionSelected={this.handleSelect.bind(this,’destination’)}
               value={this.state.destination}
               inputAttributes={{placeholder:’To’}} />

    The handleSelect function uses a regular expression to separate the airport code from the string and invokes the chooseAirport action creator:

    handleSelect(target, suggestion, event){
      const airportCodeRegex = /(([^)]+))/;
      let airportCode = airportCodeRegex.exec(suggestion)[1];
      AirportActionCreators.chooseAirport(target, airportCode);
    }
  • Invoke the fetchTickets action creator when the user chooses both an origin and a destination aiport. You can do this on the componentWillUpdate lifecycle method; every time the user selects an airport, you invoke the chooseAirport action creator, and as a consequence the RouteStore dispatches a change event, and the App component will be updated. You check for two things before invoking the action creator: if both origin and destination were chosen and if either one has changed since the last update (so you only fetch once):
    componentWillUpdate(nextProps, nextState){
      let originAndDestinationSelected =   image;
                         nextState.origin && nextState.destination;
      let selectionHasChangedSinceLastUpdate =   image;
                         nextState.origin !== this.state.origin ||
                         nextState.destination !== this.state.destination;
      if(originAndDestinationSelected && selectionHasChangedSinceLastUpdate){
        AirportActionCreators.fetchTickets(nextState.origin,   image;
                                           nextState.destination);
      }
    }
  • Finally, import and implement the Ticket Item component you just created to show the loaded tickets:
    render() {
      let ticketList = this.state.tickets.map((ticket)=>(
        <TicketItem key={ticket.id} ticket={ticket} />
      ));
      return (
        <div>
          <header>
            <div className="header-brand">...</div>
            <div className="header-route">
              <Autosuggest id=’origin’ ... />
              <Autosuggest id=’destination’ ... />
            </div>
          </header>
          <div>
            {ticketList}
          </div>

        </div>
      );
     }

Listing 6-31 shows the complete updated App component with all the mentioned changes.

If you test now, the application should be working and loading tickets after the origin and destinations are selected.

Evolving Your Async Data Fetching Implementation

You saw that the best approach for asynchronous API communication within Flux is to encapsulate all API specific code in an API helper module. You invoke the API helper module through an action, and all remote data loaded asynchronously by the API helper module enters the system through an action. This is an elegant solution that follows the Flux principles of single direction data flow and isolates the rest of the system (stores and components) from async code. But it’s possible to further evolve this model to remove some boilerplate and decouple the API Helper module from the action creators. You achieve this by implementing a new method in the AppDispatcher: dispatch sync.

AppDispatcher’s dispatchAsync

The Flux’s dispatcher contains just a few public methods, and generally the most used one is dispatch. As you already know, the dispatch method is used to dispatch an action through all the registered stores.

As you saw in the earlier topics about asynchronous API (and in the sample AirCheap application), for every async operation there are three actions (async operation request, success, and failure). The generic dispatchAsync method expects a promise as a parameter, and the constants represent all steps of the async operation (request, success, failure) and automatically dispatch them based on the promise resolution.

Listing 6-32 shows the updated AppDispatcher with the dispatchAsync method implementation. Notice that you are using the Babel polyfill to make sure the Object.assign works on legacy browsers (make sure to install it using npm install --save babel-polyfill).

With this method, you can save a lot of typing in the ActionCreators, since instead of creating three methods for each async operation you can create only one. As an example, Listing 6-33 shows the updated AirportActionCreators file.

In the API helper module, not only do you reduce boilerplate, but you also decouple it from the action creators since the API Helper does not need to directly call the success or failure methods. All it has to do is return a promise. Listing 6-34 shows the updated AirCheapApi.js file, returning the promise created by the fetch operation and chained to the JSON parsing operation.

Kanban App: Moving to a Flux Architecture

You’ve been working on the Kanban App project since the beginning of this book, and in every chapter you’ve incrementally added new functionality to it. This chapter, however, will be different. Flux isn’t a requisite to bring new functionality to a React project, as you’ve seen throughout this chapter. Flux is an application architecture that helps make data changes in an app easier to reason about. In converting your Kanban App to a Flux architecture you’re not adding features; you’re making it more predictable and easier to reason about (and in this sense it certainly helps with adding new functionality in the future).

Refactor: Creating Flux Basic Structure and Moving Files

To get started, make sure to install flux in the project using npm: npm install --save flux. Next, let’s create folders for Flux’s files and move all your components (with the exception of the App.js file) to a components folder. The constants and utils files can also remain in the root of the app folder. Figure 6-10 shows the new folder structure.

9781484212615_Fig06-10.jpg

Figure 6-10. The new folder structure for the Kanban app

Fixing Imports

Obviously, you need to update the import statement in your components to reflect the new folder structure. Fortunately, the imports are all relative, so you don’t need to update every single component. The only affected components are

  • App.js (where you need to correct all the imported component’s paths)
  • KanbanBoardContainer.js (where you need to update only the utils import)
  • Card.js and List.js (where you need to fix the import of the constants.js module)

Listings 6-35 through 6-38 shows the aforementioned files with updated imports.

Adding Flux Basic Files

Flux is all about actions, stores, and a dispatcher (plus an API helper to handle API async requests). Let’s add five new files in the project to cover these:

  • An AppDispatcher.js
  • A store: CardStore.js, inside the stores folder
  • Actions: CardActionCreators.js and TaskActionCreators.js inside the actions folder
  • Finally, a KanbanApi.js helper module inside the api folder

You extend the base Flux dispacher with the DispatchAsync method you used earlier. As for the CardStore.js, CardActionCreators.js, TaskActionCreators.js, and KanbanAPi.js files, you begin with a basic skeleton for each and enhance them in the following sections. Listings 6-39 through 6-43 show the source for all these files, starting with the AppDispatcher.

For the CardStore, you extend Flux’s ReduceStore.

The CardActionCreators and TaskActionCreators start as plain JavaScript objects, but you import the modules that will be used later.

The same goes for the KanbanApi: it also starts as just a plain JavaScript object with the import statements for the modules that will be used later.

Moving the Data Fetching to the Flux Architecture

Your project structure is now ready to use Flux, and the first piece of code you’re going to port to the new architecture is the initial data fetching. Currently, all API communication (including the initial data fetching) is done in the KanbanBoardContainer, and the cards are kept in the component’s state. In the Flux architecture, the KanbanBoardContainer and child components such as the Card component will just fire actions; the API communication will be done by the API helper module and the cards will be kept in the CardStore.

Editing the KanbanBoardContainer

Since you’re tackling only the initial data fetching for now, you need to make the following changes to the KanbanBoardContainer:

  • Import the CardStore and CardActionCreator modules.
  • Make the KanbanBoardContainer listen to change events in the CardStore and map its state to the CardStore state (you can do this manually or using the Flux library Container higher order function). In the process, you remove the local state declared in the class constructor.
  • In the ComponentDidMount lifecycle method, instead of directly fetching data, you call an action creator to dispatch an action. This action will trigger a series of effects. The API helper will fetch the remote data, the CardStore will update itself with the new data and dispatch a change event, and finally the KanbanBoardContainer will have its state updated and will trigger a re-render.

The updated KanbanBoardContainer code is shown in Listing 6-44.

The end objective is to remove the eight methods that deal with card and task manipulations from the KanbanBoardContainer, but for now let’s just stick to the plan and only deal with the initial data fetching.

Implementing the FetchCards Action, API Method Call, and Store Callback

So far you’ve worked on two different Flux projects (Flux Bank and Air Cheap), so the process should be familiar: you need to define an action creator that, when invoked, will call the API Helper, receive a JavaScript promise, and dispatch different actions along the process (init of the fetching process, success or failure). The CardStore will respond to the success action and populate its state with the loaded cards.

FetchCards Constants and Action Creator

As you know, every action needs a constant to identify it. You already have a constants file. Let’s add three new constants: FETCH_CARDS, FETCH_CARDS_SUCCESS, and FETCH_CARDS_ERROR (as shown in Listing 6-45).

In the sequence, let’s create the fetchCards method in the CardActionCreators. You use the AppDispatcher’s DispatchAsync method to make things leaner. Listing 6-46 shows the implemented method.

Notice that you assume in the code that the KanbanAPI module has a fetchCards method. Let’s implement it in the next section.

fetchCards API Method

After the actionCreator (with the corresponding constants), let’s move to the kanbanApi. You basically copy the configuration and the fetch method that were used in the KanbanBoardContainer, but in this case, you simply return the fetch promise (instead of manipulating the card’s state; this part is now responsibility of the store), as show in Listing 6-47.

CardStore: Responding to FETCH_CARDS_SUCCESS

Finally, let’s update the reduce method in the CardStore to respond to the FETCH_CARD_SUCCESS and update its state with the loaded cards, as shown in Listing 6-48.

Since the KanbanBoardContainer is already listening to CardStore’s changes, your task is complete. If you test now, you should see the cards normally.

Moving All Card and Task Manipulations to the Flux Architecture

In the previous section, you removed the initial data fetching code from the KanbanBoardContainer, but the component still has eight other methods that manipulate cards and tasks. These methods are currently passed down as props through all the component hierarchy and are invoked from the List, Card, and Task components. You created these methods yourself throughout the book, but let’s take a brief recap: Table 6-3 lists these methods and what they do.

Table 6-3. Data Manipulation Methods Currently in KanbanBoardContainer Component

Method

Description

addCard

Receives an object with card properties as parameters; creates a new card.

updateCard

Receives an object with the updated card properties; updates the properties of the given card. In the refactor, it receives two properties: the original card properties and the changed card properties.

updateCardPosition

Receives the current card id and the card id with which the current card will switch positions. Called during the card drag-and-drop. Switches the positions of the given cards.

updateCardStatus

Receives the current card Id and the new status Id. Called during the card drag-and-drop. Updates the card status.

persistCardDrag

Receives an object containing a given card’s ID and the new card status. Called after a card’s drag-and-drop. Persists the new card’s position and status on the server.

addTask

Receives a card Id and a task name; creates a new task for a given card. In the refactor, you will pass an entire Task object instead of just the task name.

deleteTask

Receives a card Id, a task id, and the task index; deletes the task. In the refactor, you pass the entire card object instead of just the id.

toggleTask

Receives a card id, a task id, and the task index; toggles the task “done” property. In the refactor, you pass the entire card object instead of just the id.

Make all these changes at once. You first replicate all the method functionalities of the Flux architecture (action creators, KanbanApi, and CardStore). Only then do you update the KanbanBoardContainer and all the affected components in the hierarchy (KanbanBoard, List, Card, and Checklist components).

Preparing for the Functionality Migration

Before getting your hands on the action creators, API module, or store, let’s do some preparation. You declare all the necessary constants in the constants file. Listing 6-49 shows the updated constants.js file.

Action Creators

In sequence, let’s implement all the Card and Task manipulation actions. Notice that in the CardActionCreators module you’re importing and using the throttle utility function. Listing 6-50 shows the CardActionCreators.js file and Listing 6-51 shows the updated TaskActionCreators.js file.

KanbanApi

Following with your migration to the Flux architecture, let’s update the KanbanApi module, as shown in Listing 6-52.

CardStore

The final piece in this process is the updated CardStore (shown in Listing 6-53). Notice that besides responding to all the actions to manipulate its state, you also created the helper methods: getCard and getCardIndex.

Components

Heading back to the components, let’s remove all the data manipulation methods from the KanbanBoardContainer component. Observe that all these methods are grouped into two objects (taskActions and cardActions) and passed as props through the KanbanBoard, List, Card and CheckList components. You must remove these methods from the KanbanBoardContainer, and change the proptypes and render methods of all the mentioned components. And since you also won’t pass the cards as props to the NewCard and EditCart components, you need to edit them as well.

KanbanBoardContainer

Let’s tackle one file at a time. Listing 6-54 shows the updated code for the KanbanBoardContainer, without the constructor and data manipulation methods, but with updated render method (without passing the methods as props).

KanbanBoard

Next in the hierarchy is the KanbanBoard component. You don’t need to pass the taskCallbacks and cardCallbacks objects down to the Lists, and you also don’t need to clone the children prop that is provided by the React Router. You cloned the component to inject props into it, but in the Flux architecture they won’t be necessary. Listing 6-55 shows the updated KanbanBoard component.

List

Moving on, the next component in the hierarchy is the List component. You keep removing the taskCallbacks and cardCallbacks, but in this file you’ll also update the hover method in the listTargetSpec object; it used to invoke cardCallbacks.updateStatus, but now it’s going to invoke the updateCardStatus action creator. Listing 6-56 shows the updated source code.

Card

It’s time to update the Card component. The same premise is valid here: you remove any reference to CardCallbacks and TaskCallbacks as well as change any calls to those props to action creator calls. The updated card component is shown in Listing 6-57.

CheckList

In the Checklist component, let’s get rid of any TaskCallback calls and insert TaskActionCreator calls. Listing 6-58 shows the updated code.

NewCard and EditCard

Finally, let’s update both the NewCard and EditCard components. In both, you substitute the callback that was passed as props to action creator calls. In the EditCard, you go even further: since the component won’t have the cards array as props anymore, it will talk directly to the CardStore to retrieve the selected card details. Listing 6-59 shows the updated NewCard component and Listing 6-60 shows the updated EditCard component.

Removing All Component State

Ideally, you should avoid using and manipulating component state when using Flux. All the component state (even UI-related state) should be kept in stores. This is a good practice and a desirable target when writing Flux applications, but there’s nothing inherently wrong in having stateful components with limited, small, UI-related data.

That’s precisely the case for the Kanban App so far. You’ve converted pretty much everything to the Flux architecture, but some components still have local state. The Card component holds a showDetails local state, and the EditCard and NewCard components also have local state for the draft card being manipulated. As we said, this isn’t wrong, but for the sake of having a complete Flux port, let’s move all those local states to stores and keep the components leaner.

Show/Hide Card Details

Let’s start with the showDetails in the Card component. You won’t persist this data on the server, but you will use the existing CardStore to keep this value. The CardStore sets its state with the loaded cards data from the Kanban API. You need to add the ShowProperties key for each card, but for simplicity you’re not going to do it in the initial data fetch. Instead, you will assume a default value for the cards in which this property hasn’t been set yet, and only set this property when the user switches the visibility of the card details. In plain English, if there’s no showDetails property in the card, you assume that the details will show. When the user closes the card details for the first time, you then create this property on the desired card and set its value to false.

Card Component

Starting with the Card component, you will

  • Get rid of the constructor (since you don’t need to set an initial state).
  • Call an actionCreator when the user tries to toggle the details visibility.
  • Change all references from this.state.showDetails to this.props.showDetails.
  • Make sure to show the details only if the property exists (by checking if it is explicitly set to false).

Listing 6-61 shows the updated Card Component.

Constant and Action Creator

Next, you implement the toggleCardDetails action creator. You need a constant to identify this action, so add a new TOGGLE_CARD_DETAILS to the constants.js file, as show in Listing 6-62.

In the sequence, let’s edit the CardActionCreator file, as shown in Listing 6-63.

CardStore

Finally, let’s update the CardStore. Notice that you check if the showDetails value explicitly equals to false (a test that will fail is the property hasn’t been set yet). Listing 6-64 shows the updated file.

Edit and New Card Components

The last few components that still have local state are the EditCard and NewCard. In their case, though, the local state is more complex than a single property. It holds a complete card structure. For this reason, you create a completely new store, the DraftStore, that will hold Card information that is being edited.

DraftStore

The DraftStore responds to two actions: CREATE_DRAFT and UPDATE_DRAFT. When the CREATE_DRAFT action is dispatched, the DraftStore updates its internal state to either an empty card object (in the case of a new card) or a copy of an existing card object (in the case of a card edit). This draft card is supplied to a controlled form and an UPDATE_DRAFT action is dispatched for every change.

Let’s take a look at the DraftStore source code (as shown in Listing 6-65) before moving to the rest of the implementation.

There are a few things worth noticing in the code. First, you have a function called defaultDraft that returns a default, clear card object with a temporary ID.

Also, in the switch statement in the reduce method, when responding to the CREATE_DRAFT action, you check if a card object was passed as payload. This is the case where an existing card is being edited; the card properties are then copied and set as the store state. If no card is passed as parameter (which is the case when the user is creating a new card), the defaultDraft method is invoked to create a default empty card that is set as the store state.

The UPDATE_DRAFT action passes two payloads: the field that the user edited, and its new value. In this case, the new value is set in the corresponing property of the draft card in the store’s state.

Constants and ActionCreators

There is nothing especially notable here. Just add the new constants and declare the new action creators, as show in Listings 6-66 and 6-67, respectively.

EditCard and NewCard Components

To finish removing local state from your components, let’s update the EditCard and NewCard files. You remove the constructor method and substitute any local state manipulation for action creator calls. Additionally, both components use the Flux library Container higher order function to listen to the DraftStore changes and map its state. Listings 6-68 and 6-69 show the updated code.

It certainly was a big refactor in your project, but the end result is a clearer, more organized, and predictable code base. As usual, the complete source code is available at the Apress site (www.apress.com) and on this book’s GitHub page (pro-react.github.io).

Summary

In this chapter, you learned what Flux is and which problems it solves. You saw how to integrate Flux in a React application and how to architect complex applications including async API communication.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.130.24