React Components

Dr. Greg Bernstein

Updated September 17th, 2021

React Class Components

Learning Objectives

  • Why and where to keep state in React
  • How to properly set state in React for values, arrays, and objects
  • Learn how to use common events with React
  • How to deal with React,this, and JavaScript bind()
  • Learn React “data flows down” (with props) and up with function calls

Readings

Why?

  • Class components let us define components with internal state

  • Class components let our components participate/extend the components life cycle.

Example Quiz-O-Matic

Three planned stages of application use:

  • Welcome message and instructions portion
  • Testing portion: give them questions and record inputs
  • Results/summary portion

Break into Components

Components files:

  • Intro component, file: intro.js
  • Quiz component, file: questions.js
  • Summary component, file: summary.js

Problems

  • How do we know what component to show?
  • Where do we keep the user name?
  • Where do we track their quiz score

Add State!

  • Where is the user in the app: (intro, quiz, summary)
  • Question list
  • User name, Score, etc…

React Class Component

Use a JavaScript object derived from React.Component to keep track of state.

class MyComponent extends React.Component {
    constructor(props) {
        super(props); // Must call
        // Set up state here
    }

    // Renders component based on current state and props
    render() {
        // Any code you like
        return // Must return a JSX element here
    }
}

Quiz-O-Matic App Component

examples branch classComp1

class QuizOMatic extends React.Component {
    constructor(props) {
        super(props); // Must call
        // a member variable called "state" to hold the state as a JS object
        this.state = {show: "intro", questions: questions,
            user: "CS351", score: "0/0", minutes: 12};
    }

    // Renders component based on current state and props
    render() {
        let contents = null;
        switch (this.state.show) {
            case "intro":
                contents = <Intro user={this.state.user}/>;
                break;
            case "quiz":
                contents = <Quiz questions={this.state.questions}/>;
                break;
            case "summary":
                contents = <Summary user={this.state.user} score={this.state.score}
                            minutes={this.state.minutes}/>;
                break;
            default:
                contents = <h2>Warning something went wrong!!!</h2>;
        }

        return <div><h1>Quiz-O-Matic</h1>
          {contents}
        </div>
    }
}

React Dev Tools

Both of these React developer tool add-ons allow you to inspect components state and properties and modify them!

Quiz-O-Matic intro

Initial state show: "intro"

Intro

Quiz-O-Matic summary

Use dev tool to change state show: "quiz"

Summary

What Happened?

Important!

When a class components state is properly changed React will call its render() method to update the component

Setting React State Correctly

  • “Do Not Modify State Directly”, always use the setState() method.

  • “State Updates are Merged”. Okay to call setState() from different places with different pieces of the state.

When to change state

  • User interactions
  • Network interactions (receiving data)
  • Timer or other updates

Simple Interactivity

Buttons

Let’s change state via a button bar

Buttons

Adding a Button Bar

Just adds some buttons! Branch: classComp2

render() {
  let contents = null;
  let buttonBar = <div className="buttonBar">
    <button>Intro</button>
    <button>Quiz Me</button>
    <button>Results</button>
  </div>;

  switch (this.state.show) {
    case "intro":
      contents = <Intro user={this.state.user}/>;
      break;
    case "quiz":
      contents = <Quiz questions={this.state.questions}/>;
      break;
    case "summary":
      contents = <Summary user={this.state.user} score={this.state.score}
                          minutes={this.state.minutes}/>;
      break;
    default:
      contents = <h2>Warning something went wrong!!!</h2>;
  }
  return <div>
    {buttonBar}
    <main>
      <h1>Quiz-O-Matic</h1>
      {contents}
    </main>
  </div>
}

Event Handling General

From MDN on-event handlers: “The Web platform provides several ways to get notified of DOM events.”

  • HTML Based Approach:
<button onclick="return handleClick(event);">

Event Handling via JavaScript

  • “The on-event handlers are a group of properties offered by DOM elements to help manage how that element reacts to events.”
document.getElementById("mybutton").onclick = function(event) { ... }
  • The addEventListener() interface is the most general and flexible approach

React Event Handling

From React: Handling Events:

  • JSX syntax:
<button onClick={activateLasers}>
  Activate Lasers
</button>
  • where activateLasers is a callback function.

Adding Event Handling 1

Defining event handlers in the class component:

class QuizOMatic extends React.Component {
  constructor(props) {
    super(props); // Must call
    this.state = {
      show: "intro", questions: questions,
      user: "CS351", score: "0/0", minutes: 12
    };
  }

  introHandler(event) {
    console.log(event);
    console.log(event.target)
    console.log("You pressed a button");
    this.setState({show: "intro"});
  }

  quizHandler(event) {
    this.setState({show: "quiz"});
  }

  resultHandler(event) {
    this.setState({show: "summary"});
  }

  render() {...}
}

setState(...)

  • The setState(...) member function is defined by React in the React.Component class.

  • It’s purpose is to let React know that the state has been changed so it can update the DOM.

  • Not using setState(...) to change the state will break React. The only place we do not use setState(...) is when we define the initial state in the constructor.

Adding Event Handling 2

Need to be careful with this:

class QuizOMatic extends React.Component {
// constructor and event handlers not shown
  render() {
    let contents = null;
    // Notice the syntax below
    let buttonBar = <div className="buttonBar">
      <button onClick={this.introHandler.bind(this)}>Intro</button>
      <button onClick={this.quizHandler.bind(this)}>Quiz Me</button>
      <button onClick={this.resultHandler.bind(this)}>Results</button>
    </div>;

    switch (this.state.show) {
      case "intro":
        contents = <Intro user={this.state.user}/>;
        break;
      case "quiz":
        contents = <Quiz questions={this.state.questions}/>;
        break;
      case "summary":
        contents = <Summary user={this.state.user} score={this.state.score}
                            minutes={this.state.minutes}/>;
        break;
      default:
        contents = <h2>Warning something went wrong!!!</h2>;
    }
    return <div>
      {buttonBar}
      <main>
        <h1>Quiz-O-Matic</h1>
        {contents}
      </main>
    </div>
  }
}

Adding Event Handling 3

Try it without the bind call:

class QuizOMatic extends React.Component {
// constructor and event handlers not shown
  render() {
    let contents = null;
    // Notice the syntax below
    let buttonBar = <div className="buttonBar">
      <button onClick={this.introHandler}>Intro</button>
      <button onClick={this.quizHandler}>Quiz Me</button>
      <button onClick={this.resultHandler}>Results</button>
    </div>;

    switch (this.state.show) {
      case "intro":
        contents = <Intro user={this.state.user}/>;
        break;
      case "quiz":
        contents = <Quiz questions={this.state.questions}/>;
        break;
      case "summary":
        contents = <Summary user={this.state.user} score={this.state.score}
                            minutes={this.state.minutes}/>;
        break;
      default:
        contents = <h2>Warning something went wrong!!!</h2>;
    }
    return <div>
      {buttonBar}
      <main>
        <h1>Quiz-O-Matic</h1>
        {contents}
      </main>
    </div>
  }
}

this Problem

The mysterious undefined this error:

This undefined

Bind()

Function.prototype.bind()

From MDN bind()

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

  • Syntax: function.bind(thisArg[, arg1[, arg2[, ...]]])
  • Where thisArg is the this you want to bind and arg1, arg2, etc are any other arguments you want to bind.

bind() Example 1

From MDN bind()

var module = {x: 42,   getX: function() {return this.x;}}
var unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined
var boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

bind() Formal Definition

The bind() function creates a new bound function (BF). A BF is an exotic function object (a term from ECMAScript 2015) that wraps the original function object. Calling a BF generally results in the execution of its wrapped function.

bind() Example without this

Just use null where the thisArg would go:

function addem(x, y) {return x + y};
var add3 = addem.bind(null, 3);
console.log(addem(2, 7));
console.log(add3(7));

Creating Lots of Functions 1

With Bind

users = ["Dr. B", "Student X", "Student Y"];
function sayHi(name) {
  alert("Hi " + name);
}
// Using array destructuring assignment below
[hiDrB, hiX, hiY] = users.map(x => sayHi.bind(null, x));
// Try calling them
hiDrB();
hiX();
hiY();

Creating Lots of Functions 2

Without Bind (closure)

users = ["Dr. B", "Student X", "Student Y"];
function sayHi(name) {
  alert("Hi " + name);
}
// A function that returns a function (closure!)
function hiFunc(s) {
  return function() {
    sayHi(s);
  }
}
// Using array destructuring assignment below
[hiDrB, hiX, hiY] = users.map(x => hiFunc(x));
// Try calling them
hiDrB();
hiX();
hiY();

Bind Takeaways

  • We can use bind to produce “new” functions with fewer arguments from a function with more arguments by “fixing” some of those arguments with bind.
  • Alternatively and more generally we can use a function returning a function.
  • bind is the primary way for us to set this for a function in JavaScript

Setting React State

Properly Setting React State

  • Give React a “new thingy” rather than changing a “sub part” of that “thingy” and giving it back to React.

  • Easy for a property that is a single value:

// Properly updating state of the *show* property
this.setState({show: "quiz"});

Quiz-O-Matic State

  • questions property is an array
  • user though a string would most likely be a JS object with a bunch of user info.
this.state = {
  show: "intro", questions: questions,
  user: "CS351", score: "0/0", minutes: 12
};

Updating a State Array: Wrong

Suppose we allow users to add questions to the quiz.

Straightforward and wrong approach:

// Wrong modifying questions without setState
this.state.questions.push({question:"What does CSS mean", choices: []});
// Wrong: looks like the same object as before React won't see change
this.setState({questions: this.state.questions})

Updating a State Array: Right

We need to give React a new array but we want to be efficient

Use Array.concat as it returns a new array instance:

// Creates a new array with the additional question
let newQs = this.state.questions.concat({question:"What does CSS mean", choices: []})
// newQs is a different array from state.questions so React will update
this.setState({questions: newQs});

Updating State Arrays

Use JS Array methods that return new arrays:

  • Removing elements from a state array: Use the Array.filter method.

  • Updating elements in a state array: Use the Array.map method.

  • Remember that both filter and map callbacks receive current value, index, and the entire array as parameters.

Updating State Objects 1

Suppose you need to change part of a state property that is a JS Object

this.state = {
  show: "intro", questions: questions,
  user: {first: "CS", last: "351", netid: "tb1524", inClass: true},
  score: "0/0", minutes: 12
};

Updating State Objects 2

  • Use Object.assign({},...) to create a copy of the object
  • Modify the copy, use it in setState()
let newUser = Object.assign({}, this.state.user);
// Modify newUser any way you like
this.setState({user: newUser}); //React will see changes

React Data Flows

Data Flows Down 1

From The Data Flows Down

  • Neither parent nor child components can know if a certain component is stateful or stateless, and they shouldn’t care whether it is defined as a function or a class.

  • This is why state is often called local or encapsulated. It is not accessible to any component other than the one that owns and sets it.

Data Flows Down 2

From The Data Flows Down

  • A component may choose to pass its state down as props to its child components

  • This is commonly called a “unidirectional” data flow. Any state is always owned by some specific component, and any data or UI derived from that state can only affect components “below” them in the tree.

Changing state in a parent component 1

  • You cannot write to props they should be treated as read-only!

  • “Data flows down” is the theme, what can we do?

  • Why would you want to do that?

Quiz-O-Matic Buttons

  • We put the buttons to change state in a tool bar that shows up all the time.
  • Couldn’t we put the “result/grade” button only on the quiz section?
  • Couldn’t we put a button on the “result” section to take another quiz?

Updated UI 1

See branch classComp3

New GUI 1

Updated UI 2

See branch classComp3

New GUI 2

Updated UI 3

See branch classComp3

New GUI 3

Pass a function down!

  • We already have event handler functions defined to switch states

  • Pass the appropriate functions down to child components via properties

  • So “data flows down” but functions can call up!

Passing Functions Down

Updated QuizOMatic component render:

render() {
  let contents = null;
  let buttonBar = <div className="buttonBar">
    <button onClick={this.introHandler.bind(this)}>About</button>
  </div>;

  switch (this.state.show) {
    case "intro":
      contents = <Intro user={this.state.user} takeQuiz={this.quizHandler.bind(this)}/>;
      break;
    case "quiz":
      contents = <Quiz questions={this.state.questions} gradeIt={this.resultHandler.bind(this)}/>;
      break;
    case "summary":
      contents = <Summary user={this.state.user} score={this.state.score}
                          minutes={this.state.minutes} again={this.quizHandler.bind(this)}/>;
      break;
    default:
      contents = <h2>Warning something went wrong!!!</h2>;
  }
  return <div>
    {buttonBar}
    <main>
      <h1>Quiz-O-Matic</h1>
      {contents}
    </main>
  </div>
}

Receiving Functions via Props 1

Updated Quiz component

function Quiz(props) {
    let allQs = props.questions.map(function(mChoice, i){
        return <Question key={"q" + i} multiChoiceQ={mChoice} />
    })
    return <div>
        <h2>Quiz</h2>
        <button onClick={props.gradeIt}>Grade it!</button>
        {allQs}
    </div>;
}

Receiving Functions via Props 2

Updated Summary component

function Summary(props) {
    let user = "Friend";
    if (props.user) {
        user = props.user;
    }
    return <div>
        <h2>Summary</h2>
        <p>{user} scored {props.score}, in {props.minutes} minutes.</p>
        <p>Keep up the good work and come back soon!</p>
        <button onClick={props.again}>Take Another Quiz</button>
    </div>;
}
// reveal.js plugins