DEV Community

Felipe Galvão
Felipe Galvão

Posted on • Edited on • Originally published at felipegalvao.com.br

Learn React - Part 3 - Components, State and Props

Originally published on my blog

Hey folks. In our last Learn React post, we talked about JSX, the Javascript syntax extension that makes it easier to build interfaces with React, leading to code that seems like a mix of Javascript and HTML.

In this post, we will talk about one of the coolest concepts of React, components. We'll learn how to create components, import them and organize your application's data around these components.

As an example, we will create a simple counter with buttons to increment and decrement. This simple application will allow us to put into practice all the concepts related to components, state and props.

Again, we will use the setup that we worked on the first post of the Learn React series. You can click here to open it, where you can find the Github repository (or you can just click here to access it directly), that you can clone and keep up with this post.

Components

As usual, I'll put the basic code that we will start from here. This is what he have right now, if you cloned the repository indicated:

import React from "react";
import ReactDOM from "react-dom";

const Index = () => {
  return <div>Hello React!</div>;
};

ReactDOM.render(<Index />, document.getElementById("index"));

Ok, the first thing that you need to know is that, using the code above, you already created your first component. When we define Index and make it return a JSX element, we use one of the two main ways of creating a component, which is, through a function. Well, let's start to organize our app moving this Index component to it's own file. We will have a main component, where we will import and use the others that we will create in this post. First, inside of the src folder, we will create the components folder, and then, inside of this folder, we will create App.js file. This will be our main component. Note that the name of this file is not a rule, you don't need to call it App.js, this is just a personal preference:

import React from "react";
import ReactDOM from "react-dom";

const App = () => {
  return <div>Hello React!</div>;
};

export default App;

Now, let's import this component and use it. In our index.js file, located inside of the src folder, we will remove the Index definition and then import the App component that we just created. We will then, pass it to the render method:

import React from "react";
import ReactDOM from "react-dom";

import App from "./components/App";

ReactDOM.render(<App />, document.getElementById("index"));

If your Webpack server is not running yet, run yarn run start in your terminal, and then open your browser in http://localhost:8080/ (or wait for it to open automatically, depending on your Webpack setup), and marvel as your application is unchanged. That is expected, since we didn't changed anything yet, we just moved some stuff around, getting one component into it's own file.

Now, let's create a new component, which will be responsible for showing the current count. In this first step, we will define the current count manually, but we will soon learn how to make it dynamic. First, let's create the CountDisplay.js file inside of the components folder. This is the initial code for this file / component:

import React from "react";
import ReactDOM from "react-dom";

const CountDisplay = () => {
  return <p>The count is 0</p>
};

export default CountDisplay;

As we saw in our last post regarding JSX, we can include the value of a variable inside of an element, using curly braces ({}). But what if we could pass the information to this component about the current count value.

Props

Props, an abbreviation for properties is basically data that we pass to the component. It can be a string, a number, even a function. This value can then be used by the component that receives it. First, we pass the data we want to the component, defining it as an attribute, but with the name that we want. Let's pass the currentCount to the CountDisplay component, that is being called in the App.js file. For a numeric value, we pass it inside curly braces. Let's see how it will look like.:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

const App = (props) => {
  return <CountDisplay 
    currentCount={3}
  />;
};

export default App;

Now, let's grab this value inside the component that received it, CountDisplay. For that, the first thing we need to do is include props as an argument to the function that defines the component. Then, we will have access to whatever props we passed to this component. Let's run a console.log to see what the props looks like. the code in the CountDisplay.js file will be like this:

import React from "react";
import ReactDOM from "react-dom";

const CountDisplay = (props) => {
  console.log(props);
  return <p>The count is 0</p>;
};

export default CountDisplay;

As you can notice when you refresh your browser tab and open its console, what we have is an object with the countDisplay prop that we just passed to this component. Let's do a quick test, passing an additional prop to this component, which will be like this:

const App = (props) => {
  return <CountDisplay 
    currentCount={3}
    name="Felipe"
  />;
};

When you refresh your browser tab again, you will see in your browser's console the props object with values for currentCount and name, the props that we passed to this component.

Now we can remove the line where we do the console.log and also the line where we pass the name prop, since we only used it to do a test. Then, we can grab the currentCount value inside of the CountDisplay component and replace it on the paragraph that shows the count. This is what you will have:

import React from "react";
import ReactDOM from "react-dom";

const CountDisplay = (props) => {
  return <p>The count is { props.currentCount }</p>;
};

export default CountDisplay;

Now you must be asking yourself how this helps us, since we just changed the place where we define the current count manually. Well, now we will talk about state.

State

State, just like props, is data to be used by a component. Again, it can be a string, a number, an object. The difference with the state is that it is private for the component and entirely controlled by it. For we to make use of the state, we need to learn the second way to create a component, which is through class. So, we will build class components, instead of the function components that we were building until now. To create a class component, we need to extend the React.Component class. Converting our App.js functional component to a class component, it will be like this:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  render() {
    return (
      <CountDisplay 
        currentCount={3}
      />
    );
  }
}

export default App;

Now, to define the state for this component, we must define a constructor for this class, and inside of it, use this.state to define an object that will have the initial values for the state:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  render() {
    return (
      <CountDisplay 
        currentCount={3}
      />
    );
  }
}

export default App;

I defined the current count to 1 in the state, but notice that we are still passing the fixed value 3 to the component, and so, nothing changes for now. To use the value that is saved in the state, all we need to do is get it with this.state. With this step, the code is as follows:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  render() {
    return (
      <CountDisplay 
        currentCount={this.state.currentCount}
      />
    );
  }
}

export default App;

Now, let's understand how we manipulate the state First, let's create two buttons, one to increment and another one to decrement. Let's use the button element, and your App.js file will be like this:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  render() {
    return (
      <div>
        <button>
          +
        </button>
        <button>
          -
        </button>
        <CountDisplay 
          currentCount={this.state.currentCount}
        />
      </div>
    );
  }
}

export default App;

Now, there is an attribute on the button element called onClick. With this attribute, we can define what happens when a button is clicked. Let's use this attribute to run a function that will update our state, increasing or decreasing the value by 1 depending on the button pressed. First, let's create the class methods to increment and decrement, and then, assign these methods to each button accordingly. To update the state for this component, we use this.setState, where this refers to the component instance We pass to this method an object with the key that we want to change and the new value. In this case, we will use the current count, plus or minus one:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  handleIncrement(event) {
    this.setState({currentCount: this.state.currentCount + 1});
  }

  handleDecrement(event) {
    this.setState({currentCount: this.state.currentCount - 1});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleIncrement.bind(this)}>
          +
        </button>
        <button onClick={this.handleDecrement.bind(this)}>
          -
        </button>
        <CountDisplay 
          currentCount={this.state.currentCount}
        />
      </div>
    );
  }
}

export default App;

Notice that, besides putting the function in there, we added bind(this). This is required so that we can use this inside the function. If we don't do that, this will be undefined inside of the function and setState won't work. There are at least 2 other ways to do this. The first one is to do the bind inside of the constructor. This is the result:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};

    this.handleIncrement = this.handleIncrement.bind(this);
    this.handleDecrement = this.handleDecrement.bind(this);
  }

  handleIncrement(event) {
    this.setState({currentCount: this.state.currentCount + 1});
  }

  handleDecrement(event) {
    this.setState({currentCount: this.state.currentCount - 1});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleIncrement}>
          +
        </button>
        <button onClick={this.handleDecrement}>
          -
        </button>
        <CountDisplay 
          currentCount={this.state.currentCount}
        />
      </div>
    );
  }
}

export default App;

And there is still another way, arrow functions. If you use arrow functions, you don't have to bind, since arrow functions already do that automatically. Like this:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  handleIncrement(event) {
    this.setState({currentCount: this.state.currentCount + 1});
  }

  handleDecrement(event) {
    this.setState({currentCount: this.state.currentCount - 1});
  }

  render() {
    return (
      <div>
        <button onClick={() => this.handleIncrement()}>
          +
        </button>
        <button onClick={() => this.handleDecrement()}>
          -
        </button>
        <CountDisplay 
          currentCount={this.state.currentCount}
        />
      </div>
    );
  }
}

export default App;

All of those ways to bind work, so, feel free to use the one you prefer.

Now our component is working. You can increment and decrement the count using the buttons. But we can refactor a little bit. The idea is for the buttons to be part of their own component. Let's see how we can structure our application to update the state of a component when a function is called on another component.

First, let's create a CounterButtons.js component. Now, let's extract whatever is in the App.js component and pass it to the new component we created. Our CounterButtons.js component will then look like this:

import React from "react";
import ReactDOM from "react-dom";

class CounterButtons extends React.Component {
  render() {
    return (
      <div>
        <button onClick={() => this.handleIncrement()}>
          +
        </button>
        <button onClick={() => this.handleDecrement()}>
          -
        </button>
      </div>
    );
  }
}

export default CounterButtons;

And in our App.js component, we will have the following code:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";
import CounterButtons from "./CounterButtons";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  handleIncrement(event) {
    this.setState({currentCount: this.state.currentCount + 1});
  }

  handleDecrement(event) {
    this.setState({currentCount: this.state.currentCount - 1});
  }

  render() {
    return (
      <div>
        <CounterButtons />
        <CountDisplay 
          currentCount={this.state.currentCount}
        />
      </div>
    );
  }
}

export default App;

If you go to your browser now and refresh, your application will be rendered, but the buttons will not work, as they don't have access to the methods that update the state. What we are going to do is pass functions as props, and call these functions in the new component we created. Let's do this.

First, in the App.js component, let's pass the methods through the props of the CounterButton component. Let's use onIncrement and onDecrement as names for the props The App.js component will be like this:

import React from "react";
import ReactDOM from "react-dom";

import CountDisplay from "./CountDisplay";
import CounterButtons from "./CounterButtons";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {currentCount: 1};
  }

  handleIncrement() {
    this.setState({currentCount: this.state.currentCount + 1});
  }

  handleDecrement() {
    this.setState({currentCount: this.state.currentCount - 1});
  }

  render() {
    return (
      <div>
        <CounterButtons
          onIncrement={this.handleIncrement.bind(this)}
          onDecrement={this.handleDecrement.bind(this)}
        />
        <CountDisplay 
          currentCount={this.state.currentCount}
        />
      </div>
    );
  }
}

export default App;

And now, on the CounterButtons.js file, we change the onClick attribute on the buttons to call the functions passed through the props. To avoid having to bind, I will use arrow functions, so that we can call the functions directly on the onClick definition. This is how the CounterButtons.js component will be:

import React from "react";
import ReactDOM from "react-dom";

class CounterButtons extends React.Component {
  render() {
    return (
      <div>
        <button onClick={() => this.props.onIncrement()}>
          +
        </button>
        <button onClick={() => this.props.onDecrement()}>
          -
        </button>
      </div>
    );
  }
}

export default CounterButtons;

In this case, where our application is really simple, this seems like adding unnecessary complexity, but in bigger and more complex applications, the separation of concepts and the organization in well defined components is vital.

Conclusion

To summarize what we saw in this post:

  • There are two ways to create components, functional components and class components
  • Props are data passed to a component to be used by them
  • State is data that is private to a component and entirely controller by it
  • Only class components can use state in React
  • Besides passing values through props, you can also pass functions

With this post, I hope that I was able to explain the main concepts about components, props and state in React. Any questions, just leave it in the comments.

Cheers

Top comments (0)