Saltar al contenido

b

props.children y proptypes

Mostrando el formulario de inicio de sesión solo cuando sea apropiado

Modifiquemos la aplicación para que el formulario de inicio de sesión no se muestre por defecto:

navegador mostrando botón de login por defecto

El formulario de inicio de sesión aparece cuando el usuario presiona el botón login:

usuario en vista de formulario de login a punto de presionar el botón cancel

El usuario puede cerrar el formulario de inicio de sesión haciendo clic en el botón cancel.

Comencemos extrayendo el formulario de inicio de sesión en su propio componente:

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

El estado y todas las funciones relacionadas con él se definen fuera del componente y se pasan al componente como props.

Ten en cuenta que los props se asignan a las variables mediante la desestructuración, lo que significa que en lugar de escribir:

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

donde se accede a las propiedades del objeto props mediante, por ejemplo, props.handleSubmit, las propiedades se asignan directamente a sus propias variables.

Una forma rápida de implementar la funcionalidad es cambiar la función loginForm del componente App así:

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

  // ...
}

El estado de el componente App ahora contiene el booleano loginVisible, que define si el formulario de inicio de sesión se debe mostrar al usuario o no.

El valor de loginVisible se alterna con dos botones. Ambos botones tienen sus controladores de eventos definidos directamente en el componente:

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

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

La visibilidad del componente se define dándole al componente una regla de estilo en línea, donde el valor de la propiedad display es none si no queremos que se muestre el componente:

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

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

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

Una vez más estamos utilizando el operador ternario "signo de interrogación". Si loginVisible es true, entonces la regla CSS del componente será:

display: 'none';

Si loginVisible es false, entonces display no recibirá ningún valor relacionado con la visibilidad del componente.

Los componentes hijos, también conocidos como props.children

El código relacionado con la gestión de la visibilidad del formulario de inicio de sesión podría considerarse su propia entidad lógica, y por esta razón, sería bueno extraerlo del componente App en su propio componente independiente.

Nuestro objetivo es implementar un nuevo componente Togglable que se pueda usar de la siguiente manera:

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

La forma en que se utiliza el componente es ligeramente diferente a la de nuestros componentes anteriores. El componente tiene etiquetas de apertura y cierre que rodean un componente LoginForm. En la terminología de React, LoginForm es un componente hijo de Togglable.

Podemos agregar cualquier elemento de React que queramos entre las etiquetas de apertura y cierre de Togglable, como este, por ejemplo:

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

El código para el componente Togglable se muestra a continuación:

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

La parte nueva e interesante del código es props.children, que se utiliza para hacer referencia a los componentes hijos del componente. Los componentes hijos son los elementos de React que definimos entre las etiquetas de apertura y cierre de un componente.

Esta vez, los hijos son renderizados en el código que se utiliza para renderizar el componente en sí:

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

A diferencia de los props "normales" que hemos visto antes, React agrega automáticamente children y siempre existe. Si un componente se define con una etiqueta /> de cierre automático, como esta:

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

Entonces props.children es un array vacío.

El componente Togglable es reutilizable y podemos usarlo para agregar una funcionalidad de alternancia de visibilidad similar al formulario que se usa para crear nuevas notas.

Antes de hacer eso, extraigamos el formulario para crear notas en su propio 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>
  )
}

A continuación, definamos el componente de formulario dentro de un componente Togglable:

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

Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part5-4 de este repositorio de GitHub.

Estado de los formularios

El estado de la aplicación se encuentra actualmente en el componente App.

La documentación de React dice lo siguiente sobre dónde colocar el estado:

A veces, quieres que el estado de dos componentes cambie siempre al mismo tiempo. Para hacerlo, elimina el estado de ambos, muévelo al componente padre más cercano que tengan en común, y luego pásalo a ellos a través de props. Esto se conoce como "elevar el estado", y es una de las cosas más comunes que harás al escribir código React.

Si pensamos en el estado de los formularios, por ejemplo, el contenido de una nueva nota antes de que se haya creado, el componente App no lo necesita para nada. También podríamos mover el estado de los formularios a los componentes correspondientes.

El componente para crear una nueva nota cambia así:

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: Al mismo tiempo, cambiamos el comportamiento de la aplicación para que las nuevas notas sean importantes por defecto, es decir, el campo important obtiene el valor true.

La variable de estado newNote y el controlador de eventos responsable de cambiarlo se han movido del componente App al componente responsable del formulario de la nota.

Solo queda un prop, la función createNote, que el formulario llama cuando se crea una nueva nota.

El componente App se vuelve más simple ahora que nos hemos deshecho del estado newNote y su controlador de eventos. La función addNote para crear nuevas notas recibe una nueva nota como parámetro, y la función es el único prop que enviamos al formulario:

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

  // ...
}

Podríamos hacer lo mismo con el formulario de inicio de sesión, pero lo dejaremos para un ejercicio opcional.

El código de la aplicación se puede encontrar en GitHub, rama part5-5.

Referencias a componentes con ref

Nuestra implementación actual es bastante buena, pero tiene un aspecto que podría mejorarse.

Después de crear una nueva nota, tendría sentido ocultar el formulario de nueva nota. Actualmente, el formulario permanece visible. Hay un pequeño problema al ocultarlo, la visibilidad se controla con la variable visible dentro del componente Togglable.

Una solución a esto sería mover el control del estado del componente Togglable fuera del componente. Sin embargo, no lo haremos ahora, porque queremos que el componente sea responsable de su propio estado. Por lo tanto, tenemos que encontrar otra solución y hallar un mecanismo para cambiar el estado del componente externamente.

Hay varias formas diferentes de implementar el acceso a las funciones de un componente desde fuera del componente, pero usemos el mecanismo de ref de React, que ofrece una referencia al componente.

Hagamos los siguientes cambios en el 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>
  )

  // ...
}

El hook useRef se utiliza para crear una referencia noteFormRef, que se asigna al componente Togglable que contiene el formulario para crear la nota. La variable noteFormRef actúa como referencia al componente. Este hook asegura que se mantenga la misma referencia (ref) en todas las re-renderizaciones del componente.

También realizamos los siguientes cambios en el 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

La función que crea el componente está envuelta dentro de una llamada a la función forwardRef. De esta manera el componente puede acceder a la referencia que le fue asignada.

El componente usa el hook useImperativeHandle para que su función toggleVisibility esté disponible fuera del componente.

Ahora podemos ocultar el formulario llamando a noteFormRef.current.toggleVisibility() después de que se haya creado una nueva nota:

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

En resumen, la función useImperativeHandle es un hook de React, que se usa para definir funciones en un componente que se pueden invocar desde fuera del componente.

Este truco funciona para cambiar el estado de un componente, pero parece un poco desagradable. Podríamos haber logrado la misma funcionalidad con código un poco más limpio usando los "viejos" componentes de clase de React. Analizaremos estos componentes de clase durante la parte 7 del material del curso. Hasta ahora, esta es la única situación en la que el uso de hooks de React conduce a un código que no es más limpio que con los componentes de clase.

También hay otros casos de uso para las refs además de acceder a los componentes de React.

Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part5-6 de este repositorio de GitHub.

Un punto sobre los componentes

Cuando definimos un componente en React:

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

Y lo usamos así:

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

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

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

Creamos tres instancias separadas del componente que tienen su propio estado separado:

tres componentes togglable en el navegador

El atributo ref se utiliza para asignar una referencia a cada uno de los componentes en las variables togglable1, togglable2 y togglable3.

El juramento actualizado del desarrollador full stack

El número de componentes aumenta. Al mismo tiempo, aumenta la probabilidad de encontrarnos en una situación en la que buscamos un error en el lugar equivocado. Por lo tanto, necesitamos ser aún más sistemáticos.

Entonces, debemos extender una vez más nuestro juramento:

El desarrollo full stack es extremadamente difícil, por eso utilizaré todos los medios posibles para hacerlo lo más fácil posible

  • Mantendré abierta la consola de desarrollo del navegador todo el tiempo
  • Utilizaré la pestaña network de las herramientas de desarrollo del navegador para asegurarme de que el frontend y el backend estén comunicándose como espero
  • Mantendré un ojo constantemente en el estado del servidor para asegurarme de que los datos enviados por el frontend se guarden allí como espero
  • Mantendré un ojo en la base de datos: ¿el backend guarda los datos allí en el formato correcto?
  • Progresaré con pequeños pasos
  • cuando sospeche que hay un error en el frontend, me asegurare de que el backend funcione correctamente
  • cuando sospeche que hay un error en el backend, me asegurare de que el frontend funcione correctamente
  • Escribiré muchos console.log para asegurarme de entender cómo se comportan el código y las pruebas y para ayudar a localizar problemas
  • Si mi código no funciona, no escribiré más código. En cambio, empezare a eliminarlo hasta que funcione o simplemente regresare a un estado en el que todo funcionaba
  • Si una prueba no pasa, me asegurare de que la funcionalidad probada funciona correctamente en la aplicación
  • Cuando pida ayuda en el canal de Discord o Telegram del curso o en cualquier otro lugar, formularé mis preguntas correctamente, consulta aquí cómo pedir ayuda

PropTypes

El componente Togglable asume que se le da el texto para el botón a través del prop buttonLabel. Si nos olvidamos de definir este prop al componente:

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

La aplicación funciona, pero el navegador muestra un botón sin texto.

Nos gustaría hacer cumplir que cuando se usa el componente Togglable, se debe dar un valor al prop de texto del botón.

Los props esperados y requeridos de un componente se pueden definir con el paquete prop-types. Instalemos el paquete:

npm install prop-types

Podemos definir el prop buttonLabel como un prop obligatorio o required de tipo string como se muestra a continuación:

import PropTypes from 'prop-types'

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

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

La consola mostrará el siguiente mensaje de error si el prop se deja sin definir:

error en la consola, buttonLabel es undefined

La aplicación todavía funciona y nada nos obliga a definir props a pesar de las definiciones de PropTypes. Eso sí, es extremadamente poco profesional dejar cualquier output de color rojo en la consola del navegador.

También definamos PropTypes para el 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
}

Si el tipo de un prop pasado es incorrecto, por ejemplo, si intentamos definir el prop handleSubmit como un string, esto resultará en la siguiente advertencia:

error de consola, handleSubmit espera una función

ESlint

En la parte 3 configuramos la herramienta de estilo de código para el backend ESlint. Utilicemos ESlint también en el frontend.

Vite ha instalado ESlint en el proyecto de forma predeterminada, por lo que todo lo que nos queda por hacer es definir nuestra configuración deseada en el archivo .eslintrc.cjs.

Creemos un archivo .eslintrc.js con el siguiente contenido:

module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  settings: { react: { version: '18.2' } },
  plugins: ['react-refresh'],
  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/react-in-jsx-scope": "off",
    "react/prop-types": 0,
    "no-unused-vars": 0    
  },
}

NOTA: Si estás utilizando Visual Studio Code junto con el plugin ESLint, es posible que debas agregar una configuración de espacio de trabajo adicional para que funcione. Si ves Failed to load plugin react: Cannot find module 'eslint-plugin-react', necesitas una configuración adicional. Agregar la línea "eslint.workingDirectories": [{ "mode": "auto" }] a settings.json en el espacio de trabajo parece funcionar. Mira esto para obtener más información.

Vamos a crear un archivo .eslintignore con el siguiente contenido en la raíz del repositorio

node_modules
dist
.eslintrc.cjs
vite.config.js

Ahora los directorios dist y node_modules se omitirán al realizar el linting.

Como de costumbre, puedes realizar el linting desde la línea de comandos con el siguiente comando:

npm run lint

o usando el plugin de Eslint del editor.

El componente Togglable está causando una advertencia desagradable Component definition is missing display name:

vscode mostrando error en la definición del componente

Las react-devtools también muestran que el componente no tiene un nombre:

react devtools mostrando forwardRef como anónimo

Afortunadamente, esto es fácil de solucionar.

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

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

Togglable.displayName = 'Togglable'
export default Togglable

Puedes encontrar el código para nuestra aplicación actual en su totalidad en la rama part5-7 de este repositorio de GitHub.