Pular para o conteúdo

d

Alterando dados no servidor

Quando criamos notas em nossa aplicação, naturalmente queremos armazená-las em algum servidor back-end. O pacote json-server afirma ser uma API REST ou RESTful em sua documentação:

Use uma API REST falsa completa sem precisar programá-la em menos de 30 segundos (sério!)

O json-server não corresponde exatamente à descrição fornecida pela definição do que é uma API REST, e nem mesmo a maioria das outras APIs que afirmam ser RESTful.

Vamos nos aprofundar mais em REST na próxima parte do curso. Porém, é importante já nos familiarizarmos com algumas das convenções usadas pelo json-server e APIs REST em geral. Em particular, vamos dar uma olhada no uso convencional de rotas (routes) — também conhecidas como URLs —, e os tipos de requisição HTTP em REST.

REST

Na terminologia REST, nos referimos a objetos de dados individuais — as notas em nossa aplicação, por exemplo — como recursos. Cada recurso tem um endereço único associado a ele — sua URL. De acordo com uma convenção geral usada pelo json-server, poderíamos localizar uma nota individual na URL do recurso notes/3, onde 3 é o id do recurso. A URL notes, por outro lado, apontaria para uma coleção de recursos contendo todas as notas.

Os recursos são buscados do servidor com requisições HTTP GET. Por exemplo, uma requisição HTTP GET para a URL notes/3 retornará a nota que tem o número de id 3. Uma requisição HTTP GET para a URL notes retornaria uma lista de todas as notas.

A criação de um novo recurso para armazenar uma nota é feita fazendo uma requisição HTTP POST para a URL notes de acordo com a convenção REST a qual o json-server adere. Os dados para o novo recurso de nota são enviados no corpo (body) da requisição.

O json-server exige que todos os dados sejam enviados no formato JSON. O que isso significa na prática é que os dados devem ser formatados como string e a requisição deve conter o cabeçalho de requisição Content-Type (Tipo de Conteúdo) com o valor application/json.

Enviando dados ao servidor

Vamos fazer as seguintes alterações no gerenciador de evento responsável por criar uma nova nota:

addNote = event => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    important: Math.random() < 0.5,
  }

  axios    .post('http://localhost:3001/notes', noteObject)    .then(response => {      console.log(response)    })}

Criamos um novo objeto para a nota, mas omitimos a propriedade id já que é melhor deixar o servidor gerar os ids para nossos recursos!

O objeto é enviado ao servidor usando o método post do axios. O gerenciador de evento assinalado registra (logs) a resposta que é enviada de volta pelo servidor para o console.

Quando tentamos criar uma nova nota, a seguinte saída aparece no console:

saída dos dados json no console

O recurso de nota recém-criado é armazenado no valor da propriedade data do objeto response.

Por vezes é útil inspecionar as requisições HTTP na guia Rede das Ferramentas do Desenvolvedor do Chrome (ou do navegador que esteja utilizando), recurso esse que foi amplamente usado no início da Parte 0.

Podemos usar o inspetor para verificar se os cabeçalhos enviados na requisição POST são os que esperávamos:

o header no dev tools mostra '201 created' para localhost:3001/notes

Como os dados que enviamos na requisição POST eram um objeto JavaScript, o axios sabia automaticamente definir o valor apropriado de application/json para o cabeçalho Content-Type.

A guia Visualização (Payload) pode ser usada para verificar os dados da requisição:

a guia VIsualização do devtools mostra os campos content e important

Também é útil a guia Resposta (Response), pois mostra qual foi os dados que o servidor respondeu:

a guia Resposta do devtools mostra o mesmo conteúdo visto na guia Visualização, mas com o campo id incluído

A nova nota ainda não é renderizada na tela. Isso se deve ao fato de que não atualizamos o estado do componente App quando criamos a nova nota. Vamos consertar isso:

addNote = event => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    important: Math.random() > 0.5,
  }

  axios
    .post('http://localhost:3001/notes', noteObject)
    .then(response => {
      setNotes(notes.concat(response.data))      setNewNote('')    })
}

A nova nota retornada pelo servidor back-end é adicionada à lista de notas no estado da nossa aplicação seguindo a forma habitual do uso da função setNotes e, em seguida, reinicia o formulário de criação de notas. Um detalhe importante a lembrar é que o método concat não muda o estado original do componente, mas cria uma nova cópia da lista.

Assim que os dados retornados pelo servidor começam a ter efeito no comportamento das nossas aplicações web, somos imediatamente confrontados com um conjunto inteiro de novos desafios decorrentes como, por exemplo, a assincronicidade da comunicação. Isso necessita de novas estratégias de depuração, como o "console.log" e outros meios de depuração que se tornam cada vez mais importantes. Também devemos desenvolver uma boa compreensão dos princípios do ambiente de execução JavaScript e dos componentes React. Só ficar adivinhando não será suficiente.

Algo benéfico é inspecionar o estado do servidor back-end, por exemplo, através do navegador:

saída de dados JSON do back-end

Isso torna possível verificar se todos os dados que pretendíamos enviar realmente foram recebidos pelo servidor.

Na próxima parte do curso, aprenderemos a implementar nossa própria lógica no back-end. Em seguida, daremos uma olhada mais atenta em ferramentas como Postman, que nos ajuda a depurar nossas aplicações de servidor. Por ora, inspecionar o estado do json-server através do navegador é suficiente para nossas necessidades atuais.

O código para o estado atual de nossa aplicação pode ser encontrado na branch part2-5 neste repositório no GitHub.

Alterando a importância das notas

Vamos adicionar um botão ao lado de cada nota para podermos alternar sua importância.

Façamos as seguintes alterações no componente Note:

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

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

Adicionamos um botão ao componente e atribuímos o seu gerenciador de evento como a função toggleImportance ("alternarImportancia") passada nas props do componente.

O componente App define uma versão inicial da função gerenciadora de evento toggleImportanceOf ("alternarImportanciaDe") e passa para cada componente Note:

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  const toggleImportanceOf = (id) => {    console.log('importance of ' + id + ' needs to be toggled')  }
  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>      
      <ul>
        {notesToShow.map(note => 
          <Note
            key={note.id}
            note={note} 
            toggleImportance={() => toggleImportanceOf(note.id)}          />
        )}
      </ul>
      // ...
    </div>
  )
}

Note como cada nota recebe o seu próprio gerenciador de evento único, uma vez que o id de cada nota é único.

Por exemplo, se o note.id for 3, a função gerenciadora de evento retornada por toggleImportance (note.id) será:

() => { console.log('importance of 3 needs to be toggled') }

Um breve lembrete: a string impressa pelo gerenciador de evento é definida de um jeito Java, isto é, adicionando strings:

console.log('importance of ' + id + ' needs to be toggled')

A sintaxe das template strings, funcionalidade essa adicionada com o ES6, pode ser usada para escrever strings similares de uma maneira muito mais agradável:

console.log(`importance of ${id} needs to be toggled`)

Agora podemos usar a sintaxe de "dollar-bracket" (cifrão-colchete) para adicionar partes à string que avaliará expressões JavaScript como, por exemplo, o valor de uma variável. Observe que usamos crases em template strings em vez de aspas usadas em strings JavaScript regulares.

As notas individuais armazenadas no json-server do back-end podem ser modificadas de duas maneiras diferentes fazendo requisições HTTP para a URL única da nota. Podemos substituir a nota inteira com uma requisição HTTP PUT ou apenas alterar algumas das propriedades da nota com uma requisição HTTP PATCH.

A forma final da função gerenciadora de evento é a seguinte:

const toggleImportanceOf = id => {
  const url = `http://localhost:3001/notes/${id}`
  const note = notes.find(n => n.id === id)
  const changedNote = { ...note, important: !note.important }

  axios.put(url, changedNote).then(response => {
    setNotes(notes.map(n => n.id !== id ? n : response.data))
  })
}

Quase todas as linhas de código no corpo da função contêm detalhes importantes. A primeira linha define a URL única para cada recurso de nota com base em seu id.

O método de array find ("achar" ou "encontrar") é usado para encontrar a nota que queremos modificar e, em seguida, atribuí-la à variável note.

Depois disso, criamos um novo objeto que é uma cópia exata da antiga nota, exceto pela propriedade "important" que tem o valor invertido (de verdadeiro para falso ou de falso para verdadeiro).

O código para criar o novo objeto que usa a sintaxe object spread (espalhamento de objeto) pode parecer um tanto estranho de primeira vista:

const changedNote = { ...note, important: !note.important }

Na prática, { ...note } cria um novo objeto com cópias de todas as propriedades do objeto note. Quando adicionamos propriedades dentro das chaves depois do objeto spread, por exemplo, { ...note, important: true }, então o valor da propriedade important do novo objeto será true. Em nosso exemplo, a propriedade important obtém a negação de seu valor anterior no objeto original.

Há algumas coisas a se pontuar. Por que fizemos uma cópia do objeto "note" que queríamos modificar quando o seguinte código também parece funcionar?

const note = notes.find(n => n.id === id)
note.important = !note.important

axios.put(url, note).then(response => {
  // ...

Isso não é recomendado porque a variável note é uma referência a um item no array notes no estado do componente e, como sabemos, nunca devemos mudar diretamente o estado em React.

Também vale a pena notar que o novo objeto changedNote é apenas uma cópia superficial, o que significa que os valores do novo objeto são os mesmos que os valores do objeto antigo. Se os valores do objeto antigo eram objetos em si, então os valores copiados no novo objeto referenciariam os mesmos objetos que estavam no objeto antigo.

A nova nota é então enviada com uma requisição PUT ao back-end, onde ela substituirá o objeto antigo.

A função callback (função de retorno de chamada) define o estado do componente notes como um array novo que contém todos os itens do array notes anterior, exceto pela nota antiga, que é substituída pela versão atualizada dela retornada pelo servidor:

axios.put(url, changedNote).then(response => {
  setNotes(notes.map(note => note.id !== id ? note : response.data))
})

Isto é feito utilizando o método map:

notes.map(note => note.id !== id ? note : response.data)

O método map cria um array novo mapeando cada item do array antigo em um item no array novo. Em nosso exemplo, o array novo é criado de forma condicional de modo que se note.id !== id for verdadeiro, simplesmente copiamos o item do array antigo para o array novo. Se a condição for falsa, então o objeto de nota retornado pelo servidor é adicionado ao array.

Esse truque do método map pode parecer um pouco estranho agora no início, mas vale a pena gastar algum tempo entendendo como ele funciona. Nós usaremos este método muitas vezes ao longo do curso.

Separando a Comunicação com o Back-end em um Módulo Único

O componente App ficou um pouco carregado após adicionar o código para se comunicar com o servidor back-end. No espírito do princípio da responsabilidade única (single responsibility principle), achamos sensato extrair esta comunicação em seu próprio módulo.

Vamos criar um diretório src/services e adicionar lá um arquivo chamado notes.js:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  return axios.get(baseUrl)
}

const create = newObject => {
  return axios.post(baseUrl, newObject)
}

const update = (id, newObject) => {
  return axios.put(`${baseUrl}/${id}`, newObject)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

O módulo retorna um objeto que tem três funções (getAll, create, e update) como suas propriedades que lidam com as notas. As funções retornam diretamente as promessas retornadas pelos métodos da biblioteca axios.

O componente App usa a declaração import para ter acesso ao módulo:

import noteService from './services/notes'
const App = () => {

As funções do módulo podem ser usadas diretamente com a variável importada noteService, como a seguir:

const App = () => {
  // ...

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

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

    noteService      .update(id, changedNote)      .then(response => {        setNotes(notes.map(note => note.id !== id ? note : response.data))      })  }

  const addNote = (event) => {
    event.preventDefault()
    const noteObject = {
      content: newNote,
      important: Math.random() > 0.5
    }

    noteService      .create(noteObject)      .then(response => {        setNotes(notes.concat(response.data))        setNewNote('')      })  }

  // ...
}

export default App

Poderíamos levar nossa implementação um passo adiante. Quando o componente App usa as funções, ele recebe um objeto que contém a resposta inteira para a requisição HTTP:

noteService
  .getAll()
  .then(response => {
    setNotes(response.data)
  })

O componente App usa apenas a propriedade response.data do objeto de resposta.

Seria muito melhor de usar o módulo se, em vez de obter a resposta HTTP inteira, só obtivéssemos os dados da resposta. Então, o uso do módulo ficaria assim:

noteService
  .getAll()
  .then(initialNotes => {
    setNotes(initialNotes)
  })

Podemos fazer o que estamos planejando mudando o código no módulo da seguinte forma (o código atual contém um pouco de "copia e cola", mas vamos tolerar isso por enquanto):

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

Não retornamos mais a promessa entregue diretamente pelo axios. Em vez disso, atribuímos a promessa à variável request (requisição) e chamamos o seu método then:

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

A última linha em nossa função é simplesmente uma expressão mais compacta do mesmo código mostrado abaixo:

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => {    return response.data  })}

A função modificada getAll ainda retorna uma promessa, já que o método then de uma promessa também retorna uma promessa.

Depois de definir o parâmetro do método then para retornar diretamente response.data, conseguimos fazer com que a função getAll funcionasse da forma que desejávamos. Quando a requisição HTTP é bem-sucedida, a promessa retorna os dados enviados de volta na resposta do back-end.

Temos que atualizar o componente App para funcionar com as mudanças feitas em nosso módulo. Temos que consertar as funções callback dadas como parâmetros para os métodos do objeto noteService para que elas usem os dados de resposta que foram diretamente retornados:

const App = () => {
  // ...

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

  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))      })
  }

  const addNote = (event) => {
    event.preventDefault()
    const noteObject = {
      content: newNote,
      important: Math.random() > 0.5
    }

    noteService
      .create(noteObject)
      .then(returnedNote => {        setNotes(notes.concat(returnedNote))        setNewNote('')
      })
  }

  // ...
}

Tudo isso é bastante complicado, e tentar explicar pode deixar ainda mais difícil de entender. A internet está cheia de material sobre o tópico, como este.

O livro "Async and performance" da série de livros You do not know JS explica bem o tópico, mas é uma explicação de muitas páginas.

Promessas são vitais para o desenvolvimento em JavaScript moderno, e é extremamente recomendável investir um tempo razoável para entendê-las.

Uma sintaxe mais limpa para definir Objetos Literais (Object Literals)

O módulo que define os serviços relacionados às notas exporta atualmente um objeto com as propriedades getAll, create e update que são atribuídas a funções que gerenciam as notas.

A definição do módulo era:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

O módulo exporta o seguinte objeto, mesmo que pareça um tanto peculiar:

{
  getAll: getAll,
  create: create,
  update: update
}

As etiquetas (labels) à esquerda do dois-pontos na definição do objeto são as chaves (keys) do objeto, enquanto as à direita são as variáveis (variables) que são definidas dentro do módulo.

Como os nomes das chaves e das variáveis atribuídas são os mesmos, podemos escrever a definição do objeto com uma sintaxe mais compacta:

{ 
  getAll, 
  create, 
  update 
}

Como resultado, a definição do módulo simplifica-se da seguinte forma:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { getAll, create, update }

Ao definir o objeto usando esta notação mais curta, fazemos uso de uma nova funcionalidade que foi introduzida ao JavaScript por meio do ES6, permitindo uma maneira ligeiramente mais compacta de se definir objetos usando variáveis.

Para demonstrar essa nova funcionalidade, consideremos uma situação em que temos os seguintes valores atribuídos às variáveis:

const name = 'Leevi'
const age = 0

Em versões mais antigas de JavaScript, tínhamos que definir um objeto assim:

const person = {
  name: name,
  age: age,
}

No entanto, como tanto os campos de propriedades quanto os nomes de variáveis no objeto são os mesmos, basta escrever o seguinte, utilizando o padrão JavaScript ES6:

const person = { name, age }

O resultado é idêntico para ambas as expressões. Ambos criam um objeto com uma propriedade name com o valor Leevi e uma propriedade age com o valor 0.

Promessas e Erros

Se a nossa aplicação permitisse que os usuários excluíssem notas, poderíamos acabar em uma situação em que um usuário tenta mudar a importância de uma nota que já foi excluída do sistema.

Vamos simular essa situação fazendo com que a função getAll do serviço de notas retorne uma "nota de exemplo" ("'hardcoded' note ") que na verdade não existe no servidor back-end:

const getAll = () => {
  const request = axios.get(baseUrl)
  const nonExisting = {
    id: 10000,
    content: 'This note is not saved to server',
    important: true,
  }
  return request.then(response => response.data.concat(nonExisting))
}

Quando tentamos mudar a importância da nota, vemos no console a mensagem de erro abaixo, cujo conteúdo revela que o servidor back-end respondeu à nossa requisição HTTP PUT com um código de status 404 not found (não encontrado(a)).

erro 404 not found nas ferramentas do desenvolvedor

A aplicação deve ser capaz de lidar com estes tipos de erro de forma elegante. Os usuários não serão capazes de dizer que ocorreu um erro a menos que estejam com o console aberto. A única maneira de o erro ser percebido na aplicação é a importância da nota não ser alternada quando se clica no botão.

Mencionamos anteriormente que uma promessa pode estar em um dos três estados diferentes. Quando uma requisição HTTP falha, a promessa associada é rejeitada. O nosso código atual não gerencia por nenhum meio essa rejeição.

A rejeição de uma promessa é gerenciada fornecendo ao método then uma segunda função callback, que é chamada na situação em que a promessa é rejeitada.

A forma mais comum de adicionar um gerenciador para promessas rejeitadas é usar o método catch (grosso modo, "pegar" ou "capturar").

Na prática, o gerenciador de erro para promessas rejeitadas é definido da seguinte forma:

axios
  .get('http://example.com/probably_will_fail')
  .then(response => {
    console.log('success!')
  })
  .catch(error => {
    console.log('fail')
  })

Se a requisição falhar, o gerenciador de evento registrado com o método catch é chamado.

O método catch é frequentemente utilizado colocando-o mais no final no encadeamento de promessas.

Quando a nossa aplicação faz uma requisição HTTP, na verdade estamos criando um encadeamento de promessa(s) (promise chain):

axios
  .put(`${baseUrl}/${id}`, newObject)
  .then(response => response.data)
  .then(changedNote => {
    // ...
  })

O método catch pode ser usado para definir uma função gerenciadora no final de um encadeamento de promessas, que é chamada/acionada uma vez que qualquer promessa no encadeamento lance uma exceção e a promessa se torne rejeitada.

axios
  .put(`${baseUrl}/${id}`, newObject)
  .then(response => response.data)
  .then(changedNote => {
    // ...
  })
  .catch(error => {
    console.log('fail')
  })

Vamos usar essa funcionalidade e registrar um gerenciador de erro no componente App:

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 => {      alert(        `the note '${note.content}' was already deleted from server`      )      setNotes(notes.filter(n => n.id !== id))    })}

A mensagem de erro é exibida ao usuário com a antiga e confiável caixa de diálogo alert (alerta), e a nota excluída é filtrada do estado.

A remoção de uma nota já excluída do estado da aplicação é feita com o método de array filter (filtrar), que retorna um array novo com apenas os itens da lista para os quais a função passada como parâmetro retorna verdadeiro para:

notes.filter(n => n.id !== id)

Não é uma boa ideia usar o "alert" em aplicações React mais sérias. Em breve aprenderemos uma maneira mais avançada de exibir mensagens e notificações aos usuários. No entanto, há situações em que um método simples e testado como o alert pode funcionar como um ponto de partida. Uma maneira mais avançada sempre pode ser adicionada posteriormente, desde que haja tempo e energia disponíveis para isso.

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

Juramento do Programador Full Stack

Chegou novamente a hora dos exercícios. A complexidade de nossa aplicação está aumentando, já que além de cuidarmos dos componentes React no front-end, também temos um back-end que persiste os dados da aplicação.

Para lidar com essa complexidade crescente, devemos estender o Juramento do Programador Web para o Juramento do Programador Full Stack, que nos lembrará de garantir com que a comunicação entre front e back-end aconteça como planejado.

Então aqui está o juramento atualizado:

Desenvolvimento Full Stack é algo extremamente difícil, e é por isso que eu usarei todos os meios possíveis para torná-lo mais fácil:

  • Eu manterei meu Console do navegador sempre aberto;
  • Eu usarei a guia Rede das Ferramentas do Desenvolvedor do navegador para garantir que o front-end e o back-end estejam se comunicando da forma que eu planejei ;
  • Eu ficarei de olho no estado do servidor para garantir que os dados enviados pelo front-end estejam sendo salvos da forma que eu planejei;
  • Eu vou progredir aos poucos, passo a passo;
  • Eu escreverei muitas instruções console.log para ter certeza de que estou entendendo como o código se comporta e para me ajudar a identificar os erros;
  • Se meu código não funcionar, não escreverei mais nenhuma linha no código. Em vez disso, começarei a excluir o código até que funcione ou retornarei ao estado em que tudo ainda estava funcionando; e
  • Quando eu pedir ajuda no canal do Discord do curso ou em outro lugar, formularei minhas perguntas de forma adequada. Veja aqui como pedir ajuda.