Pular para o conteúdo

e

Adicionando estilos à aplicação React

A aparência atual da nossa aplicação está bastante modesta. No exercício 0.2, o objetivo era passar pelo tutorial CSS da Mozilla.

Vamos dar uma olhada em como podemos adicionar estilos a uma aplicação React. Existem várias maneiras diferentes de fazer isso e veremos os outros métodos mais tarde. Primeiro, adicionaremos o CSS à nossa aplicação da maneira antiga; em um único arquivo sem usar um pré-processador CSS (embora isso não seja inteiramente verdade, como aprenderemos mais tarde).

Vamos criar um novo arquivo chamado index.css no diretório src e vamos adicioná-lo à aplicação importando-o no arquivo index.js:

import './index.css'

Vamos adicionar a seguinte regra CSS ao arquivo index.css:

h1 {
  color: green;
}

As regras CSS consistem em seletores (selectors) e declarações (declarations). O seletor define a quais elementos a regra deve ser aplicada. O seletor acima é h1, que corresponderá a todas as tags de cabeçalho h1 em nossa aplicação.

A declaração define a propriedade color com o valor green (verde).

Uma regra CSS pode conter um número arbitrário de propriedades. Vamos modificar a regra anterior para tornar o texto cursivo, definindo o estilo da fonte como italic (itálico):

h1 {
  color: green;
  font-style: italic;}

Existem muitas maneiras de corresponder a elementos usando diferentes tipos de seletores CSS.

Se quiséssemos direcionar, digamos, cada uma das notas com nossos estilos, poderíamos usar o seletor li, já que todas as notas estão envolvidas em tags li:

const Note = ({ note, toggleImportance }) => {
  const label = note.important 
    ? 'make not important' 
    : 'make important';

  return (
    <li>
      {note.content} 
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

Vamos adicionar a seguinte regra à nossa folha de estilo (já que meu conhecimento em web design moderno elegante é próximo a zero, os estilos aqui adicionados não fazem muito sentido):

li {
  color: grey;
  padding-top: 3px;
  font-size: 15px;
}

Usar tipos de elementos para definir regras CSS é um tanto problemático. Se nossa aplicação contiver outras tags li, a mesma regra de estilo também será aplicada a elas.

Se quisermos aplicar nosso estilo especificamente às notas, a melhor opção é usar seletores de classe.

Em HTML comum, as classes são definidas como o valor do atributo class:

<li class="note">algum texto...</li>

Em React, temos que usar o atributo className em vez do atributo class. Com isso em mente, façamos as seguintes alterações em nosso componente Note:

const Note = ({ note, toggleImportance }) => {
  const label = note.important 
    ? 'make not important' 
    : 'make important';

  return (
    <li className='note'>      {note.content} 
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

Os seletores de classe são definidos com a sintaxe .className:

.note {
  color: grey;
  padding-top: 5px;
  font-size: 15px;
}

Se você adicionar outros elementos li à aplicação agora, eles não serão afetados pela regra de estilo acima.

Uma mensagem de erro aprimorada

Anteriormente, implementamos a mensagem de erro que era exibida quando o usuário tentava alternar a importância de uma nota excluída com o método alert. Vamos implementar a mensagem de erro como seu próprio componente React.

O componente é bastante simples:

const Notification = ({ message }) => {
  if (message === null) {
    return null
  }

  return (
    <div className='error'>
      {message}
    </div>
  )
}

Se o valor da prop message for null, nada é renderizado na tela e, em outros casos, a mensagem é renderizada dentro de um elemento div.

Vamos adicionar um novo pedaço de estado chamado errorMessage ao componente App. Vamos inicializá-lo com alguma mensagem de erro para que possamos testar imediatamente nosso componente:

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)
  const [errorMessage, setErrorMessage] = useState('some error happened...')
  // ...

  return (
    <div>
      <h1>Notes</h1>
      <Notification message={errorMessage} />      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>      
      // ...
    </div>
  )
}

Vamos adicionar uma regra de estilo que sirva para uma mensagem de erro:

.error {
  color: red;
  background: lightgrey;
  font-size: 20px;
  border-style: solid;
  border-radius: 5px;
  padding: 10px;
  margin-bottom: 10px;
}

Agora estamos prontos para adicionar a lógica para exibir a mensagem de erro. Vamos mudar a função toggleImportanceOf da seguinte maneira:

  const toggleImportanceOf = id => {
    const note = notes.find(n => n.id === id)
    const changedNote = { ...note, important: !note.important }

    noteService
      .update(id, changedNote).then(returnedNote => {
        setNotes(notes.map(note => note.id !== id ? note : returnedNote))
      })
      .catch(error => {
        setErrorMessage(          `Note '${note.content}' was already removed from server`        )        setTimeout(() => {          setErrorMessage(null)        }, 5000)        setNotes(notes.filter(n => n.id !== id))
      })
  }

Quando o erro acontece, adicionamos uma mensagem de erro descritiva ao estado errorMessage. Ao mesmo tempo, iniciamos um temporizador, que definirá o estado errorMessage como null após 5 (cinco) segundos.

O resultado fica assim:

captura de tela de erro: removido do servidor da aplicação

O código para o estado atual da nossa aplicação pode ser encontrado na branch part2-7 no GitHub.

Estilos inline

React também possibilita escrever estilos diretamente no código com o chamados estilos inline (ou "estilos em linha").

A ideia por trás da definição de estilos inline é extremamente simples. É possível fornecer a qualquer componente ou elemento React um conjunto de propriedades CSS como um objeto JavaScript através do atributo style (estilo).

As regras CSS são definidas de forma um tanto diferente em JavaScript se comparadas com as de arquivos CSS comuns. Digamos que quiséssemos dar a um elemento a cor verde e uma fonte itálica de 16 pixels de tamanho. Em CSS, ficaria assim:

{
  color: green;
  font-style: italic;
  font-size: 16px;
}

Mas como se trata de um objeto de estilo inline React, ficaria assim:

{
  color: 'green',
  fontStyle: 'italic',
  fontSize: 16
}

Cada propriedade CSS é definida como uma propriedade separada do objeto JavaScript. Valores numéricos em pixels podem ser definidos com simples números inteiros. Uma das principais diferenças em comparação ao CSS comum é que propriedades CSS com hífen (kebab case) são escritas em camelCase.

Em seguida, poderíamos adicionar um "bloco inferior" à nossa aplicação criando um componente Footer e definindo-o com os seguintes estilos inline:

const Footer = () => {  const footerStyle = {    color: 'green',    fontStyle: 'italic',    fontSize: 16,  }  return (    <div style={footerStyle}>      <br />      <em>Note app, Department of Computer Science, University of Helsinki 2022</em>    </div>  )}
const App = () => {
  // ...

  return (
    <div>
      <h1>Notes</h1>

      <Notification message={errorMessage} />

      // ...  

      <Footer />    </div>
  )
}

Estilos inline possuem certas limitações. Por exemplo, não é possível usar as chamadas pseudo-classes diretamente neles.

Estilos inline e algumas outras maneiras de adicionar estilos aos componentes React vão completamente contra a corrente das antigas convenções. Tradicionalmente, tem sido considerada a melhor prática separar completamente o CSS do conteúdo (HTML) e da funcionalidade (JavaScript). De acordo com essa antiga escola de pensamento, o objetivo era escrever o CSS, o HTML e o JavaScript em arquivos separados.

A filosofia do React é, na verdade, o oposto disso. Como a separação de CSS, HTML e JavaScript em arquivos separados não parecia ter escalabilidade em aplicativos maiores, o React baseia a divisão do aplicativo ao longo das linhas de suas entidades funcionais lógicas.

As unidades estruturais que compõem as entidades funcionais da aplicação são os componentes React. Um componente React define o HTML para estruturar o conteúdo, as funções JavaScript para determinar a funcionalidade e também o estilo do componente; tudo em um só lugar. Isso é para criar componentes individuais que sejam o mais independentes e reutilizáveis possível.

O código da versão final da nossa aplicação pode ser encontrado na branch part2-8 no GitHub.

Algumas observações importantes

Há alguns exercícios mais desafiadores no final desta parte. Você pode pular os exercícios se eles forem muito complicados, pois nós voltaremos aos mesmos temas mais tarde; porém, vale a pena ler o conteúdo, de qualquer forma.

Fizemos uma coisa em nossa aplicação que "mascara" uma fonte muito típica de erro.

Definimos o estado notes com um valor inicial de um array vazio:

const App = () => {
  const [notes, setNotes] = useState([])

  // ...
}

Esse é um valor inicial bem lógico, uma vez que as notas são um conjunto, isto é, há muitas notas que o estado irá armazenar.

Se o estado estivesse salvando apenas "uma coisa", um valor inicial mais adequado seria null, indicando que não há nada no início do estado. Vamos ver o que acontece se usarmos esse valor inicial:

const App = () => {
  const [notes, setNotes] = useState(null)
  // ...
}

A aplicação quebra:

console com erro de tipo: typerror cannot read properties of null

A mensagem de erro fornece a razão e a localização do erro. O código que causou o problema é o seguinte:

  // notesToShow gets the value of notes
  const notesToShow = showAll
    ? notes
    : notes.filter(note => note.important)

  // ...

  {notesToShow.map(note =>    <Note key={note.id} note={note} />
  )}

A mensagem de erro é:

Cannot read properties of null (reading 'map')

A variável notesToShow recebe primeiro o valor do estado notes e depois o código tenta chamar o método map em um objeto inexistente, ou seja, null.

Qual é a razão disso?

O hook de efeito (effect hook) utiliza a função setNotes para definir que notes terá as notas que o back-end está retornando:

  useEffect(() => {
    noteService
      .getAll()
      .then(initialNotes => {
        setNotes(initialNotes)      })
  }, [])

No entanto, o problema é que o efeito é executado somente após a primeira renderização. E por conta de notes ter o valor inicial null:

const App = () => {
  const [notes, setNotes] = useState(null)
  // ...

o código a seguir é executado na primeira renderização:

notesToShow = notes

// ...

notesToShow.map(note => ...)

e isso faz com que a aplicação quebre, já que não podemos chamar o método map no valor null.

Não há erro quando definimos notes para ser inicialmente um array vazio, já que é permitido chamar o método map em um array vazio.

Assim, a inicialização do estado "mascarou" o problema que é causado pelo fato de que os dados ainda não foram buscados no back-end.

Outra maneira de contornar o problema é usar a renderização condicional e retornar um valor nulo se o estado do componente não estiver adequadamente inicializado:

const App = () => {
  const [notes, setNotes] = useState(null)  // ... 

  useEffect(() => {
    noteService
      .getAll()
      .then(initialNotes => {
        setNotes(initialNotes)
      })
  }, [])

  // do not render anything if notes is still null
  if (!notes) {     return null   }
  // ...
} 

Assim, nada é renderizado na primeira renderização. Quando as notas chegam do servidor, o efeito usa a função setNotes que define o valor do estado notes. Isso faz com que o componente seja renderizado novamente e, na segunda renderização, as notas são exibidas na tela.

O método baseado na renderização condicional é adequado em casos em que é impossível definir o estado para o qual a renderização inicial seja possível.

Um outro detalhe que ainda precisamos examinar mais de perto é o segundo parâmetro de useEffect:

  useEffect(() => {
    noteService
      .getAll()
      .then(initialNotes => {
        setNotes(initialNotes)  
      })
  }, [])

O segundo parâmetro de useEffect é utilizado para especificar com que frequência o efeito é executado. O princípio é que o efeito é sempre executado após a primeira renderização do componente e quando o valor do segundo parâmetro muda.

Se o segundo parâmetro for um array vazio [], seu conteúdo nunca muda e o efeito é executado somente após a primeira renderização do componente. Isso é exatamente o que queremos quando estamos inicializando o estado da aplicação a partir do servidor.

No entanto, há situações em que queremos executar o efeito em outros momentos, por exemplo, quando o estado do componente muda de uma maneira específica.

Considere a aplicação simples a seguir que consulta as taxas de câmbio da Exchange rate API:

import { useState, useEffect } from 'react'
import axios from 'axios'

const App = () => {
  const [value, setValue] = useState('')
  const [rates, setRates] = useState({})
  const [currency, setCurrency] = useState(null)

  useEffect(() => {
    console.log('effect run, currency is now', currency)

    // skip if currency is not defined
    if (currency) {
      console.log('fetching exchange rates...')
      axios
        .get(`https://open.er-api.com/v6/latest/${currency}`)
        .then(response => {
          setRates(response.data.rates)
        })
    }
  }, [currency])

  const handleChange = (event) => {
    setValue(event.target.value)
  }

  const onSearch = (event) => {
    event.preventDefault()
    setCurrency(value)
  }

  return (
    <div>
      <form onSubmit={onSearch}>
        currency: <input value={value} onChange={handleChange} />
        <button type="submit">exchange rate</button>
      </form>
      <pre>
        {JSON.stringify(rates, null, 2)}
      </pre>
    </div>
  )
}

A interface de usuário da aplicação possui um formulário, onde no campo de entrada é escrito o nome da moeda (currency) desejada. Se a moeda existir, a aplicação renderiza as taxas de câmbio da moeda inserida para outras moedas:

Navegador exibindo taxas de câmbio com "eur" digitado e console dizendo "buscando as taxas de câmbio..."

Quando o botão é clicado, a aplicação pega o nome da moeda inserido no formulário e faz o set no estado currency.

Quando currency recebe um novo valor, a aplicação busca suas taxas de câmbio da API na função de efeito:

const App = () => {
  // ...
  const [currency, setCurrency] = useState(null)

  useEffect(() => {
    console.log('effect run, currency is now', currency)

    // skip if currency is not defined
    if (currency) {
      console.log('fetching exchange rates...')
      axios
        .get(`https://open.er-api.com/v6/latest/${currency}`)
        .then(response => {
          setRates(response.data.rates)
        })
    }
  }, [currency])  // ...
}

O hook useEffect agora tem [currency] como segundo parâmetro. A função de efeito é, portanto, executada após a primeira renderização e sempre depois que a tabela que é definida no segundo parâmetro [currency] muda. Ou seja, quando o estado currency recebe um novo valor, o conteúdo da tabela muda e a função de efeito é executada.

O efeito tem esta condição:

if (currency) { 
  // exchange rates are fetched
}

que impede a requisição das taxas de câmbio logo após a primeira renderização, quando a variável currency ainda tem o valor inicial, ou seja, um valor nulo.

Portanto, se o usuário escrever, por exemplo, eur no campo de pesquisa, a aplicação usa a biblioteca Axios para fazer uma requisição HTTP GET ao endereço https://open.er-api.com/v6/latest/eur e armazena a resposta no estado rates.

Quando o usuário inserir outro valor no campo de pesquisa, por exemplo, usd, a função de efeito é executada novamente e as taxas de câmbio da nova moeda são requisitadas da API.

A forma apresentada aqui para fazer requisições à API pode parecer um pouco estranha. Esta aplicação em específico poderia ter sido completamente construída sem a necessidade de usar o hook useEffect, por meio de requisições feitas à API diretamente na função de gerência de envio do formulário:

  const onSearch = (event) => {
    event.preventDefault()
    axios
      .get(`https://open.er-api.com/v6/latest/${value}`)
      .then(response => {
        setRates(response.data.rates)
      })
  }

No entanto, existem situações em que essa técnica não funcionaria. Por exemplo, é possível que você encontre uma situação dessas no exercício 2.20, onde o uso do hook useEffect possa fornecer uma solução. Observe que isso depende muito da abordagem selecionada; por exemplo, a solução do modelo não usa esse truque.