Gerenciando o Estado

Intermediate

À medida que a sua aplicação cresce, ajuda ser mais intencional sobre como o seu estado é organizado e como os dados fluem entre os seus componentes. O estado redundante ou duplicado é uma fonte comum de bugs. Neste capítulo, você aprenderá como estruturar bem o seu estado, como manter a lógica de atualização do seu estado e como compartilhar o estado entre componentes distantes.

Reagindo à inserção de dados com o estado

Com React, você não modificará a UI a partir do código diretamente. Por exemplo, você não vai escrever comandos como “desativar o botão”, “ativar o botão”, “mostrar a mensagem de sucesso”, etc. Em vez disso, você descreverá a UI que deseja ver para os diferentes estados visuais de seu componente (“estado inicial”, “estado de digitação”, “estado de sucesso”) e então acionará as mudanças de estado em resposta à inserção de dados do usuário. Isto é semelhante a como os designers pensam sobre a UI.

Aqui está um formulário de questionário construído usando React. Observe como ele utiliza a variável status para determinar se deve habilitar ou desabilitar o botão de submeter, e se deve mostrar a mensagem de sucesso no seu lugar.

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Ready to learn this topic?

Leia Reagindo à inserção de dados com o estado para aprender como abordar as interações com uma mentalidade orientada ao estado.

Read More

Como estruturar bem o estado

A boa estruturação do estado pode fazer a diferença entre um componente que é agradável de modificar e debugar e um que é uma fonte contínua de bugs. O princípio mais importante é que o estado não deve conter informações redundantes ou duplicadas. Se houver algum estado desnecessário, é fácil esquecer de atualizá-lo e introduzir bugs!

Por exemplo, este formulário tem uma variável de estado redundante fullName:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Você pode removê-lo e simplificar o código calculando fullName enquanto o componente está renderizando:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Isto pode parecer uma pequena mudança mas muitos bugs em aplicações React são corrigidos desta forma.

Ready to learn this topic?

Leia Escolhendo a estrutura do estado para aprender como modelar a forma do estado para evitar bugs.

Read More

Compartilhando o estado entre componentes

Às vezes, você quer que o estado de dois componentes mude sempre em conjunto. Para fazer isso, remova o estado de ambos, mova-o para seu parente comum mais próximo e depois passe-o para eles por meio de props. Isto é conhecido como “levantar o estado” e é uma das coisas mais comuns que você fará escrevendo código React.

Neste exemplo, apenas um painel deve estar ativo de cada vez. Para conseguir isto, em vez de manter o estado ativo dentro de cada painel individual, o componente parente possui o estado e especifica os adereços para seus filhos.

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

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

Ready to learn this topic?

Leia Compartilhando o estado entre componentes para aprender como levantar o estado e manter os componentes em sincronia.

Read More

Preservando e reinicializando o estado

Quando você renderiza novamente um componente, o React precisa decidir quais partes da árvore manter (e atualizar) e quais partes descartar ou recriar do zero. Na maioria dos casos, o comportamento automático do React funciona suficientemente bem. Por padrão, o React preserva as partes da árvore que “combinam” com a árvore de componentes anteriormente renderizada.

No entanto, às vezes isso não é o que se deseja. Por exemplo, neste aplicativo, digitar uma mensagem e depois trocar o destinatário não reinicia a caixa de texto. Isto pode fazer com que o usuário acidentalmente envie uma mensagem para a pessoa errada:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

React permite que você sobreponha o comportamento padrão e force um componente para repor seu estado passando-lhe uma key diferente, como <Chat key={email} />. Isto diz ao React que se o destinatário for diferente, deve ser considerado um componente Chat diferente que precisa ser recriado a partir do zero com os novos dados (e UI como campos de entrada). Agora, a troca entre os destinatários sempre redefine o campo de entrada - mesmo que você renderize o mesmo componente.

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

Ready to learn this topic?

Leia Preservando e reinicializando o estado para aprender a vida útil do estado e como controlá-lo.

Read More

Extraindo a lógica de estado para um redutor

Componentes com muitas atualizações de estado espalhados por muitos manipuladores de evento podem apresentar uma complexidade avassaladora. Para estes casos, você pode consolidar toda a lógica de atualização do estado fora de seu componente em uma única função, chamada de “redutor”. Os seus manipuladores de eventos tornam-se concisos porque eles especificam apenas as “ações” do usuário. Na parte inferior do arquivo, a função redutor especifica como o estado deve ser atualizado em resposta a cada ação!

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

Ready to learn this topic?

Leia Extraindo a lógica de estado para um redutor para aprender como consolidar a lógica numa função redutora.

Read More

Passando os dados profundamente com o contexto

Normalmente, você passará informações de um componente parente para um componente descendente através de props. Mas a passagem de props pode tornar-se inconveniente se você precisar passar algum prop por muitos componentes ou se muitos componentes precisarem da mesma informação. O contexto permite que o componente parente torne alguma informação disponível para qualquer componente da árvore abaixo dele - não importa quão profundo seja - sem passá-lo explicitamente através de props.

Aqui, o componente “Heading” determina seu nível de cabeçalho ao “perguntar” à “Seção” mais próxima pelo seu nível. Cada “Section” monitora seu próprio nível, perguntando à “Section” parente e acrescentando-lhe um nível. Cada “Section” fornece informações a todos os componentes abaixo dela sem passar props - faz isso através do contexto.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Ready to learn this topic?

Leia Passando os dados profundamente com o contexto para aprender sobre o uso do contexto como uma alternativa à passagem de props.

Read More

Escalonamento com redutor e contexto

Redutores permitem consolidar a lógica de atualização do estado de um componente. O contexto permite que você transmita informações em profundidade para outros componentes. Você pode combinar redutores e contexto juntos para gerenciar o estado de uma tela complexa.

Com esta abordagem, um componente parente com estado complexo, o gerencia com um redutor. Outros componentes em qualquer parte profunda da árvore podem ler seu estado através do contexto. Eles também podem despachar ações para atualizar esse estado.

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Ready to learn this topic?

Leia Escalonamento com redutor e contexto para aprender como a gestão do estado escala numa aplicação em crescimento.

Read More

O que vem a seguir?

Vá até Reagindo à inserção de dados com o estado para começar a ler este capítulo página por página!

Ou, se você já está familiarizado com estes tópicos, por que não ler sobre Escape Hatches?