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:
O formulário de login aparece quando o usuário pressiona o botão login:
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:
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:
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:
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:
O react-devtools também revela que o componente não tem um nome:
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.