Compartilhando State Entre Componentes

Às vezes, você deseja que o state de dois componentes seja sempre alterado em conjunto. Para fazer isso, remova o state de ambos, mova-os para o pai comum mais próximo e, em seguida, passe-os aos componentes por meio de props. Isto é conhecido como elevação de state e é uma das coisas mais comuns que você fará ao escrever código React.

Você aprenderá

  • Como compartilhar state entre componentes por sua elevação
  • O que são componentes controlados e não controlados

Exemplificando a elevação de state

Neste exemplo, um componente Accordion pai renderiza dois componentes Panel separados:

  • Accordion
    • Panel
    • Panel

Cada componente Panel tem um state booleano isActive que determina se o seu conteúdo está visível.

Pressione o botão “Show” de ambos os painéis:

import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Cazaquistão</h2>
      <Panel title="Sobre">
        Com uma população de cerca de 2 milhões de habitantes, Almaty é a maior cidade do Cazaquistão. De 1929 a 1997, foi sua capital.
      </Panel>
      <Panel title="Etimologia">
        O nome vem de <span lang="kk-KZ">алма</span>, a palavra cazaque para "maçã", e é frequentemente traduzido como "cheio de maçãs". De fato, acredita-se que a região em torno de Almaty seja o lar ancestral da maçã, e o <i lang="la">Malus sieversii</i> selvagem é considerado um provável candidato a ancestral da maçã doméstica moderna.
      </Panel>
    </>
  );
}

Observe como o fato de pressionar o botão de um painel não afeta o outro painel—eles são independentes.

Diagrama mostrando uma árvore de três componentes, um componente principal denominado Accordion e dois componentes secundários denominados Painel. Ambos os componentes Paneil contêm isActive com valor falso.
Diagrama mostrando uma árvore de três componentes, um componente principal denominado Accordion e dois componentes secundários denominados Painel. Ambos os componentes Paneil contêm isActive com valor falso.

Inicialmente, o state isActive de cada Panel é falso, de modo que ambos aparecem recolhidos

O mesmo diagrama que o anterior, com o isActive do primeiro componente filho do Painel destacado, indicando um clique com o valor isActive definido como verdadeiro. O segundo componente Painel ainda contém o valor falso.
O mesmo diagrama que o anterior, com o isActive do primeiro componente filho do Painel destacado, indicando um clique com o valor isActive definido como verdadeiro. O segundo componente Painel ainda contém o valor falso.

Clicar em um dos botões do Panel somente atualizará o state isActive desse Panel.

Mas agora digamos que você queira alterá-lo para que apenas um painel seja expandido por vez. Com esse design, expandir o segundo painel deve recolher o primeiro. Como você faria isso?

Para coordenar esses dois painéis, você precisa “elevar o state deles” para um componente pai em três etapas:

  1. Remover o state dos componentes filhos.
  2. Passar dados estáticos do pai comum.
  3. Adicionar o state ao pai comum e passá-lo para baixo junto com os manipuladores de eventos.

Isso permitirá que o componente Accordion coordene ambos os Panels e expanda apenas um de cada vez.

Etapa 1: Remover o state dos componentes filhos

Você dará o controle do isActive do Panel ao seu componente pai. Isso significa que o componente pai passará o isActive para o Panel como uma propriedade. Comece removendo essa linha do componente Panel:

const [isActive, setIsActive] = useState(false);

Em vez disso, adicione isActive à lista de props do Panel:

function Panel({ title, children, isActive }) {

Agora, o componente pai do Panel pode controlar o isActive transmitindo-o como uma prop. Por outro lado, o componente Panel agora não tem nenhum controle sobre o valor de isActive; isso agora depende do componente pai!

Etapa 2: passar dados estáticos do pai comum

Para elevar o state, você deve localizar o componente pai comum mais próximo de ambos os componentes filhos que deseja coordenar:

  • Accordion (pai comum mais próximo)
    • Panel
    • Panel

Neste exemplo, é o componente Accordion. Como ele está acima de ambos os painéis e pode controlar suas props, ele se tornará a “fonte da verdade” para saber qual painel está ativo no momento. Faça com que o componente Accordion passe um valor estático de isActive (por exemplo, true) para ambos os painéis:

import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Cazaquistão</h2>
      <Panel title="Sobre" isActive={true}>
        Com uma população de cerca de 2 milhões de habitantes, Almaty é a maior cidade do Cazaquistão. De 1929 a 1997, foi sua capital.
      </Panel>
      <Panel title="Etmologia" isActive={true}>
        O nome vem de <span lang="kk-KZ">алма</span>, a palavra cazaque para "maçã", e é frequentemente traduzido como "cheio de maçãs". De fato, acredita-se que a região em torno de Almaty seja o lar ancestral da maçã, e o<i lang="la"> Malus sieversii</i> selvagem é considerado um provável candidato a ancestral da maçã doméstica moderna.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Exibir
        </button>
      )}
    </section>
  );
}

Tente editar os valores estáticos isActive no componente Accordion e veja o resultado na tela.

Etapa 3: Adicionar state ao pai comum

Elevar o state muitas vezes muda a natureza do que você está armazenando como state.

Nesse caso, apenas um painel deve estar ativo por vez. Isso significa que o componente pai comum Accordion precisa manter o controle de qual painel está ativo. Em vez de um valor booleano, ele poderia usar um número como índice do Panel ativo para a variável do state:

const [activeIndex, setActiveIndex] = useState(0);

Quando o activeIndex é 0, o primeiro painel está ativo, e quando é 1, é o segundo.

Clicar no botão “Exibir” em um dos “Painéis” precisa alterar o índice ativo no “Accordion”. Um Panel não pode definir o state activeIndex diretamente porque ele é definido dentro do Accordion. O componente Accordion precisa permitir explicitamente que o componente Panel altere seu state, passando um manipulador de eventos como uma prop:

<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>

O <button> dentro do Panel agora usará a propriedade onShow como seu manipulador de eventos de clique:

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Cazaquistão</h2>
      <Panel
        title="Sobre"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Com uma população de cerca de 2 milhões de habitantes, Almaty é a maior cidade do Cazaquistão. De 1929 a 1997, foi sua capital.
      </Panel>
      <Panel
        title="Etmologia"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        O nome vem de <span lang="kk-KZ">алма</span>, a palavra cazaque para "maçã", e é frequentemente traduzido como "cheio de maçãs". De fato, acredita-se que a região em torno de Almaty seja o lar ancestral da maçã, e o<i lang="la"> Malus sieversii</i> selvagem é considerado um provável candidato a ancestral da maçã doméstica moderna.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Exibir
        </button>
      )}
    </section>
  );
}

Isso completa a elevação do state! Mover o state para o componente pai comum permitiu que você coordenasse os dois painéis. Usar o índice ativo em vez de dois sinalizadores “é exibido” garantiu que apenas um painel estivesse ativo em um determinado momento. E passar o manipulador de eventos para o filho permitiu que ele alterasse o state do pai.

Diagrama que mostra uma árvore de três componentes, um pai denominado Accordion e dois filhos denominados Panel. O Accordion contém um valor activeIndex igual a zero, que se transforma em um valor isActive verdadeiro passado para o primeiro Panel, e um valor isActive falso passado para o segundo Panel.
Diagrama que mostra uma árvore de três componentes, um pai denominado Accordion e dois filhos denominados Panel. O Accordion contém um valor activeIndex igual a zero, que se transforma em um valor isActive verdadeiro passado para o primeiro Panel, e um valor isActive falso passado para o segundo Panel.

Inicialmente, o activeIndex do Accordion é 0, portanto o primeiro Panel recebe isActive = true

O mesmo diagrama que o anterior, com o valor activeIndex do componente Accordion pai destacado, indicando um clique com o valor alterado para um. O fluxo para ambos os componentes Panel filhos também é destacado, e o valor isActive passado para cada filho é definido como o oposto: false para o primeiro Panel e true para o segundo.
O mesmo diagrama que o anterior, com o valor activeIndex do componente Accordion pai destacado, indicando um clique com o valor alterado para um. O fluxo para ambos os componentes Panel filhos também é destacado, e o valor isActive passado para cada filho é definido como o oposto: false para o primeiro Panel e true para o segundo.

Quando o state activeIndex do Accordion muda para 1, o segundo Panel recebe isActive = true em seu lugar

Deep Dive

Componentes controlados e não controlados

É comum chamar um componente com algum state local de “não controlado”. Por exemplo, o componente Panel original com uma variável de state isActive não é controlado porque seu pai não pode influenciar se o painel está ativo ou não.

Por outro lado, pode-se dizer que um componente é “controlado” quando as informações importantes nele contidas são orientadas por props em vez de seu próprio state local. Isso permite que o componente pai especifique totalmente seu comportamento. O componente final Panel com a prop isActive é controlado pelo componente Accordion.

Os componentes não controlados são mais fáceis de usar dentro de seus pais porque exigem menos configuração. Mas são menos flexíveis quando você deseja coordená-los entre si. Os componentes controlados são extremamente flexíveis, mas exigem que os componentes principais os configurem totalmente com props.

Na prática, “controlado” e “não controlado” não são termos técnicos rigorosos - cada componente geralmente tem uma mistura de state local e props. No entanto, essa é uma maneira útil de falar sobre como os componentes são projetados e quais recursos eles oferecem.

Ao escrever um componente, considere quais informações nele devem ser controladas (por meio de props) e quais informações não devem ser controladas (por meio de state). Mas você sempre pode mudar de ideia e refatorar mais tarde.

Uma única fonte de verdade para cada state

Em um aplicativo React, muitos componentes terão seu próprio state. Alguns states podem “residir” perto dos componentes de folha (componentes na parte inferior da árvore), como entradas. Outro state pode “residir” mais perto do topo do aplicativo. Por exemplo, até mesmo as bibliotecas de roteamento do lado do cliente geralmente são implementadas armazenando a rota atual no state do React e passando-a para baixo por meio de props!

Para cada parte exclusiva do state, você escolherá o componente que o “possui”. Esse princípio também é conhecido como ter uma “única fonte de verdade”. Isso não significa que todo o state esteja em um único lugar, mas que, para cada parte do state, há um componente específico que contém essa parte da informação. Em vez de duplicar o state compartilhado entre os componentes, levante-o para o pai compartilhado comum e passe-o para os filhos que precisam dele.

Seu aplicativo mudará à medida que você trabalhar nele. É comum que você mova o state para baixo ou para cima enquanto ainda está descobrindo onde cada parte do state “reside”. Tudo isso faz parte do processo!

Para ver como isso funciona na prática com mais alguns componentes, leia Pensando em React.

Recap

  • Quando você quiser coordenar dois componentes, mova o state deles para o pai comum.
  • Em seguida, passe as informações por meio de props vindas de seu pai comum.
  • Por fim, passe os manipuladores de eventos para que os filhos possam alterar o state do pai.
  • É útil considerar os componentes como “controlados” (acionados por props) ou “não controlados” (acionados pelo state).

Challenge 1 of 2:
Entradas sincronizadas

Essas duas entradas são independentes. Faça com que elas fiquem sincronizadas: a edição de uma entrada deve atualizar a outra entrada com o mesmo texto e vice-versa.

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="First input" />
      <Input label="Second input" />
    </>
  );
}

function Input({ label }) {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}