Aprenda React – Componentes, State e Props

September 24, 2018

Olá pessoal. No último post, falamos sobre JSX, a extensão de sintaxe para Javascript que facilita muito a tarefa de criar interfaces com React, levando a um código que se assemelha muito com um Javascript misturado com HTML.

Neste post, vamos falar sobre um dos conceitos mais legais do React, os componentes. Vamos aprender a criar componentes, importá-los e como organizar as informações de sua aplicação em torno destes componentes.

Para exemplificar, vamos construir um simples contador com botões para incremento e decremento, que vai permitir que coloquemos em prática todos os conceitos relativos a componentes, state (estado), e props (propriedades).

Vamos novamente utilizar a configuração que trabalhamos no primeiro post da série Aprenda React. Pode clicar aqui para ir até ele, e lá você achará o link para o repositório no Github ou pode só clicar aqui.

Componentes

Como de costume, vou demarcar o nosso código básico, de onde partiremos. Isto é o que temos no momento:

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

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

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

Ok. A primeira coisa que você tem que saber é que, com o código acima, você já criou seu primeiro componente. Ao definir Index e fazer com que retorne um elemento JSX, utilizamos uma das duas formas principais de criar um componente, que é através da definição de uma função. Bem, vamos começar a organizar as coisas, movendo este componente para o seu próprio arquivo. Teremos um componente principal, onde importaremos os outros componentes que vão compor nosso simples contador. Primeiro, dentro da pasta src, vamos criar a pasta components, e então, dentro desta pasta, vamos criar o arquivo App.js. Este será nosso componente principal:

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

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

export default App;

Agora, vamos importar este componente e utilizá-lo. No nosso arquivo index.js, localizado dentro da pasta src, vamos remover a definição de Index e então importar o componente App que acabamos de criar, e vamos então passar este componente para o método render. Ficará assim:

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

import App from "./components/App";

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

Se o seu servidor do Webpack ainda não estava rodando, execute o comando yarn run start em seu terminal, abra o seu navegador e se maravilhe em como sua aplicação não mudou nada. Era esperado, visto que só mudamos o componente que estava no arquivo principal para seu próprio arquivo.

Agora, vamos criar um novo componente, que será o responsável por exibir nossa contagem. Nessa primeira iteração, a contagem será simplesmente definida manualmente, mas logo vamos ver como torná-la dinâmica. Primeiro, criamos o arquivo CountDisplay.js dentro da pasta components. Este é o código inicial para este componente:

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

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

export default CountDisplay;

Como vimos no post anterior sobre JSX, podemos incluir o valor de uma variável dentro de um elemento, usando as chaves ({}). Mas e se pudéssemos passar a informação para este componente sobre qual o valor atual da contagem?

Props

Props, que é uma abreviação de properties, ou propriedades, são informações que podem ser passadas para um componente. Pode ser uma string, um número, até mesmo uma função. Este valor pode então ser utilizado pelo componente que a recebe. Primeiro, passamos o dado que desejamos passar ao componente definindo-o como um atributo, onde o componente é utilizado. Vamos passar a prop currentCount para o componente CountDisplay, que está sendo chamado no arquivo App.js. Para o valor numérico, passamos o mesmo dentro de chaves. Vejamos:

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

import CountDisplay from "./CountDisplay";

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

export default App;

Agora, vamos resgatar este valor no componente CountDisplay para utilizá-lo. Para isso, na definição de componente, podemos incluir as props como parâmetro da função. E aí, teremos todas as props que foram passadas para este componente. Vamos dar um console.log nas props para ver o que temos. O código no arquivo CountDisplay.js fica desse jeito:

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

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

export default CountDisplay;

Como você pode perceber ao atualizar a aba do seu navegador e abrir o console dele, o que temos é um objeto, com a prop que você acabou de passar para o componente CountDisplay. Vamos fazer um rápido teste e adicionar mais uma prop neste componente, que ficará assim:

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

Ao atualizar novamente a aba do seu navegador e checar o console novamente, temos exatamente o que esperamos. Agora, o objeto props possui valores para currentCount e name, conforme passamos.

Agora, podemos remover a linha onde fazemos o console.log e também a prop name, que utilizamos somente para um rápido teste. E então, vamos resgatar o valor de currentCount dentro do componente CountDisplay e substitui-lo no parágrafo que exibe a contagem. Ficará assim:

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

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

export default CountDisplay;

Muito bem, agora você deve estar se perguntando em que isso nos ajuda, já que apenas mudamos o local onde definimos um valor fixo para ser repassado ao componente. Vamos lá, vamos falar de state.

State

State, assim como as props, são dados utilizados pelo componente. Novamente, pode ser uma string, um objeto, um array, um número. A diferença, no caso do state, é que ao invés de receber a informação e somente utilizá-la, o state é privado e completamente controlado pelo componente. Para utilizarmos o state, precisamos primeiro conhecer a segunda forma de criar um componente, que são os componentes de classe (em contraste aos componentes funcionais, ou de função, que temos criado até o momento). Primeiro, para criar um componente de classe, devemos extender a classe React.Component. Convertendo nosso componente funcional App.js para um componente de classe, ele fica assim:

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;

Agora, para definir o state deste componente, devemos adicionar um constructor para esta classe, e dentro dela utilizar this.state para definir um objeto que terá os valores iniciais do 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;

Defini a contagem atual para 1 no state, mas perceba que ainda estamos passando o valor fixo 3 para o componente. Para usar o valor salvo no state, tudo o que precisamos é recuperá-lo usando this.state. Assim, o código fica desse jeito:

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;

Agora, vamos entender como manipular os valores no state. Primeiro, criamos dois botões, um para adicionar e outro para subtrair. Vamos usar o elemento button, fazendo com que seu arquivo App.js fique dessa forma:

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;

Agora, existe um atributo no elemento button chamado onClick. Com este atributo, podemos definir o que queremos que aconteça ao clicar em um botão. Vamos usar este atributo para executar uma função que irá atualizar o nosso state, aumentando ou decrescendo o valor por 1 dependendo do botão pressionado. Primeiro, vamos criar métodos de classe para acrescer e decrescer, e então associar estes métodos com cada botão. Para atualizar o state deste componente, usamos this.setState, sendo this referente à instância do componente. Passamos a este método um objeto, com a chave que desejamos atualizar e o novo valor do mesmo. Neste caso, usaremos o valor atual da contagem, mais ou menos 1:

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;

Repare que após a função, utilizamos bind(this). Isto é necessário para que, dentro da função possamos usar o this, se referindo ao componente. Caso contrário, o this será undefined dentro da função e o setState não funcionará. Existem outras formas de fazer isso. A primeira é fazer o bind no constructor. Ficará assim:

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;

E ainda há outra forma, que é usar arrow functions. Neste caso, você não tem que fazer bind, porque a arrow function já faz isso automaticamente. Fica desta forma:

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;

Todas estas formas funcionam e atingem o mesmo resultado. Você pode usar aquela que lhe parecer mais legível, mais clara.

Agora, nosso componente está totalmente funcional, mas podemos refatorar um pouco. A ideia é que estes botões sejam um componente a parte. Vamos ver como faremos para atualizar o state de um componente a partir da chamada da função em outro.

Primeiro, vamos criar um componente CounterButtons.js. Agora, vamos apenas extrair o que já temos no componente App.js e passar para o novo componente que criamos. Nosso componente CounterButtons.js então ficará assim:

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;

E no componente App.js, vamos ter o seguinte:

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;

Neste momento, sua aplicação já será renderizada como antes, mas os botões não irão funcionar, pois eles não tem acesso aos métodos que atualizam o state. O que vamos fazer, então, é passar uma função como uma prop, e chamar esta função através das props recebidas no componente novo que criamos. Vamos lá então.

Primeiro, no componente App.js, vamos passar os métodos através das props do componente CounterButtons.js. Vamos utilizar como nomes para as props, onIncrement e onDecrement. O componente App.js ficará desta forma:

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;

E agora, no componente CounterButtons.js, nós alteramos o atributo onClick dos botões para que chame as funções passadas através das props. Para que possamos já chamar a função diretamente, sem precisar fazer bind e chamar um método definido externamente, vamos utilizar arrow functions. E então, seu componente CounterButtons.js ficará desta forma:

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;

Neste caso, onde nossa aplicação é muito simples, tudo isso parece apenas adicionar complexidade de forma desnecessária, mas em aplicações maiores, essa separação de conceitos e organização em componentes é vital.

Conclusão

Para resumir um pouco tudo que vimos, seguem alguns pontos:

  • Existem duas formas de criar componentes, a forma funcional e componentes de classes
  • Props são dados passados a um componente para serem usados por ele
  • State são dados a serem usados pelo componente, privados e controlados pelo próprio
  • Apenas componentes de classes podem utilizar o state no React
  • Você pode passar funções através das props ao organizar sua aplicação em componentes

Bem, com este post espero ter ajudado a esclarecer um pouco os conceitos sobre componentes, state e props no React. Qualquer dúvida é só enviar nos comentários.

Abraços