CHAPTER 3

image

Architecting Applications with Components

The previous chapters provided an overview of React. You saw that React is all about bringing a component-based architecture to interface building. You understood the evolutionary approach of bringing HTML together with JavaScript to describe components and achieve separation of concerns not by separating technologies or languages, but by having discreet, isolated, reusable, and composable components.

This chapter will cover how to structure a complex user interface made of nested components. You will see the importance of exposing a component API through propTypes, understand how data flows in a React application, and explore techniques on how to compose components.

Prop Validation

When creating components, remember that they can be composed into bigger components and reused (in the same project, in other projects, by other developers). Therefore, it is a good practice to make explicit in your components which props can be used, which ones are required, and which types of values they accept. This can be done by declaring propTypes. propTypes help document your components, which benefits future development in two ways.

  1. You can easily open up a component and check which props are required and what type they should be.
  2. When things get messed up, React will give you an error message in the console, saying which props are wrong/missing and the render method that caused the problem.

propTypes are defined as a class constructor property. For example, given this Greeter React component

import React, { Component } from ’react’;
import { render } from ’react-dom’;

class Greeter extends Component {
  render() {
    return (
      <h1>{this.props.salutation}</h1>
    )
  }
}

render(<Greeter salutation="Hello World" />, document.getElementById(’root’));

the salutation prop needs to be a string and is required (you can’t render unless a salutation is provided). To achieve this, you have to define the propTypes as a class constructor property, like this:

import React, { Component, PropTypes } from ’react’;
import { render } from ’react-dom’;

class Greeter extends Component {
  render() {
    return (
      <h1>{this.props.salutation}</h1>
    )
  }
}
Greeter.propTypes = {
  salutation: PropTypes.string.isRequired
}

render(<Greeter salutation="Hello World" />, document.getElementById(’root’));

If the requirements of the propTypes are not met when the component is instantiated, a console.warn will be logged. For example, if you try to render a Greeter component without any props

React.render(<Greeter />, document.getElementById(’root’));

the warning will be

Warning: Failed propType: Required prop `salutation` was not specified in `Greeter`.

For optional props, simple leave the .isRequired off, in which case the prop type will only be checked by React if a value is provided.

Default Prop Values

You can also provide a default prop value in case none is provided. The syntax is similar: define a defaultProps object as a constructor property.

You could, for example, leave the prop salutation optional (by removing the isRequired) and give it a default value of “Hello World”:

class Greeter extends Component {
  render() {
    return (
      <h1>{this.props.salutation}</h1>
    )
  }
}

Greeter.propTypes = {
  salutation: PropTypes.string
}
Greeter.defaultProps = {
  salutation: "Hello World"
}

render(<Greeter />, document.getElementById(’root’));

Now, if no salutation prop is provided, your component will render a default “Hello World”. If a salutation is provided, though, it needs to be of type string.

As said earlier, you are not required to use propTypes in your application, but they provide a good way to describe the API of your component, and it is a good practice to always declare them.

Built-in propType Validators

React propTypes export a range of validators that can be used to make sure the data you receive is valid. By default, all of the propTypes in Tables 3-1 through 3-3 are optional, but you can chain with isRequired to make sure a warning is shown if the prop isn’t provided.

Table 3-1. JavaScript Primitives PropTypes

Validator

Description

PropTypes.array

Prop must be an array.

PropTypes.bool

Prop must be a Boolean value (true/false).

PropTypes.func

Prop must be a function.

PropTypes.number

Prop must be a number (or a value that can be parsed into a number).

PropTypes.object

Prop must be an object.

PropTypes.string

Prop must be a string.

Table 3-2. Combined Primitives PropTypes

Validator

Description

PropTypes.oneOfType

An object that could be one of many types, such as

PropTypes.oneOfType([

  PropTypes.string,

  PropTypes.number,

  PropTypes.instanceOf(Message)

])

PropTypes.arrayOf

Prop must be an array of a certain type, such as

PropTypes.arrayOf(PropTypes.number)

PropTypes.objectOf

Prop must be an object with property values of a certain type, such as

PropTypes.objectOf(PropTypes.number)

PropTypes.shape

Prop must be an object taking on a particular shape. It needs the same set of properties, such as

PropTypes.shape({

  color: PropTypes.string,

  fontSize: PropTypes.number

})

Table 3-3. Special PropTypes

Validator

Description

PropTypes.node

Prop can be of any value that can be rendered: numbers, strings, elements, or an array.

PropTypes.element

Prop must be a React element.

PropTypes.instanceOf

Prop must be instance of a given class (this uses JS’s instanceof operator.), such as PropTypes.instanceOf(Message).

PropTypes.oneOf

Ensure that your prop is limited to specific values by treating it as an enum, like PropTypes.oneOf([’News’, ’Photos’]).

Kanban App: Defining Prop Types

The correct approach is to declare a component’s propTypes as soon as you create the component itself, but given that you just learned about them and their importance, let’s review all of the Kanban App’s components and declare its propTypes (as shown in Listings 3-1 to 3-4).

Custom PropType Validators

As mentioned, React offers a great suite of built-in propType validators that cover pretty much every basic use case, but there may still be some scenarios where one might need a more specific validator.

Validators are basically just functions that receive a list of properties, the name of the property to check, and the name of the component. The function must then return either nothing (if the tested prop was valid) or an instance of an Error suitable for the invalid prop.

Kanban App: Defining a Custom PropType Validator

In your Kanban app, the Card component has a title, a description, and other properties. By way of an example, you’re going to write a validator that will warn if the card title is longer than 80 characters. The code is shown in Listing 3-5, and a sample card failing the custom propType validator is represented in Figure 3-1.

9781484212615_Fig03-01.jpg

Figure 3-1. A new card failing the custom propType validation

Image Note  In this sample code, you use the new JavaScript ES6 syntax for string interpolation. You can learn more about this and other ES6 language features used throughout this book in the online Appendix C.

Component Composition Strategies and Best Practices

This section will cover strategies and best practices for creating React applications by composing components. You will discuss how to achieve state management, data fetching, and control over user interactions in a structured and organized way.

Stateful and Pure Components

So far you’ve seen that components can have data as props and state.

  • Props are a component’s configuration. They are received from above and immutable as far as the component receiving them is concerned.
  • State starts with a default value defined in the component’s constructor and then suffers from mutations in time (mostly generated from user events). A component manages its own state internally, and every time the state changes, the component is rendered again.

In React’s components, state is optional. In fact, in most React applications the components are split into two types: those that manage state (stateful components) and those that don’t have internal state and just deal with the display of data (pure components).

The goal of pure components is to write them so they only accept props and are responsible for rendering those props into a view. This makes it easier to reuse and test those components.

However, sometimes you need to respond to user input, a server request, or the passage of time. For this, you use state. Stateful components usually are higher on the component hierarchy and wrap one or more stateful or pure components.

It’s a good practice to keep most of an app’s components stateless. Having your application’s state scattered across multiple components makes it harder to track. It also reduces predictability because the way your application works becomes less transparent. There’s also the potential to introduce some very hard-to-untangle situations in your code.

Which Components Should Be Stateful?

Recognizing which components should own state is often the most challenging part for React newcomers to understand. When in doubt, follow this four-step checklist. For each piece of state in your application,

  • Identify every component that renders something based on that state.
  • Find a common owner component (a single component above all the components that need the state in the hierarchy).
  • Either the common owner or another component higher up in the hierarchy should own the state.
  • If you can’t find a component where it makes sense to own the state, create a new component simply to hold the state and add it somewhere in the hierarchy above the common owner component.

To illustrate this concept, let’s build a very simple contact app, as shown in Figure 3-2.

9781484212615_Fig03-02.jpg

Figure 3-2. The sample contacts app with search

The component hierarchy is

  • ContactsApp: The main component
    • SearchBar: Shows an input field so the user can filter the contacts
    • ContactList: Loops through data, creating a series of ContactItems
      • ContactItem: Displays the contact data

In the code, the contact list data is stored in a global variable. In a real app, the data would probably be fetched remotely, but for the sake of simplicity, it will be hard-coded on this example. Listing 3-6 shows the complete code including the ContactsApp, SearchBar, ContactList, and ContactItem components (as well as their propTypes).

At this moment, all of your application’s components are pure; they only render data received via props. However, you need to add the filter behavior to your app, and you will need to store mutable state to achieve that. Let’s run through the checklist to figure out where state in this application should live.

ContactList needs to filter the contacts based on state, and SearchBar needs to display the search text. The common owner component is ContactsApp.

It conceptually makes sense for the filter text to live as a state in ContactsApp. The ContactsApp in turn will pass the filter text down as props. The SearchBar component will use it as value for the input field and the ContactList will use it to filter the contacts. Let’s implement this component by component (as shown in Listings 3-7 through 3-9). In Listing 3-8, the SearchBar component will receive the filterText as a prop and set the input field value to this prop. The input field now is a controlled form component (as seen in Chapter 2). In Listing 3-9, the ContactList component also receives filterText as a prop and filters the contacts to show based on its value.

Now your application has only one stateful component on the top of the hierarchy and three pure components that display data received via props. The ContactList component filters the data to show based on the filterText prop (you can try right now by changing the ContactsApp’s filterText state on the code), but the user can’t type anything on the search field because it can’t change its state from inside the SearchBar component; the state is owned by the parent component.

In the next section, you will learn how child (pure) components can communicate back to parent (stateful) components.

Data Flow and Component Communication

In a React application, data flows down in the hierarchy of components: React makes this data flow explicit to make it easy to understand how your program works.

In non-trivial apps, though, nested child components need to communicate with the parent component. One method to achieve this is through callbacks passed by parent components as props.

Let’s use the ContactApp example to illustrate this. State belongs to the topmost ContactApp component and is passed down as props to SearchBar and ContactList.

You want to make sure that whenever the user changes the search form, you update the state to reflect the user input. Since components should only update their own state, ContactApp will pass a callback to SearchBar that will fire whenever the state should be updated. You can use the onChange event on the inputs to be notified of it. On the ContactsApp, you create a local function to change the filterText state and pass this function down as a prop to the searchBar (Listing 3-10).

The SearchBar component receives the callback as a prop and calls on the onChange event of the input field (Listing 3-11).

The search in action and the complete code are shown in Figure 3-3 and Listing 3-12.

9781484212615_Fig03-03.jpg

Figure 3-3. The Contacts app’s filter in action

Component Lifecycle

When creating React components, it’s possible to declare methods that will be automatically called in certain occasions throughout the lifecycle of the component. Understanding the role that each component lifecycle method plays and the order in which they are invoked will enable you to perform certain actions when a component is created or destroyed. It also gives you the opportunity to react to props or state changes accordingly.

Moreover, an implicit knowledge about the lifecycle methods is also necessary for performance optimizations (covered in Chapter 7) and to organize your components in a Flux architecture (covered in Chapter 6).

Lifecycle Phases and Methods

To get a clear idea of the lifecycle, you need to differentiate between the initial component creation phase, state and props changes, triggered updates, and the component’s unmouting phase. Figures 3-4 to 3-7 demonstrate which methods are called on each phase.

9781484212615_Fig03-04.jpg

Figure 3-4. Lifecycle methods invoked on the mounting cycle

9781484212615_Fig03-05.jpg

Figure 3-5. Lifecycle method invoked on the unmounting cycle

9781484212615_Fig03-06.jpg

Figure 3-6. Lifecycle methods invoked when the props of a component change

9781484212615_Fig03-07.jpg

Figure 3-7. Lifecycle methods invoked when the component’s state change

Lifecycle Functions in Practice: Data Fetching

To illustrate the usage of lifecycle methods in practice, imagine you want to change your last Contacts application to fetch the contacts data remotely. Data fetching is not really a React subject; it’s just plain JavaScript, but the important aspect to notice is that you do have to fetch the data on a specific lifecycle of the component, the componentDidMount lifecycle method.

Since this chapter is about strategies and good practices for component composition, it is also worth noting that you should avoid adding data fetching logic to a component that already has other responsibilities. A good practice, instead, is to create a new stateful component whose single responsibility is communicating with the remote API, and passing data and callbacks down as props. Some people call this type of component a container component.

You will use the idea of a container component in your Contacts app, so instead of adding the data-fetching logic to the existing ContactsApp component, you will create a new component called ContactsAppContainer on top of it. The old ContactsApp won’t be changed in any way. It will continue to receive data via props.

Image Note  In this sample code, you use the new window.fetch function, which is an easier way to make web requests and handle responses than using XMLHttpRequest. At the time of this writing, only Chrome and Firefox support this new standard, so install and import the whatwg-fetch polyfill from npm. (Polyfill is browser fallback that allows specific functionality to work in browsers that do not have the support for that functionality built in.)

npm install --save whatwg-fetch

Let’s start by moving the hard-coded data to a json file (the json file must be in the public or static folder, so it will be served by the development server), as shown in Figure 3-8. Your project folder structure may vary; the important thing to notice is that the file is in the public or static folder that will be served by the web server.

9781484212615_Fig03-08.jpg

Figure 3-8. The new contacts.json file

The new ContactsAppContainer component is shown in Listing 3-13. No other components were changed, except that instead of rendering ContactsApp you now render ContactsAppContainer to the document (as shown in the last lines of the listing).

That’s all for remote data fetching. If you reload the Contacts app in the browser, it will look like nothing has changed, but underneath it is now loading the contacts data from an external source.

A Brief Talk About Immutability

React provides a setState method to make changes to the component internal state. Be careful to always use the setState method to update the state of your component’s UI and never manipulate this.state directly. As a rule of thumb, treat this.state as if it were immutable.

There are different reasons for this. For one, by manipulating this.state directly you are circumventing React’s state management, which not only works against the React paradigm but can also be potentially dangerous because calling setState() afterwards may replace the mutation you made. Furthermore, manipulating this.state directly minimizes the possibilities for future performance improvements in the application.

You will learn about performance improvements in later chapters but in many cases it deals with object comparison and checking weather a JavaScript object has changed or not. As it turns out, this is a pretty expensive operation in JavaScript that can generate a lot of overhead, but there’s a simpler and faster way: if any time an object is changed it’s replaced instead edited in place, then the check is orders of magnitude faster (because you can simply compare object references, such as object1 === object2).

That’s the basic idea of immutability. Instead of changing an object, replace it.

Immutability in Plain JavaScript

The main idea behind immutability is just to replace the object instead of changing it, and while this is absolutely possible in plain JavaScript, it’s not the norm. If you’re not careful, you may unintentionally mutate objects directly instead of replacing them. For example, let’s say you have this stateful component that displays data about a voucher for an airline travel (the render method is omitted in this example because you are only investigating the component’s state):

import React, { Component } from ’react’;
import { render } from ’react-dom’;

class Voucher extends Component {
  constructor() {
    super(...arguments)
    this.state = {
      passengers:[
       ’Simmon, Robert A.’,
       ’Taylor, Kathleen R.’
      ],
      ticket:{
        company: ’Dalta’,
        flightNo: ’0990’,
        departure: {
          airport: ’LAS’,
          time: ’2016-08-21T10:00:00.000Z’
        },
        arrival: {
          airport: ’MIA’,
          time: ’2016-08-21T14:41:10.000Z’
        },
        codeshare: [
          {company:’GL’, flightNo:’9840’},
          {company:’TM’, flightNo:’5010’}
        ]
      }
    }
  }

  render() {...}
}

Now, suppose you want to add a passenger to the passengers array. If you’re not careful, you may unintentionally mutate the component state directly. For example,

let updatedPassengers = this.state.passengers;
updatedPassengers.push(’Mitchell, Vincent M.’);
this.setState({passengers:updatedPassengers});

The problem in this sample code, as you may have guessed, is that in JavaScript, objects and arrays are passed by reference. This means that when you say updatedPassengers=this.state.passengers you’re not making a copy of the array; you are just creating a new reference to the same array that is in the current component’s state. Furthermore, by using the array method push, you end up mutating its state directly.

To create actual array copies in JavaScript, you need to use non-destructive methods, that is, methods that will return an array with the desired mutations instead of actually changing the original one. map, filter, and concat are just a few examples of non-destructive array methods. Let’s reapproach the earlier problem of adding a new passenger to the array, this time using the Array’s concat method:

// updatedPassengers is a new array, returned from concat
let updatedPassengers = this.state.passengers.concat(’Mitchell, Vincent M.’);
this.setState({passengers:updatedPassengers});

There are also alternatives for generating new objects with mutations in JavaScript, like using Object.assign. Object.assign works by merging all properties of all given objects to the target object:

Object.assign(target, source_1, ..., source_n)

It first copies all enumerable properties of source 1 to the target, then those of source_2, etc. For example, to change the flightNo on the ticket state key, you could do this:

// updatedTicket is a new object with the original properties of this.state.ticket
// merged with the new flightNo.
let updatedTicket = Object.assign({}, this.state.ticket, {flightNo:’1010’});
this.setState({ticket:updatedTicket});

Image Note  At the time of this writing, only Chrome and Firefox supported the new method Object.assign, but the good news is that Babel (the ES6 compiler you’re using together with Webpack) already provides the polyfill for other browsers. All you need to do is install with ’npm install --save babel-polyfill’ and import it with import ’babel-polyfill’.

Nested Objects

Although an array’s non-destructive methods and Object.assign will do the job on most cases, it gets really tricky if your state contains nested objects or arrays. This is because of a characteristic of the JavaScript language: objects and arrays are passed by reference, and neither the array’s non-destructive methods nor Object.assign make deep copies. In practice, this means the the nested objects and arrays in your newly returned object will only be references to the same objects and arrays on the old object.

Let’s see this in practice, given the ticket object you were working on:

let originalTicket={
  company: ’Dalta’,
  flightNo: ’0990’,
  departure: {
    airport: ’LAS’,
    time: ’2016-08-21T10:00:00.000Z’
  },
  arrival: {
    airport: ’MIA’,
    time: ’2016-08-21T14:41:10.000Z’
  },
  codeshare: [
    {company:’GL’, flightNo:’9840’},
    {company:’TM’, flightNo:’5010’}
  ]
}

If you create a ticket object with the Object.assign, like

let newTicket = Object.assign({}, originalTicket, {flightNo ’5690’}}

You will end up with two objects in memory, as shown in Figure 3-9.

9781484212615_Fig03-09.jpg

Figure 3-9. Note that originalTicket and newTicket have different flightNo properties

However, given the default JavaScript behavior of passing arrays and objects by reference, the departure and arrival objects on newTicket aren’t separate copies; they’re references to the same originalTicket object. If you try to change the arrival object on newTicket, for example,

newTicket.arrival.airport="MCO"

Figure 3-10 shows both object representations now.

9781484212615_Fig03-10.jpg

Figure 3-10. originalTicket and newTicket arrival keys references the same object

Again, this has nothing to do with React; it’s just the default JavaScript behavior, but this default behavior can and will impact React if you want to mutate a component state with nested objects. You could try making a deep clone of the original object, but this isn’t a good option because it is expensive in performance and even impossible to do in some cases. The good news is that there is a simple solution: the React add-ons package provides a utility function (called immutability helper) that helps update more complex and nested models.

React Immutability Helper

React’s add-on package provides an immutability helper called update. The update function works on regular JavaScript objects and arrays and helps manipulates these objects as if they were immutable: instead of actually changing the object, it always return a new, mutated object.

To begin with, you’ll need to install and require the library:

npm install –save react-addons-update

Then, in your javascript file, import is using

import update from ’react-addons-update’;

The update method accepts two parameters. The first one is the object or array that you want to update. The second parameter is an object that describes WHERE the mutation should take place and WHAT kind of mutation you want to make. So, given this simple object:

let student = {name:’John Caster’, grades:[’A’,’C’,’B’]}

to create a copy of this object with a new, updated grade, the syntax for update is

let newStudent = update(student, {grades:{$push: [’A’]}})

The object {grades:{$push: [’A’]}} informs, from left to right, that the update function should

  1. Locate the key grades (“where” the mutation will take place).
  2. Push a new value to the array (“what” kind of mutation should happen).

If you want to completely change the array, you use the command $set instead of $push:

let newStudent = update(student, {grades:{$set: [’A’,’A’,’B’]}})

There’s no limit to the amount of nesting you can do. Let’s head back to your voucher ticket object, where you were having trouble creating a new object with a different arrival information. The original object was

let originalTicket={
  company: ’Dalta’,
  flightNo: ’0990’,
  departure: {
    airport: ’LAS’,
    time: ’2016-08-21T10:00:00.000Z’
  },
  arrival: {
    airport: ’MIA’,
    time: ’2016-08-21T14:41:10.000Z’
  },
  codeshare: [
    {company:’GL’, flightNo:’9840’},
    {company:’TM’, flightNo:’5010’}
  ]
}

The information you want to change (airport) is nested three levels deep. In React’s update addon, all you need to do is keep nesting objects with their names on the objects that describe the mutation:

let newTicket = update(originalTicket, {
                         arrival: {
                           airport: {$set: ’MCO’}
                         }
                       });

Now only the new Ticket has the arrival airport set to “MCO”. The original ticket maintains the original arrival airport, as shown in Figure 3-11.

9781484212615_Fig03-11.jpg

Figure 3-11. originalTicket and newTicket now don’t share the same arrival nested object

Array Indexes

It’s also possible to use array indexes to find WHERE a mutation should happen. For example, if you want to mutate the first codeshare object (the array elopement at index 0),

let newTicket = update(originalTicket,{
                         codeshare: {
                           0: { $set: {company:’AZ’, flightNo:’7320’} }
                         }
                       });

Figure 3-12 shows the different objects with the newTicket array mutated.

9781484212615_Fig03-12.jpg

Figure 3-12. Changes by array index using React’s immutability helpers

Available Commands

The available commands to determinate “what” kind of mutation should happen are shown in Table 3-4.

Table 3-4. React Immutability Helper Commands

Command

Description

$push

Similar to Array’s push, it adds one or more elements to the end of an array. Example:

let initialArray = [1, 2, 3];

let newArray = update(initialArray, {$push: [4]});

// => [1, 2, 3, 4]

$unshift

Similar to Array’s unshift, it adds one or more elements to the beginning of an array. Example:

let initialArray = [1, 2, 3];

let newArray = update(initialArray, {$unshift: [0]});

// => [0,1, 2, 3]

$splice

Similar to Array’s splice, it changes the content of an array by removing and/or adding new elements. The main syntactical difference here is that you should provide an array of arrays as a parameter, each individual array containing the splice parameters to operate on the array. Example:

let initial Array = [1, 2, ’a’];

let newArray = update(initialArray, {$splice: [[2,1,3,4]]});

// => [1, 2, 3, 4]

$set

Replace the target entirely.

$merge

Merge the keys of the given object with the target. Example:

let ob. = {a: 5, b: 3};

let newObj = update(obj, {$merge: {b: 6, c: 7}});

// => {a: 5, b: 6, c: 7}

$apply

Pass in the current value to the given function and update it with the new returned value. Example:

let obj = {a: 5, b: 3};

let newObj = update(obj, {b: {$apply: (value) => value*2 }});

// => {a: 5, b: 6}

Kanban App: Adding (a Little) Complexity

To put all the new knowledge about components composition and state management in practice, you will connect the Kanban App Connect to an external API. You will fetch all the application’s data from the server and manipulate tasks (delete, create, and toggle).

Fetching the Initial Cards from the External API

You will start by creating a new component at the top of your hierarchy. This container component will be used for data fetching/persistence. Create a new file named KanbanBoardContainer.js with a basic React component structure (as shown in Listing 3-14).

In the sequence, you fetch the data from the Kanban API Server. As you did earlier in this chapter, you use the new window.fetch function available on the latest generation of browsers. To make sure your app will run on other browsers as well, install the fetch polyfill from npm and save it as a dependency of the project:

npm install --save whatwg-fetch

For convenience, an online API for testing is provided at http://kanbanapi.pro-ract.com.

If you prefer to run locally, you can download the Kanban API Server from www.apress.com or from the book’s github page at https://github.com/pro-react.

The only difference between the online API at kanbanapi.pro-react.com and the API Server is that to use the former you need to pass an authorization header (so the server can uniquely identify you and serve your own cards and tasks). The authorization can be any string that uniquely identifies your app or yourself (such a generic combination of characters or your e-mail address, for example). In both cases, a standard set of cards and tasks are already available on your first use so you can start testing immediately.

Image Note  The online Kanban rest API at kanbanapi.pro-react.com is provided for educational purposes only. As such, stored information will be reset after 24 hours of inactivity.

Also, please be careful about storing sensitive information on the kanbanapi.pro-react.com server. Although the server employs standard security measures, it is by definition not private.

The online API’s terms of use statement is available at http://kanbanapi.pro-react.com/terms.

Let’s start fetching the initial data for the application on the KanbanBoardContainer component, as shown in Listing 3-15. Note that you also add custom headers to the fetch command to make sure the server will respond properly.

You create a new container component that fetches data remotely and passes to its corresponding pure component. All you need to do now is change the original App.js file to render the new KanbanBoardContainer, instead of rendering KanbanBoard directly:

import React from ’react’;
import {render} from ’react-dom’;
import KanbanBoardContainer from ’./KanbanBoardContainer’;

render(<KanbanBoardContainer />, document.getElementById(’root’));

If you test right now, it will look like nothing happened at all. The difference is that the Kanban app is live, so the data is no longer hard-coded.

Wiring Up the Task Callbacks as Props

Now let’s create three functions to manipulate the tasks: addTask, deleteTask, and toggleTask. Since tasks belong to a card, all functions need to receive the cardId as a parameter. The addTask will receive the new task text, while both deleteTask and toggleTask should receive the taskId and the taskIndex (the position inside the card’s array of tasks). You will pass the three functions down the whole hierarchy of components as props.

As a small trick to save a little typing, instead of creating one prop to pass each new function, you create a single object that references the three functions and pass it as a single prop. The code is shown in Listing 3-16.

Now there’s some repetitive work to be done: all the components between the top of the hierarchy and the CheckList component (that is, the KanbanBoard, List and Card components) must receive the taskCallbacks prop from its parent and pass it along as a prop to its children. Despite looking like a repetitive task, this will make very clear how the communication is flowing from component to component. Listings 3-17, 3-18, and 3-19 show the updated code for those three components.

In Listing 3-18, it’s worth noticing the use of the spread operator to reduce some typing when passing props to the Card component. To learn more about the spread operator, reference the online appendixes.

Finally, when in the Checklist component, you make use of taskCallbacks.taskCallbacks.delete and taskCallbacks.toggle, which can be directly associated with element event handlers:

class CheckList extends Component {
  render() {
    let tasks = this.props.tasks.map((task, taskIndex) => (
      <li key={task.id} className="checklist__task">
        <input type="checkbox" checked={task.done} onChange={
           this.props.taskCallbacks.toggle.bind(null, this.props.cardId, task.id, taskIndex)
        } />
        {task.name}{’ ’}
        <a href="#" className="checklist__task--remove" onClick={
           this.props.taskCallbacks.delete.bind(null, this.props.cardId, task.id, taskIndex)
        } />
      </li>
    ));

    return (...);
  }
}

To add a new task, however, you do some pre-processing inside the component before invoking the taskCallbacks.add callback. You do so for two reasons: to check if the user pressed the Enter key, and to clear the input field after invoking the callback function:

class CheckList extends Component {
  checkInputKeyPress(evt){
    if(evt.key === ’Enter’){
      this.props.taskCallbacks.add(this.props.cardId, evt.target.value);
      evt.target.value = ’’;
    }
  }

  render() {
    let tasks = this.props.tasks.map((task, taskIndex) => (...));

    return (
      <div className="checklist">
        <ul>{tasks}</ul>
        <input type="text"
               className="checklist--add-task"
               placeholder="Type then hit Enter to add a task"
               onKeyPress={this.checkInputKeyPress.bind(this)}  />
      </div>
    )
  }
}

The complete code for the CheckList component is shown in Listing 3-20.

Manipulating Tasks

In this last part, you make the actual manipulations of the tasks in the KanbanAppContainer state and persist all changes on the server through the API. In all three methods (deleteTask, toggleTask, and addTask), you need to make sure not to manipulate the current state directly, so you will use React’s immutability helpers. Don’t forget to install them using npm install --save react-addons-update.

There is one problem, though: since you filtered the cards in the KanbanList component, you don’t have access to their original indexes anymore (and their indexes will be required to use the immutability helpers). So you can use the new findIndex() array method that runs a testing function on each element and returns the index of the element that satisfies the testing function.

Image Note  At the time of this writing, only Chrome and Firefox supported the new methods array.prototype.find and array.prototype.findIndex, so make sure to install babel-polyfill:

npm install --save babel-polyfill

Then, in your file, import it using:

import ’babel-polyfill’

Let’s start coding the methods, beginning with the deleteTask method. You start by finding the index of the card you want by its ID. Then you create a new mutated object without the deleted task using the immutability helpers. Finally, you setState for the mutated object and use Fetch to inform the server of the change.

deleteTask(cardId, taskId, taskIndex){
    // Find the index of the card
    let cardIndex = this.state.cards.findIndex((card)=>card.id == cardId);

    // Create a new object without the task
    let nextState = update(this.state.cards, {
                            [cardIndex]: {
                              tasks: {$splice: [[taskIndex,1]] }
                            }
                          });

    // set the component state to the mutated object
    this.setState({cards:nextState});

    // Call the API to remove the task on the server
    fetch(`${API_URL}/cards/${cardId}/tasks/${taskId}`, {
      method: ’delete’,
      headers: API_HEADERS
    });
  }

Toggling a task will happen in a similar fashion, but instead of splicing the array, you walk the object hierarchy up to the done property of the task and directly manipulate its value using a function:

toggleTask(cardId, taskId, taskIndex){
    // Find the index of the card
    let cardIndex = this.state.cards.findIndex((card)=>card.id == cardId);
    // Save a reference to the task’s ’done’ value
    let newDoneValue;
    // Using the $apply command, you will change the done value to its opposite
    let nextState = update(this.state.cards, {
                            [cardIndex]: {
                              tasks: {
                                [taskIndex]: {
                                  done: { $apply: (done) => {
                                      newDoneValue = !done
                                      return newDoneValue;
                                    }
                                  }
                                }
                              }
                            }
                         });

    // set the component state to the mutated object
    this.setState({cards:nextState});

    // Call the API to toggle the task on the server
    fetch(`${API_URL}/cards/${cardId}/tasks/${taskId}`, {
        method: ’put’,
        headers: API_HEADERS,
        body: JSON.stringify({done:newDoneValue})
    });
  }

As you may imagine, adding a new task works in a similar way. The only thing to notice is that since all tasks need an ID, you must generate a temporary ID for the task until it’s persisted to the server and it returns the definitive ID. Then you must update the task ID. The temporary ID can be as simple as the current time in milliseconds:

addTask(cardId, taskName){
    // Find the index of the card
    let cardIndex = this.state.cards.findIndex((card)=>card.id == cardId);

    // Create a new task with the given name and a temporary ID
    let newTask = {id:Date.now(), name:taskName, done:false};

    // Create a new object and push the new task to the array of tasks
    let nextState = update(this.state.cards, {
                      [cardIndex]: {
                        tasks: {$push: [newTask] }
                      }
                    });

    // set the component state to the mutated object
    this.setState({cards:nextState});

    // Call the API to add the task on the server
    fetch(`${API_URL}/cards/${cardId}/tasks`, {
      method: ’post’,
      headers: API_HEADERS,
      body: JSON.stringify(newTask)
    })
    .then((response) => response.json())
    .then((responseData) => {
      // When the server returns the definitive ID
      // used for the new Task on the server, update it on React
      newTask.id=responseData.id
      this.setState({cards:nextState});
    });
  }

Basic Optimistic Updates Rollback

You may have notice that you’ve made all the changes in the UI optimistically, that is, without actually waiting for the server to respond if the changes were saved. Being optimistic is important for perceived performance: when users interact with an online app, they don’t want to wait for things to happen. They don’t care that their tasks need to be stored in a remote database. Everything should appear to happen instantly. But what happens if the server fails? You need to make some new tries, revert back the UI changes, notify the user, and so on. . .

Optimistic updating and rollback is not a trivial task and can unfold in many outcomes, but it’s easy to cover the basic rollback scenario right now because of a side effect of working with immutable structures: you can keep a reference to the old state and revert it back in case of problems.

For all three task callbacks, the code will be the same. First, keep a reference to the original state of the component:

// Keep a reference to the original state prior to the mutations
// in case you need to revert the optimistic changes in the UI
let prevState = this.state;

In the sequence, use setState to revert back to the original state if the fetch command fails OR if the server response status was not ok:

fetch(..., {...})
.then((response) => {
  if(!response.ok){
    // Throw an error if server response wasn’t ’ok’
    // so you can revert back the optimistic changes
    // made to the UI.
    throw new Error("Server response wasn’t OK")
  }
})
.catch((error) => {
  console.error("Fetch error:",error)
  this.setState(prevState);
});

To test, you can simply shut down the local API server (or disconnect from the Internet if you are using the online API) and try to make any changes to the tasks.

The complete for the KanbanAppContainer component is shown in Listing 3-21.

Summary

In this chapter, you studied how to structure complex UIs in React. You learned that in a React application, data always flows in a single direction, from parent to child components. For communication, a parent component can pass a callback function down as props so child components can report back.

You also saw that components can be much easier to reuse and reason about if you divide them into two categories: stateful components (which manipulate internal state) and pure components (which don’t have an internal state and only display data received via props). It’s a good practice to structure your application so that it has fewer stateful components (usually on the top levels of your application component hierarchy) and more pure components.

Finally, you saw why it’s important to treat the component state as immutable, always using this.setState to make changes on it (and you learned how to use React’s immutable helpers to generate mutated, shallow copies of this.state).

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

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