© Vasan Subramanian 2019
Vasan SubramanianPro MERN Stackhttps://doi.org/10.1007/978-1-4842-4391-6_4

4. React State

Vasan Subramanian1 
(1)
Bangalore, Karnataka, India
 

Until now, we only saw static components, that is, components that did not change. To make components that respond to user input and other events, React uses a data structure called state in the component. In this chapter, we will explore how to use React State and how it can be manipulated to change how the component looks and what it shows on screen.

The state essentially holds the data, something that can change, as opposed to the immutable properties in the form of props that you saw earlier. This state needs to be used in the render() method to build the view. It is only the change of state that can change the view. When data or the state changes, React automatically rerenders the view to show the new changed data.

For this chapter, the goal is to add a button and append a row to the initial list of issues on click of that button. We’ll add this button in place of the placeholder text in the IssueAdd component. By doing that, you’ll learn about a component’s state, how to manipulate it, how to handle events, and how to communicate between components.

We’ll start by appending a row without user interaction. We’ll do this using a timer rather than a button so that we focus on the state and modifications and not deal with things like user input. Toward the end of the chapter, we will replace the timer with an actual button and a form for user input.

Initial State

The state of a component is captured in a variable called this.state in the component’s class, which should be an object consisting of one or more key-value pairs, where each key is a state variable name and the value is the current value of that variable. React does not specify what needs to go into the state, but it is useful to store in the state anything that affects the rendered view and can change due to any event. These are typically events generated due to user interaction.

For the IssueTable component, the list of issues being displayed is definitely one such piece of data that both affects the rendered view and can also change when an issue is added, edited, or deleted. The array of issues is therefore an ideal state variable.

Other things, such as the size of the window, also can change, but this is not something that affects the DOM. Even though the display changes (for example, a line may wrap because the window is narrower), the change is handled by the browser directly based on the same DOM. So, we don’t need to capture this in the state of the component. There may be cases where it does affect the DOM; for example, if the height of the window determines how many issues we display, we may store the height of the window in a state variable and restrict the number of IssueRow components being constructed. In those cases, the height of the window, or a derived value, for example, the number of issues being shown, could also be stored in the state.

Things that don’t change, for example, the border style of the table, also don’t need to go into the state. That’s because user interaction or other events do not affect the style of borders.

For now, let’s just use an array of issues as the one and only state of the component and use that array to construct the table of issues. Thus, in the render() method of IssueTable, let’s change the loop that creates the set of IssueRows to use the state variable called issues rather than the global array like this:
...
  const issueRows = this.state.issues.map(issue =>
      <IssueRow key={issue.id} issue={issue} />
...
As for the initial state, let’s use a hard-coded set of issues and set it to the initial state. We already have a global array of issues; let’s rename this array to initialIssues, just to make it explicit that it is only an initial set.
...
const initialIssues = [
  ...
];
...
Setting the initial state needs to be done in the constructor of the component. This can be done by simply assigning the variable this.state to the set of state variables and their values. Let’s use the variable initialIssues to initialize the value of the state variable issues like this:
...
    this.state = { issues: initialIssues };
...

Note that we used only one state variable called issues. We can have other state variables, for instance if we were showing the issue list in multiple pages, and we wanted to also keep the page number currently being shown as another state variable, we could have done that by adding another key to the object like page: 0.

The set of all changes to use the state to render the view of IssueTable is shown in Listing 4-1.
...
const issues = [
const initialIssues = [
  {
    id: 1, status: 'New', owner: 'Ravan', effort: 5,
    created: new Date('2018-08-15'), due: undefined,
  },
...
class IssueTable extends React.Component {
  constructor() {
    super();
    this.state = { issues: initialIssues };
  }
  render() {
    const issueRows = issues.map(issue =>
    const issueRows = this.state.issues.map(issue =>
      <IssueRow key={issue.id} issue={issue} />
    );
...
Listing 4-1

App.jsx: Initializing and Using State

Running and testing this piece of code should show no change in the application; you will still see a table containing two rows of issues, just as before.

Exercise: Initial State

  1. 1.

    If you needed to display each row in a different background color based on the status of the issue, how would you go about it? Would you have a list of colors corresponding to each issue also stored in the state? Why or why not?

     

Answers are available at the end of the chapter.

Async State Initialization

Although we set the initial state in the constructor, it is highly unlikely that regular SPA components will have the initial state available to them statically. These will typically be fetched from the server. In the case of the Issue Tracker application, even the initial list issues to be displayed would have to be fetched via an API call.

The state can only be assigned a value in the constructor. After that, the state can be modified, but only via a call to React.Component’s this.setState() method . This method takes in one argument, which is an object containing all the changed state variables and their values. The only state variable that we have is the one called issues, which can be set to any list of issues in a call to this.setState() like this:
...
  this.setState({ issues: newIssues });
...

If there were additional state variables, setting only one variable (issues) will cause it to get merged with the existing state. For example, if we had stored the current page as another state variable, the new value of the state variable issues will be merged into the state, keeping the value of the current page unchanged.

Since at the time of constructing the component, we don’t have the initial data, we will have to assign an empty array to the issues state variable in the constructor.
...
  constructor() {
    this.state = { issues: [] };
...
We will not fetch the data from the server just yet, but to explore the changes to the state initialization, let’s simulate such a call. The key difference between a global array of issues and a call to the server is that the latter needs an asynchronous call. Let’s add a method to the IssueTable class that asynchronously returns an array of issues. Eventually, we’ll replace this with an API call to the server, but for the moment, we’ll use a setTimeout() call to make it asynchronous. In the callback to the setTimeout() call (which will eventually be an Ajax call), let’s call this.setState() with the static array of initial issues like this:
...
  loadData() {
    setTimeout(() => {
      this.setState({ issues: initialIssues });
    }, 500);
  }
...

The timeout value of 500 milliseconds is somewhat arbitrary: it’s reasonable to expect a real API call to fetch the initial list of issues within this time.

Now, it is very tempting to call loadData() within the constructor of IssueTable. It may even seem to work, but the fact is that the constructor only constructs the component (i.e., does all the initialization of the object in memory) and does not render the UI. The rendering happens later, when the component needs to be shown on the screen. If this.setState() gets called before the component is ready to be rendered, things will go awry. You may not see this happening in simple pages, but if the initial page is complex and takes time to render, and if the Ajax call returns before rendering is finished, you will get an error.

React provides many other methods called lifecycle methods to cater to this and other situations where something needs to be done depending on the stage, or changes in the status of the component. Apart from the constructor and the render() methods, the following lifecycle methods of a component could be of interest:
  • componentDidMount() : This method is called as soon as the component’s representation has been converted and inserted into the DOM. A setState() can be called within this method.

  • componentDidUpdate() : This method is invoked immediately after an update occurs, but it is not called for the initial render. this.setState() can be called within this method. The method is also supplied the previous props and previous state as arguments, so that the function has a chance to check the differences between the previous props and state and the current props and state before taking an action.

  • componentWillUnmount() : This method is useful for cleanup such as cancelling timers and pending network requests.

  • shouldComponentUpdate() : This method can be used to optimize and prevent a rerender in case there is a change in the props or state that really doesn’t affect the output or the view. This method is rarely used because, when the state and props are designed well, there will rarely be a case when the state or props change but an update is not required for the view.

The best place to initiate the loading of data in this case is the componentDidMount() method. At this point in time, the DOM is guaranteed to be ready, and setState() can be called to rerender the component. componentDidUpdate() is an option as well, but since it may not be called for the initial render, let’s not use it. Let’s add the componentDidMount() method in IssueTable and load the data within this method:
...
  componentDidMount() {
    this.loadData();
  }
...
The complete set of changes in the IssueTable class is shown in Listing 4-2.
...
class IssueTable extends React.Component {
  constructor() {
    super();
    this.state = { issues: initialIssues };
    this.state = { issues: [] };
  }
  componentDidMount() {
    this.loadData();
  }
  loadData() {
    setTimeout(() => {
      this.setState({ issues: initialIssues });
    }, 500);
  }
...
Listing 4-2

App.jsx, IssueTable: Loading State Asynchronously

If you refresh the browser (assuming you’re still running npm run watch and npm start on two different consoles), you will find that the list of issues is displayed as it used to be in the previous steps. But, you will also see that for a fraction of a second after the page is loaded, the table is empty, as shown in Figure 4-1.
../images/426054_2_En_4_Chapter/426054_2_En_4_Fig1_HTML.jpg
Figure 4-1

Empty table shown for a fraction of a second

It gets filled soon after, but still, there is a flicker. When we explore server-side rendering in later chapters, we will get rid of this ungainly flicker. For the moment, let’s live with this minor UI unpleasantness.

Updating State

In the previous sections, you saw how to set the initial state, using a direct assignment in the constructor as well as setting a value in other lifecycle methods using this.setState(). In this section, let’s make a minor change to the state rather than set a completely new value to it. Let’s add a new issue and thus change, not the complete state, but only a portion of it.

To start, let’s add a method in IssueTable to add a new issue. This can take in as an argument an issue object, to which we’ll assign a new ID and set the creation date. The new ID can be calculated from the existing length of the array.
...
  createIssue(issue) {
    issue.id = this.state.issues.length + 1;
    issue.created = new Date();
  }
...
Note that the state variable cannot be set directly, nor can it be mutated directly. That is, setting this.state.issues to a new value or modifying its elements is not allowed. The variable this.state in the component should always be treated as immutable. For example, the following should not be done:
...
    this.state.issues.push(issue);    // incorrect!
...
The reason is that React does not automatically identify such changes to the state because it is a plain JavaScript variable. The only way to let React know something has changed, and to cause a rerender, is to call this.setState() . Further, this.setState() may cause the changes that are done directly to the state variable to be overwritten. So, the following should not be done either:
...
    issues = this.state.issues;
    issues.push(issue);         // same as this.state.issues.push()!
    this.setState({ issues: issues });
...

It may seem to work, but it will have unexpected consequences in some of the lifecycle methods within this as well as descendent components. Especially in those methods that compare the old and new properties, the difference between the old state and the new one will not be detected.

What is needed in the setState() call is a fresh array of issues, say a copy of the state variable. If any existing array element, say an issue itself, is changing, not only is the copy of the array needed, but also the copy of the object that is being changed is needed. There are libraries called immutability helpers, such as immutable.js ( http://facebook.github.io/immutable-js/ ), which can be used to construct the new state object. When a property of the object is modified, the library creates a copy optimally.

But we will only append an issue, and not change an existing issue. It’s fairly straightforward to make a shallow copy of the array, and this will suffice for the moment. So, we won’t be using the library—there isn’t much extra code we need to write to handle it. If, in your application, you find that you have to make lots of copies because of deep nesting of objects in the state, you could consider using immutable.js.

The simple way to make a copy of an array is using the slice() method . So let’s create a copy of the issues array like this:
...
    issues = this.state.issues.slice();
...
Later in the chapter, we will create a user interface to add a new issue. But for now, rather than dealing with the complexity of UI and event handling, let’s just add a timer, on the expiry of which, a hard-coded sample issue will be appended to the list of issues. Let’s first declare this hard-coded sample issue object globally, right after the global initialIssues:
...
const sampleIssue = {
  status: 'New', owner: 'Pieta',
  title: 'Completion date should be optional',
};
...
Let’s use this object in a call to createIssue() , after the expiry of a timer of two seconds, in the constructor of IssueTable:
...
    setTimeout(() => {
      this.createIssue(sampleIssue);
    }, 2000);
...
This should automatically add the sample issue to the list of issues after the page is loaded. The final set of changes—for using a timer to append a sample issue to the list of issues—is shown in Listing 4-3.
...
const initialIssues = [
  ...
];
const sampleIssue = {
  status: 'New', owner: 'Pieta',
  title: 'Completion date should be optional',
};
...
class IssueTable extends React.Component {
  constructor() {
    super();
    this.state = { issues: [] };
    setTimeout(() => {
      this.createIssue(sampleIssue);
    }, 2000);
  }
  ...
  createIssue(issue) {
    issue.id = this.state.issues.length + 1;
    issue.created = new Date();
    const newIssueList = this.state.issues.slice();
    newIssueList.push(issue);
    this.setState({ issues: newIssueList });
  }
}
...
Listing 4-3

App.jsx: Appending an Issue on a Timer

On running this set of changes and refreshing the browser, you’ll see that there are two rows of issues to start with. After two seconds, a third row is added with a newly generated ID and the contents of the sample issue. A screenshot of the three-row table is shown in Figure 4-2.
../images/426054_2_En_4_Chapter/426054_2_En_4_Fig2_HTML.jpg
Figure 4-2

Appended row to initial set of issues

Note that we did not explicitly call a setState() on the IssueRow components. React automatically propagates any changes to child components that depend on the parent component’s state. Further, we did not have to write any code for inserting a row into the DOM. React calculated the changes to the virtual DOM and inserted a new row.

At this point, the hierarchy of the components and the data flow can be visually depicted, as shown in Figure 4-3.
../images/426054_2_En_4_Chapter/426054_2_En_4_Fig3_HTML.jpg
Figure 4-3

Setting state and passing data as props

Exercise: Updating State

  1. 1.

    Set up another timer at say, three seconds, right after the first timer to add yet another issue based on sampleIssue. Do you notice something going wrong when the second new issue is added? Hint: Look at the ID of the first new issue. Why do you think this is happening? How can you correct it?

     
  2. 2.

    Add a console.log in the IssueRow’s render() method. How many times do you expect render() to be called? How many console logs do you see? (Make sure you undo the changes you did in the previous exercise!)

     

Answers are available at the end of the chapter.

Lifting State Up

Before we add user interface elements to create new issues, let’s move the initiation of the creation to where it really belongs: in the IssueAdd component. This will allow us to deal with the changes one step at a time, because moving the timer for adding a new issue from the IssueTable component to the IssueAdd component is not as trivial as it first appears.

If you do try to move it, you will immediately realize that the createIssue() method will also have to move, or we need to have a variant within IssueAdd that can communicate back to IssueTable and call the createIssue() method, which continues to remain there. But there is no straightforward way to communicate between siblings in React. Only parents can pass information down to children; horizontal communication seems hard, if not impossible.

The way around this is to have the common parent contain the state and all the methods that deal with this state. By lifting the state up on level to IssueList, information can be propagated down to IssueAdd as well as to IssueTable.

Let’s start by moving the state to IssueList and the methods to load the initial state. The constructor of IssueTable had both the state initialization as well as the timer, of which only the state initialization needs to move (the timer will move to IssueAdd):
...
class IssueList extends React.Component {
  constructor() {
    super();
    this.state = { issues: [] };
  }
...
The other methods that deal with the state are componentDidMount(), loadData(), and createIssue(). Let’s move these also to the IssueList class:
...
class IssueList extends React.Component {
  ...
  componentDidMount() {
    ...
  }
  loadData() {
    ...
  }
  createIssue(issue) {
    ...
  }
  ...
}
Now, IssueTable doesn’t have a state to construct the IssueRow components from. But you have already seen how data can be passed in from a parent to a child in the form of props. Let’s use that strategy and pass the array of issues from the state within IssueList to IssueTable via props:
...
        <IssueTable issues={this.state.issues} />
...
And, within IssueTable, instead of referring to the state variable issues, we’ll need to get the same data from props:
...
    const issueRows = this.state.issues.map(issue =>
    const issueRows = this.props.issues.map(issue =>
...
As for IssueAdd, we need to move the timer into the constructor of this class and trigger the addition of a new issue from within this component. But we don’t have the createIssue() method available here. Fortunately, since a parent component can pass information down to a child component, we’ll pass the method itself as part of the props to IssueAdd from IssueList, so that it can be called from IssueAdd. Here’s the changed instantiation of the IssueAdd component within IssueList:
...
        <IssueAdd createIssue={this.createIssue} />
...
This lets us make a call to createIssue() from IssueAdd using this.props.createIssue() as part of the timer callback. So let’s create a constructor in IssueAdd and move the timer set up with a minor change to use the createIssue callback passed in via props like this:
...
    setTimeout(() => {
      this.props.createIssue(sampleIssue);
    }, 2000);
...

We still have to take care of one more thing before we can say we are done with this set of changes. All this while, we have been using the arrow function syntax to set up timers. In ES2015, the arrow function has the effect of setting the context (the value of this) to the lexical scope. This means that this within the callback will refer to whatever this was in the lexical scope, that is, outside of the anonymous function, where the code is present.

That worked as long as the called function was within the same class as the timer callback. It still works, in the loadData() method, because this refers to the IssueList component where the timer was fired, and therefore, this.state refers to the state within IssueList itself.

But, when createIssue is called from a timer within IssueAdd, this will refer to the IssueAdd component. But what we really want is for createIssue to be always called with this referring to the IssueList component. Otherwise, this.state.issues will be undefined.

The way to make this work is to bind the method to the IssueList component before passing it around. We could make this change when instantiating IssueAdd like this:
...
        <IssueAdd createIssue={this.createIssue.bind(this)} />
...

But then, if we need to ever refer to the same method again and pass it to some other child component, we’d have to repeat this code. Also, there is never going to be a case where we will need the method to be not bound, so it is best to replace the definition of createIssue with a bound version of itself. The recommended way to do this is in the constructor of the class where this method is implemented.

So, instead of binding during the instantiation of IssueAdd, let’s bind it in the constructor of IssueList .
...
    this.createIssue = this.createIssue.bind(this);
...
The new versions of each of these classes, after making all these changes, are shown in the following listings. Listing 4-4 shows the new IssueTable class; Listing 4-5 shows the new IssueAdd class; and Listing 4-6 shows the new IssueList class.
class IssueTable extends React.Component {
  render() {
    const issueRows = this.props.issues.map(issue =>
      <IssueRow key={issue.id} issue={issue} />
    );
    return (
      <table className="bordered-table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Status</th>
            <th>Owner</th>
            <th>Created</th>
            <th>Effort</th>
            <th>Due Date</th>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          {issueRows}
        </tbody>
      </table>
    );
  }
}
Listing 4-4

App.jsx: New IssueTable Class

class IssueAdd extends React.Component {
  constructor() {
    super();
    setTimeout(() => {
      this.props.createIssue(sampleIssue);
    }, 2000);
  }
  render() {
    return (
      <div>This is a placeholder for a form to add an issue.</div>
    );
  }
}
Listing 4-5

App.jsx, IssueAdd: New IssueAdd Class

class IssueList extends React.Component {
  constructor() {
    super();
    this.state = { issues: [] };
    this.createIssue = this.createIssue.bind(this);
  }
  componentDidMount() {
    this.loadData();
  }
  loadData() {
    setTimeout(() => {
      this.setState({ issues: initialIssues });
    }, 500);
  }
  createIssue(issue) {
    issue.id = this.state.issues.length + 1;
    issue.created = new Date();
    const newIssueList = this.state.issues.slice();
    newIssueList.push(issue);
    this.setState({ issues: newIssueList });
  }
  render() {
    return (
      <React.Fragment>
        <h1>Issue Tracker</h1>
        <IssueFilter />
        <hr />
        <IssueTable issues={this.state.issues} />
        <hr />
        <IssueAdd createIssue={this.createIssue} />
      </React.Fragment>
    );
  }
}
Listing 4-6

App.jsx, IssueList: New IssueList Class

The effect of these changes will not be seen in the user interface. The application will behave as it used to. On refreshing the browser, you will see an empty table to start with, which will soon be populated with two issues and after two seconds, another issue will be added.

But this sets us up nicely for the change where we can replace the timer in IssueAdd with a button that the user can click to add a new issue.

Exercise: Lifting State Up

  1. 1.

    Remove the binding of the method createIssue(). What error do you see in the console? What does it tell you?

     

Answers are available at the end of the chapter.

Event Handling

Let’s now add an issue interactively, on the click of a button rather than use a timer to do this. We’ll create a form with two text inputs and use the values that the user enters in them to add a new issue. An Add button will trigger the addition.

Let’s start by creating the form with two text inputs in the render() method of IssueAdd in place of the placeholder div.
...
      <div>This is a placeholder for a form to add an issue.</div>
      <form>
        <input type="text" name="owner" placeholder="Owner" />
        <input type="text" name="title" placeholder="Title" />
        <button>Add</button>
      </form>
...
At this point, we can remove the timer that creates an issue from the constructor.
...
  constructor() {
    super();
    setTimeout(() => {
      this.props.createIssue(sampleIssue);
    }, 2000);
  }
...
If you run the code, you’ll see a form being displayed in place of the placeholder in IssueAdd. The screenshot of how this looks is shown in Figure 4-4.
../images/426054_2_En_4_Chapter/426054_2_En_4_Fig4_HTML.jpg
Figure 4-4

IssueAdd placeholder replaced with a form

At this point, clicking Add will submit the form and fetch the same screen again. That’s not what we want. Firstly, we want it to call createIssue() using the values in the owner and title fields. Secondly, we want to prevent the form from being submitted because we will handle the event ourselves.

To handle events such as onclick and onsubmit, the properties that we need to supply to the elements are, simply, onClick and onSubmit. As in plain HTML and JavaScript, these properties take functions as values. We’ll create a class method called handleSubmit() to receive the submit event from the form when the Add button is clicked. Within this method, we’ll need a handle to the form, so as in regular HTML, let’s give the form a name, say, issueAdd which can then be referred to in JavaScript using document.forms.issueAdd.

So, let’s rewrite the form declaration with a name and an onSubmit handler like this.
...
            <form name="issueAdd" onSubmit={this.handleSubmit}>
...
Now, we can implement the method handleSubmit() in IssueAdd. This method receives the event that triggered the submit as an argument. In order to prevent the form from being submitted when the Add button is clicked, we need to call the preventDefault() function on the event. Then, using the form handle via documents.forms.issueAdd, we can get the values of the text input fields. Using these, we’ll create a new issue by calling createIssue(). After the call to createIssue(), let’s keep the form ready for the next set of inputs by clearing the text input fields.
...
  handleSubmit(e) {
    e.preventDefault();
    const form = document.forms.issueAdd;
    const issue = {
      owner: form.owner.value, title: form.title.value, status: 'New',
    }
    this.props.createIssue(issue);
    form.owner.value = ""; form.title.value = "";
  }
...

Note

At this point, we are using the conventional way of taking user input, using named inputs and getting their value using the value property of the DOM element. React has another way of dealing with user input by way of controlled components, where the value of the input is tied to a state variable. We’ll explore this in later chapters.

Since handleSubmit will be called from an event, the context, or this will be set to the object generating the event, which is typically the window object. As you saw in the previous section, to let this method have access to the object variables via this, we need to bind it to this in the constructor:
...
  constructor() {
    super();
    this.handleSubmit = this.handleSubmit.bind(this);
  }
...
The new full code of the IssueAdd class , after these changes, is shown in Listing 4-7.
class IssueAdd extends React.Component {
  constructor() {
    super();
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleSubmit(e) {
    e.preventDefault();
    const form = document.forms.issueAdd;
    const issue = {
      owner: form.owner.value, title: form.title.value, status: 'New',
    }
    this.props.createIssue(issue);
    form.owner.value = ""; form.title.value = "";
  }
  render() {
    return (
      <form name="issueAdd" onSubmit={this.handleSubmit}>
        <input type="text" name="owner" placeholder="Owner" />
        <input type="text" name="title" placeholder="Title" />
        <button>Add</button>
      </form>
    );
  }
}
Listing 4-7

App.jsx, IssueList: New IssueAdd Class

The global object sampleIssue is no longer required, so we can get rid of it. This change is shown in Listing 4-8.
...
const sampleIssue = {
  status: 'New', owner: 'Pieta',
  title: 'Completion date should be optional',
};
...
Listing 4-8

App.jsx, Removal of sampleIssue

You can now test the changes by entering some values in the owner and title fields and clicking Add. You can add as many rows as you like. If you add two issues, you’ll get a screen like the one in Figure 4-5.
../images/426054_2_En_4_Chapter/426054_2_En_4_Fig5_HTML.jpg
Figure 4-5

Adding new issues using the IssueAdd form

At the end of all this, we have been able to encapsulate and initiate the creation of a new issue from the IssueAdd component itself. To do this, we “lifted the state up” to the least common ancestor, so that all children have access to it directly via passed-in props or via callbacks that can modify the state. This new UI hierarchy data and function flow is depicted in Figure 4-6. Compare this with the situation where the state was maintained in IssueTable, as in Figure 4-3.
../images/426054_2_En_4_Chapter/426054_2_En_4_Fig6_HTML.jpg
Figure 4-6

Component hierarchy and data flow after lifting state up

Exercise: Event Handling

  1. 1.

    Refresh the browser; you’ll see that the added issues are gone. How does one persist the changes?

     
  2. 2.

    Remove e.preventDefault(). Click the Add button with some values for owner and title. What happens? What do you see in the URL bar? Can you explain this?

     
  3. 3.

    Use the developer console to inspect the table and add a breakpoint on the <tbody> element as “break on subtree modification”. Now, add a new issue. How many times is the subtree being modified? Compare this with exercise #2 in “Updating State,” where you traced the number of render() calls in a IssueRow.

     

Answers are available at the end of the chapter.

Stateless Components

We have three functioning React components (IssueAdd, IssueRow and IssueTable) composed hierarchically into IssueList (another one, the IssueFilter, is still a placeholder). But there is a difference among these functioning component classes.

IssueList has lots of methods, a state, initialization of the state, and functions that modify the state. In comparison, IssueAdd has some interactivity, but no state1. But, if you notice, IssueRow and IssueTable have nothing but a render() method. For performance reasons and for clarity of code, it is recommended that such components are written as functions rather than classes: a function that takes in props and just renders based on it. It’s as if the component’s view is a pure function of its props, and it is stateless. The render() function itself can be the component.

If a component does not depend on props, it can be written as a simple function whose name is the component name. For example, consider the following Hello World class we wrote in the beginning of Chapter 2 (React Components):
...
class HelloWorld extends React.Component {
  render() {
    return (
      <div title="Outer div">
        <h1>Hello World!</h1>
      </div>
    );
  }
}
...
This can be rewritten as a pure function like this:
...
function HelloWorld() {
  return (
    <div title="Outer div">
      <h1>Hello World!</h1>
    </div>
  );
}
...
If the rendering depends on the props alone (more often than not, this will indeed be the case), the function can be written with one argument as the props, which can be accessed within the function’s JSX body. Say the Hello World component takes in a message as part of the props. The component can be rewritten as follows:
...
function HelloWorld(props) {
  return (
    <div title="Outer div">
      <h1>{props.message}</h1>
    </div>
  );
}
...
An even more concise form using an arrow function can be used when the rendered output can be represented as a JavaScript expression, that is, a function with no other statement than just the return statement:
...
const HelloWorld = (props) => (
  <div title="Outer div">
    <h1>{props.message}</h1>
  </div>
);
...
This HelloWorld component could have been instantiated like this:
...
  <HelloWorld message="Hello World" />
...
Since IssueRow and IssueTable are stateless components, let’s change them to pure functions. The new components are shown in Listing 4-9 and Listing 4-10, respectively.
function IssueRow(props) {
  const issue = props.issue;
  return (
    <tr>
      <td>{issue.id}</td>
      <td>{issue.status}</td>
      <td>{issue.owner}</td>
      <td>{issue.created.toDateString()}</td>
      <td>{issue.effort}</td>
      <td>{issue.due ? issue.due.toDateString() : ''}</td>
      <td>{issue.title}</td>
    </tr>
  );
}
Listing 4-9

App.jsx, IssueRow as a Stateless Component

function IssueTable(props) {
  const issueRows = props.issues.map(issue =>
    <IssueRow key={issue.id} issue={issue} />
  );
  return (
    <table className="bordered-table">
      <thead>
        <tr>
          <th>ID</th>
          <th>Status</th>
          <th>Owner</th>
          <th>Created</th>
          <th>Effort</th>
          <th>Due Date</th>
          <th>Title</th>
        </tr>
      </thead>
      <tbody>
        {issueRows}
      </tbody>
    </table>
  );
}
Listing 4-10

App.jsx, IssueTable as a Stateless Component

Designing Components

Most beginners will have a bit of confusion between state and props, when to use which, what granularity of components should one choose, and how to go about it all. This section is devoted to discussing some principles and best practices.

State vs. Props

Both state and props hold model information, but they are different. The props are immutable, whereas state is not. Typically, state variables are passed down to child components as props because the children don’t maintain or modify them. They take in a read-only copy and use it only to render the view of the component. If any event in the child affects the parent’s state, the child calls a method defined in the parent. Access to this method should have been explicitly given by passing it as a callback via props.

Anything that can change due to an event anywhere in the component hierarchy qualifies as being part of the state. Avoid keeping computed values in the state; instead, simply compute them when needed, typically inside the render() method.

Do not copy props into state, just because props are immutable. If you feel the need to do this, think of modifying the original state from which these props were derived. One exception is when props are used as initial values to the state, and the state is truly disjointed from the original state after the initialization.

You can use Table 4-1 as a quick reference to the differences.
Table 4-1

State vs. Props

Attribute

State

Props

Mutability

Can be changed using this.setState()

Cannot be changed

Ownership

Belongs to the component

Belongs to an ancestor, the component gets a read-only copy

Information

Model information

Model information

Affects

Rendering of the component

Rendering of the component

Component Hierarchy

Split the application into components and subcomponents. Typically, this will reflect the data model itself. For example, in the Issue Tracker, the issues array was represented by the IssueTable component, and each issue was represented by the IssueRow component.

Decide on the granularity just as you would for splitting functions and objects. The component should be self-contained with minimal and logical interfaces to the parent. If you find it doing too many things, just like in functions, it should probably be split into multiple components, so that it follows the Single Responsibility principle (that is, every component should be responsible for one and only one thing). If you are passing in too many props to a component, it is an indication that either the component needs to be split, or it need not exist: the parent itself could do the job.

Communication

Communication between components depends on the direction. Parents communicate to children via props; when state changes, the props automatically change. Children communicate to parents via callbacks.

Siblings and cousins can’t communicate with each other, so if there is a need, the information has to go up the hierarchy and then back down. This is called lifting the state up. This is what we did when we dealt with adding a new issue. The IssueAdd component had to insert a row in IssueTable. It was achieved by keeping the state in the least common ancestor, IssueList. The addition was initiated by IssueAdd and a new array element added in IssueList’s state via a callback. The result was seen in IssueTable by passing the issues array down as props from IssueList.

If there is a need to know the state of a child in a parent, you’re probably doing it wrong. Although React does offer a way using refs, you shouldn’t feel the need if you follow the one-way data flow strictly: state flows as props into children, events cause state changes, which flows back as props.

Stateless Components

In a well-designed application, most components would be stateless functions of their properties. All states would be captured in a few components at the top of the hierarchy, from where the props of all the descendants are derived.

We did just that with the IssueList, where we kept the state. We converted all descendent components to stateless components, relying only on props passed down the hierarchy to render themselves. We kept the state in IssueList because that was the least common component above all the descendants that depended on that state. Sometimes, you may find that there is no logical common ancestor. In such cases, you may have to invent a new component just to hold the state, even though visually the component has nothing.

Summary

In this chapter, you learned how to use state and make changes to it on user interactions or other events. The more interesting aspect was how state values are propagated down the component hierarchy as props. You also had a glimpse of user interaction: the click of a button to add a new issue, and how that causes the state to change, and in turn, how the props in the descendant components changed, causing them to rerender as well. Further, you learned how a child can communicate with its parent via callbacks.

We used simulated asynchronous calls and data local to the browser to achieve all this. In the next chapter, instead of using local data, we’ll fetch the data from the server. When an issue is added, we’ll send the data to the server to persist it.

Answers to Exercises

Exercise: Initial State

  1. 1.

    You could store the background color of each row as part of the state, but then, the values will have to be calculated at some point in time. When is a good time to do this? Just before setting the state? How about when setting the initial state?

     

Since this is a derived value, it is better and more efficient to just calculate these values within the render() method and use them then and there rather than save them in the state.

Exercise: Updating State

  1. 1.

    When the second timer fires and another issue is added, you will find that it gets an ID of 4 but the ID of the third row also changes to 4. Further, in the console, you will see an error to the effect that two children with the same key were found.

    This happens because we are using the same object as the first to create the second issue, and setting the ID to 4 sets it in the one and only object: sampleIssue. To avoid this, you have to create a copy of the object before using it to create a new issue, say, using Object.assign().

     
  2. 2.

    Each row is rendered once when initialized (two renders, one for each row). After the new row is inserted, each row is rendered once again (three renders, one for each row). Although a render is called, this does not mean that the DOM is updated. Only the virtual DOM is recreated on each render. Real DOM update happens only where there are differences.

     

Exercise: Lifting State Up

  1. 1.

    On removing the bind() call, you’ll see an error that says undefined is not an object, while evaluating this.state.issues. This should tell you that this.state is undefined, and lead you to think about whether this is the correct this in this call sequence.

    In future, if you see a similar error, it should trigger off a thought that maybe a bind() call is missing somewhere.

     

Exercise: Event Handling

  1. 1.

    To persist the changes, we could either save the issues in local storage on the browser, or save it in the server. Modifying the global initialIssues variable will not work because when the page is refreshed, this variable is recreated.

     
  2. 2.

    The page is refreshed as if a new request to / has been made. In the URL bar, you can see URL query parameters for owner and title like ?owner=&title=. This is because the default action of a form is a GET HTTP request with the values of the form, and what you see in the URL bar is just the result of this call. (The values in the URL parameters are blank because they were assigned to empty strings in handleSubmit()).

     
  3. 3.

    You will see that the subtree under <tbody> is being modified only once. In the details of the modification, you can see that a child is being added, but none of the other existing children are being modified. If you compare it with the number of render() method calls, you will find that even though render() was being called for each row, only the new row is being added to the DOM.

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

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