Pular para o conteúdo

b

props.children e proptypes

Mostrando o formulário de login apenas quando apropriado

Vamos modificar a aplicação para que o formulário de login não seja exibido por padrão:

navegador mostrando o botão de login por padrão

O formulário de login aparece quando o usuário pressiona o botão login:

usuário na tela de login prestes a apertar o botão cancelar

O usuário pode fechar o formulário de login clicando no botão cancelar.

Vamos começar extraindo o formulário de login para um componente próprio:

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
  return (
    <div>
      <h2>Login</h2>

      <form onSubmit={handleSubmit}>
        <div>
          username
          <input
            value={username}
            onChange={handleUsernameChange}
          />
        </div>
        <div>
          password
          <input
            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
      </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

export default LoginForm

O estado e todas as funções relacionadas a ele são definidos fora do componente e são passados para o componente por meio de props.

Perceba que as props são atribuídas a variáveis ​​através de destructuring, o que significa que, em vez de escrever:

const LoginForm = (props) => {
  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={props.handleSubmit}>
        <div>
          username
          <input
            value={props.username}
            onChange={props.handleChange}
            name="username"
          />
        </div>
        // ...
        <button type="submit">login</button>
      </form>
    </div>
  )
}

Onde as propriedades do objeto props são acessadas por meio de, por exemplo, props.handleSubmit, as propriedades são atribuídas diretamente às suas próprias variáveis.

Uma forma rápida de implementar a funcionalidade é alterar a função loginForm do componente App da seguinte maneira:

const App = () => {
  const [loginVisible, setLoginVisible] = useState(false)
  // ...

  const loginForm = () => {
    const hideWhenVisible = { display: loginVisible ? 'none' : '' }
    const showWhenVisible = { display: loginVisible ? '' : 'none' }

    return (
      <div>
        <div style={hideWhenVisible}>
          <button onClick={() => setLoginVisible(true)}>log in</button>
        </div>
        <div style={showWhenVisible}>
          <LoginForm
            username={username}
            password={password}
            handleUsernameChange={({ target }) => setUsername(target.value)}
            handlePasswordChange={({ target }) => setPassword(target.value)}
            handleSubmit={handleLogin}
          />
          <button onClick={() => setLoginVisible(false)}>cancel</button>
        </div>
      </div>
    )
  }

  // ...
}

O estado do componente App agora contém o boolean loginVisible, que define se o formulário de login deve ser exibido ao usuário ou não.

O valor de loginVisible é alternado com dois botões. Ambos os botões têm seus gerenciadores de eventos definidos diretamente no componente:

<button onClick={() => setLoginVisible(true)}>log in</button>

<button onClick={() => setLoginVisible(false)}>cancel</button>

A visibilidade do componente é definida atribuindo uma regra de estilo inline, onde o valor da propriedade display é none se não quisermos que o componente seja exibido:

const hideWhenVisible = { display: loginVisible ? 'none' : '' }
const showWhenVisible = { display: loginVisible ? '' : 'none' }

<div style={hideWhenVisible}>
  // button
</div>

<div style={showWhenVisible}>
  // button
</div>

Nós estamos usando novamente o operador ternário "ponto de interrogação". Se loginVisible for true, então a regra CSS do componente será:

display: 'none';

Se loginVisible for false, então display não receberá nenhum valor relacionado à visibilidade do componente.

Os componentes filhos, conhecidos como props.children

O código relacionado ao gerenciamento da visibilidade do formulário de login poderia ser considerado uma entidade lógica própria, e por esse motivo, seria bom extrai-lo do componente App para um componente separado.

Nosso objetivo é implementar um novo componente Togglable que possa ser usado da seguinte maneira:

<Togglable buttonLabel='login'>
  <LoginForm
    username={username}
    password={password}
    handleUsernameChange={({ target }) => setUsername(target.value)}
    handlePasswordChange={({ target }) => setPassword(target.value)}
    handleSubmit={handleLogin}
  />
</Togglable>

A maneira como o componente é usado é ligeiramente diferente dos nossos componentes anteriores. O componente tem tags de abertura e fechamento que cercam um componente LoginForm. Na terminologia React, LoginForm é um componente filho de Togglable.

Nós podemos adicionar qualquer elemento React que quisermos entre as tags de abertura e fechamento de Togglable, como este, por exemplo:

<Togglable buttonLabel="reveal">
  <p>this line is at start hidden</p>
  <p>also this is hidden</p>
</Togglable>

O código do componente Togglable é mostrado abaixo:

import { useState } from 'react'

const Togglable = (props) => {
  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
}

export default Togglable

A nova e interessante parte do código é a props.children, que é usada para referenciar os componentes filhos do componente. Os componentes filhos são os elementos React que definimos entre as tags de abertura e fechamento de um componente.

Dessa vez, os children são renderizados no código que é usado para renderizar o próprio componente:

<div style={showWhenVisible}>
  {props.children}
  <button onClick={toggleVisibility}>cancel</button>
</div>

Diferente das props "normais" que vimos antes, children é adicionada automaticamente pelo React e sempre existe na aplicação. Se um componente é definido com uma tag de fechamento automático />, como este:

<Note
  key={note.id}
  note={note}
  toggleImportance={() => toggleImportanceOf(note.id)}
/>

Então props.children é um array vazio.

O componente Togglable é reutilizável e podemos usá-lo para adicionar funcionalidade semelhante de alternância de visibilidade ao formulário usado para criar novas anotações.

Antes de fazermos isso, vamos extrair o formulário para criar notas em um componente:

const NoteForm = ({ onSubmit, handleChange, value}) => {
  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={onSubmit}>
        <input
          value={value}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

Depois vamos definir o componente do formulário dentro de um componente Togglable:

<Togglable buttonLabel="new note">
  <NoteForm
    onSubmit={addNote}
    value={newNote}
    handleChange={handleNoteChange}
  />
</Togglable>

Você pode encontrar o código completo da nossa aplicação atual no branch part5-4 deste repositório do GitHub.

Estado dos formulários

O estado da aplicação atualmente está no componente App.

A documentação do React diz o seguinte sobre onde colocar o estado:

Muitas vezes, vários componentes precisam refletir os mesmos dados em mudança. Recomendamos levantar o estado compartilhado até o ancestral comum mais próximo.

Se pensarmos no estado dos formulários, como por exemplo o conteúdo de uma nova nota antes que ela tenha sido criada, o componente App não precisa dele para nada. Nós poderíamos simplesmente mover o estado dos formulários para os componentes correspondentes.

O componente para uma nota muda da seguinte maneira:

import { useState } from 'react'

const NoteForm = ({ createNote }) => {
  const [newNote, setNewNote] = useState('')

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: true
    })

    setNewNote('')
  }

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={event => setNewNote(event.target.value)}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

export default NoteForm

NOTA Ao mesmo tempo, mudamos o comportamento da aplicação para que as novas notas sejam importantes por padrão, ou seja, o campo important recebe o valor true.

O atributo de estado newNote e o gerenciador de eventos responsável por alterá-lo foram movidos do componente App para o componente responsável pelo formulário de notas.

Há apenas uma prop restante, a função createNote, que o formulário chama quando uma nova nota é criada.

O componente App fica mais simples agora que nós nos livramos do estado newNote e do seu gerenciador de eventos. A função addNote para criar novas notas recebe uma nova nota como parâmetro, e a função é a única prop que enviamos para o formulário:

const App = () => {
  // ...
  const addNote = (noteObject) => {    noteService
      .create(noteObject)
      .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
  const noteForm = () => (
    <Togglable buttonLabel='new note'>
      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

Nós poderíamos fazer o mesmo para o formulário de login, mas vamos deixar isso para um exercício opcional.

O código da aplicação pode ser encontrado no GitHub, branch part5-5.

Referências para componentes com ref

Nossa implementação atual é muito boa; ela tem um aspecto que poderia ser melhorado.

Depois que uma nova nota é criada, faria sentido esconder o formulário de nova nota. Atualmente, o formulário permanece visível. Há um pequeno problema em ocultar o formulário. A visibilidade é controlada com a variável visible dentro do componente Togglable. Como podemos acessá-la fora do componente?

Há muitas maneiras de implementar o fechamento do formulário a partir do componente pai, mas vamos usar o mecanismo ref

Vamos fazer as seguintes alterações no componente App:

import { useState, useEffect, useRef } from 'react'
const App = () => {
  // ...
  const noteFormRef = useRef()
  const noteForm = () => (
    <Togglable buttonLabel='new note' ref={noteFormRef}>      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

O hook useRef é usado para criar uma referência noteFormRef, que é atribuída ao componente Togglable que contém o formulário de criação de notas. A variável noteFormRef atua como uma referência ao componente. Este hook garante a mesma referência (ref) que é mantida durante as re-renderizações do componente.

Nós também fazemos as seguintes alterações no componente Togglable:

import { useState, forwardRef, useImperativeHandle } from 'react'
const Togglable = forwardRef((props, refs) => {  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  useImperativeHandle(refs, () => {    return {      toggleVisibility    }  })
  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
})
export default Togglable

A função que cria o componente é envolvida dentro de uma chamada de função forwardRef. Desta forma, o componente pode acessar a referência (ref) que é atribuída a ele.

O componente usa o hook useImperativeHandle para tornar a função toggleVisibility disponível fora do componente.

Nós podemos agora ocultar o formulário chamando noteFormRef.current.toggleVisibility() após a criação de uma nova nota:

const App = () => {
  // ...
  const addNote = (noteObject) => {
    noteFormRef.current.toggleVisibility()    noteService
      .create(noteObject)
      .then(returnedNote => {     
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
}

Para recapitular, a função useImperativeHandle é um hook do React, que é usado para definir funções em um componente, que podem ser invocadas de fora do componente.

Esse truque funciona para alterar o estado de um componente, mas parece um pouco desagradável. Poderíamos ter conseguido a mesma funcionalidade com um código um pouco mais limpo usando componentes baseados em classe do "antigo React". Vamos dar uma olhada nesses componentes de classe durante a parte 7 do material do curso. Até agora, esta é a única situação em que o uso de hooks do React leva a um código que não é mais limpo do que com componentes de classe.

Existem também outros casos de uso para referências além de acessar componentes React.

Você pode encontrar o código para nossa aplicação atual em sua totalidade na branch part5-6 deste repositório do GitHub.

Um ponto sobre componentes

Quando definimos um componente no React:

const Togglable = () => ...
  // ...
}

E o usamos assim:

<div>
  <Togglable buttonLabel="1" ref={togglable1}>
    first
  </Togglable>

  <Togglable buttonLabel="2" ref={togglable2}>
    second
  </Togglable>

  <Togglable buttonLabel="3" ref={togglable3}>
    third
  </Togglable>
</div>

Nós criamos três instâncias separadas do componente que todas têm seu próprio estado:

navegador com três componentes togglable

O atributo ref é usado para atribuir uma referência a cada um dos componentes nas variáveis togglable1, togglable2 e togglable3.

O juramento atualizado do desenvolvedor full stack

O número de partes móveis aumenta. Ao mesmo tempo, a probabilidade de acabar em uma situação em que estamos procurando um bug no lugar errado aumenta. Portanto, precisamos ser ainda mais sistemáticos.

Então, devemos estender novamente nosso juramento:

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

  • Eu irei ter o meu console de desenvolvedor do navegador aberto o tempo todo
  • Eu irei usar a aba "rede" das ferramentas de desenvolvedor do navegador para garantir que o frontend e o backend estejam se comunicando como eu espero
  • Eu irei constantemente vigiar o estado do servidor para assegurar que os dados enviados lá pelo frontend sejam salvos como eu espero
  • Eu irei ficar de olho no banco de dados: os dados salvos lá pelo backend estão no formato correto?
  • Eu avanço com pequenos passos
  • Quando eu suspeito que há um bug no frontend, eu me certifico de que o backend funciona corretamente
  • Quando eu suspeito que há um bug no backend, eu me certifico de que o frontend funciona corretamente
  • Eu irei escrever vários console.log para me certificar de que eu entendo como o código e os testes se comportam e para ajudar a localizar problemas
  • Se meu código não funciona, eu não escreverei mais código. Em vez disso, eu começo a apagar o código até que ele funcione ou apenas volte para um estado em que tudo ainda estava funcionando
  • Se um teste não passa, eu me certifico de que a funcionalidade testada funciona com certeza na aplicação
  • Quando eu peço ajuda no canal do Discord do curso ou em outro lugar, eu formulo minhas perguntas corretamente, veja aqui como pedir ajuda

PropTypes

O componente Togglable assume que ele recebe o texto para o botão via a prop buttonLabel. Se esquecermos de defini-lo para o componente:

<Togglable> buttonLabel forgotten... </Togglable>

A aplicação funciona, mas o navegador renderiza um botão que não tem texto de label.

Nós gostaríamos de impor que quando o componente Togglable é usado, o texto do label do botão deve ter um valor atribuído a ele.

As props esperadas e obrigatórias de um componente podem ser definidas com o pacote prop-types. Vamos instalar o pacote:

npm install prop-types

Nós podemos definir a prop buttonLabel como uma prop do tipo string obrigatória como mostrado abaixo:

import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ..
})

Togglable.propTypes = {
  buttonLabel: PropTypes.string.isRequired
}

O console irá exibir a seguinte mensagem de erro se a prop for deixada indefinida:

erro no console dizendo que buttonLabel está indefinido

A aplicação ainda funciona e nada nos força a definir props apesar das definições de PropTypes. Tenha em mente que é extremamente não profissional deixar qualquer saída vermelha no console do navegador.

Vamos também definir PropTypes para o componente LoginForm:

import PropTypes from 'prop-types'

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
    // ...
  }

LoginForm.propTypes = {
  handleSubmit: PropTypes.func.isRequired,
  handleUsernameChange: PropTypes.func.isRequired,
  handlePasswordChange: PropTypes.func.isRequired,
  username: PropTypes.string.isRequired,
  password: PropTypes.string.isRequired
}

Se o tipo de uma prop passada estiver errado, por exemplo, se tentarmos definir a prop handleSubmit como uma string, então isso resultará no seguinte aviso:

erro no console dizendo que handleSubmit esperava uma função

ESlint

Na parte 3 nós configuramos a ferramenta de estilo de código ESlint para o backend. Vamos usar o ESlint no frontend também.

Create-react-app instalou o ESlint no projeto por padrão, então tudo que precisamos fazer é definir nossa configuração desejada no arquivo .eslintrc.js.

Obs.: não execute o comando eslint --init. Ele irá instalar a última versão do ESlint que não é compatível com o arquivo de configuração criado pelo create-react-app!

Depois, nós iremos começar a testar o frontend e para evitar erros de linter indesejados e irrelevantes nós iremos instalar o pacote eslint-plugin-jest:

npm install --save-dev eslint-plugin-jest

Vamos criar um arquivo .eslintrc.js com o seguinte conteúdo:

/* eslint-env node */
module.exports = {
  "env": {
      "browser": true,
      "es6": true,
      "jest/globals": true 
  },
  "extends": [ 
      "eslint:recommended",
      "plugin:react/recommended"
  ],
  "parserOptions": {
      "ecmaFeatures": {
          "jsx": true
      },
      "ecmaVersion": 2018,
      "sourceType": "module"
  },
  "plugins": [
      "react", "jest"
  ],
  "rules": {
      "indent": [
          "error",
          2  
      ],
      "linebreak-style": [
          "error",
          "unix"
      ],
      "quotes": [
          "error",
          "single"
      ],
      "semi": [
          "error",
          "never"
      ],
      "eqeqeq": "error",
      "no-trailing-spaces": "error",
      "object-curly-spacing": [
          "error", "always"
      ],
      "arrow-spacing": [
          "error", { "before": true, "after": true }
      ],
      "no-console": 0,
      "react/prop-types": 0,
      "react/react-in-jsx-scope": "off"
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

NOTA: Se estiver usando o Visual Studio Code junto com o plugin ESLint, você pode precisar adicionar uma configuração de workspace para que ele funcione. Se você estiver vendo Failed to load plugin react: Cannot find module 'eslint-plugin-react', uma configuração adicional é necessária. Adicionando a linha "eslint.workingDirectories": [{ "mode": "auto" }] em settings.json no workspace parece funcionar. Veja aqui para mais informações.

Vamos criar um arquivo .eslintignore com o seguinte conteúdo na raiz do repositório:

node_modules
build
.eslintrc.js

Agora os diretórios build e node_modules serão ignorados quando o lint for executado.

Vamos também criar um script npm para executar o lint:

{
  // ...
  {
    "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "eslint": "eslint ."  },
  // ...
}
{
  // ...
  {
    "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "eslint": "eslint ."  },
  // ...
}

O componente Togglable causa um aviso desagradável Component definition is missing display name:

vscode mostrando erro de definição de componente

O react-devtools também revela que o componente não tem um nome:

react devtools mostrando forwardRef como anônimo

Felizmente, isso é fácil de consertar

import { useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ...
})

Togglable.displayName = 'Togglable'
export default Togglable

Você pode encontrar o código para nossa aplicação atual na sua totalidade na branch part5-7 desse repositório do GitHub.

Note que o create-react-app também tem uma configuração padrão do ESLint, que nós agora substituímos. A documentação menciona que é ok substituir o padrão, mas não nos encoraja a fazê-lo: Nós altamente recomendamos estender a configuração base, pois removê-la pode introduzir problemas difíceis de encontrar.