En este primer ejemplo veremos los primeros conceptos de la tecnología React
- componentes
- propiedades (props)
- manejo del estado de un componente
Lo hacemos siguiendo los pasos que están en la wiki.
La aplicación levanta como una lambda, es decir una función que devuelve como output el HTML a mostrar (lo que debe renderizar):
const App = () => (
<div className="App">
<Saludo nombre="Martín" />
<Saludo nombre="Mariano" />
<Contador />
<Contador />
</div>
)
Recordemos que const App = () => {}
es el formato en arrow functions similar a
function App() {
return (
...
)
}
Esta forma de definir un componente presentacional de React es la variante funcional, porque pensamos una página como una función. Ésta es la variante oficial desde 2019. El código que escribimos es JSX o TSX, que consiste en
- tags HTML
- más código javascript/typescript que se evalúa
- más los componentes React que nosotros definimos
- más componentes React que importamos de bibliotecas de terceros
El componente que saluda recibe como parámetro un string, que corresponde al nombre de la persona que queremos saludar. El componente principal de React llama al que saluda mediante un atributo:
<Saludo nombre="Martín" />
Saludo se define como otro componente funcional que sabe mostrar un div:
const Saludo = (props) => {
return (
<p data-testid="saludo">
Hola, {props.nombre}
</p>
)
}
Aquí vemos que lo que enviamos con el siguiente formato
<Saludo nombre="Martín" />
lo recibimos como
{props.nombre}
en el componente original. ¿Pero qué es props?
Props es un mapa de propiedades que cada componente mantiene en forma aislada. De esa manera podemos tener dos componentes que saludan y cada uno muestra diferente información en base al parámetro que enviamos.
El contador es un componente que tiene cambios de estado. Comienza inicialmente en cero, pero luego el usuario puede
- hacer click en el botón que suma uno al contador
- o hacer click en el botón que resta uno al contador
Mientras que las props no modifican el estado del componente (son de lectura), el state está íntimamente relacionado con el ciclo de vida de un componente React. Cuando un evento dispara un cambio, se genera un nuevo estado que provoca que se renderice nuevamente la vista.
En nuestro caso, el estado es simplemente un número.
Al iniciar el componente el contador será 0, y cuando el usuario presione click sobre el botón Sumar o Restar se debe generar un nuevo estado, con el contador incrementado o decrementado.
Para poder mantener un estado, vamos a utilizar un hook que nos ofrece dos funciones: una para obtener el valor del estado, y el otro para generar un nuevo estado, lo que va a producir un nuevo render.
export const Contador = (props) => {
const [contador, setContador] = useState(0)
La función useState
- recibe un valor (en este caso el número 0)
- y devuelve una lista de 2 funciones: el primer elemento es una función que permite conocer el valor del estado y el segundo permite asignarle un nuevo valor (generando un nuevo estado)
Por ejemplo: contador()
nos devuelve inicialmente 0, setContador(1)
produce un nuevo estado que dispara el render de la función Contador.
return (
<Card>
<CardContent>
...
<h3 data-testid="contadorValue">
{contador}
</h3>
<Button
variant="contained" data-testid="restar" size="medium" color="secondary"
onClick={restar}>
-
</Button>
<Button
variant="contained" data-testid="sumar" size="medium" color="primary"
onClick={sumar}>
+
</Button>
<br />
</CardContent>
</Card>
)
Debido a que además el único elemento del tag asociado al state es el tag Typography que muestra un título H3 de HTML, React manipula en forma inteligente el DOM para que la interacción con el browser sea mínima. Con F12 activamos las herramientas de desarrollo del navegador:
- Inicialmente, nuestro estado tiene un contador en cero.
- Se muestra la vista con el contador en cero.
- El usuario presiona el botón "+"
- Eso dispara el evento sumar(), que modifica el estado a
1
mediante la invocación asetContador(1)
- Al cambiar el estado, React vuelve a ejecutar el método render() buscando los tags del DOM que se han modificado
- El usuario ve en el navegador el valor "1" asociado al contador
ReactJS trabaja con las ideas de la programación funcional, esto implica que nuestros valores dentro del estado deben ser inmutables. En el caso de un número el mismo diseño de los números como valores inmutables lo hace intuitivo, pero si trabajamos con personas que tienen un nombre, es importante cambiar la referencia a la persona de manera de que React detecte los cambios:
// INCORRECTO, si pepita referencia al estado actual
setPersona(pepita)
// CORRECTO, se genera un nuevo objeto -> React dispara el render
setPersona(Object.assign(new Persona(), pepita))
Más adelante veremos otros ejemplos de uso.
Para la presentación utilizamos Material-UI, por si el lector quiere conocer.
Para el testeo unitario utilizaremos JEST + React Testing Library, un framework de testeo unitario que viene con varias funciones utilitarias y opciones de debugging que nos facilitarán la vida cuando los tests fallen. El primer test es que el componente App se renderiza sin romperse, lo que llamamos comunmente un smoke test o test de humo que verifica por ejemplo que hay un saludo a Mariano:
test('smoke test for App', () => {
render(<App />)
const saludoAMariano = screen.getByText(/Mariano/i)
expect(saludoAMariano).toBeInTheDocument()
})
No obstante, esta variante es muy frágil, si queremos saludar a otra persona el test se va a romper y eso no necesariamente indica que la aplicación no levantó. Podemos modificarlo para buscar que haya algún tipo de encabezado (h1, h2, h3, etc.):
test('smoke test for App', () => {
render(<App />)
const headings = screen.getAllByRole('heading')
// expect(headings).not.toBe.empty puede tirar un error molesto del linter
expect(headings).not.toBe('')
})
El segundo test prueba en forma aislada que el componente que saluda lo hace en forma correcta:
describe('cuando le paso un nombre', () => {
it('lo muestra', () => {
render(<Saludo nombre='Manola' />)
const appIntro = screen.getByTestId('saludo')
expect(appIntro).to.equal('Hola, Manola')
})
})
- con
render
envolvemos el componente Saludo en un objeto wrapper pasándole como nombre 'Manola' - el objeto screen guarda el estado del último render, y nos ofrece métodos helpers para por ejemplo buscar por
data-testid
(el método esgetByTestId
) - luego chequeamos que el texto de ese tag sea 'Hola, Manola'
Algo bueno que tienen los tests de React es que conservan su unitariedad, se prueban en forma aislada.
Por último vamos a testear el contador envolviendo el componente y luego simulando que apretamos 3 veces el botón "+":
describe('cuando se suma', () => {
it('el contador incrementa', async () => {
render(<Contador />)
const botonSumar = screen.getByTestId('sumar')
act(() => { botonSumar.click() })
act(() => { botonSumar.click() })
act(() => { botonSumar.click() })
const valor = screen.getByTestId('contadorValue')
expect(valor).toHaveTextContent('3')
})
})
El test espera mostrar el valor '3' en el elemento cuyo atributo data-testid
sea contadorValue
(estén atentos a que es un string).
Dentro del test es conveniente envolver todo cambio de estado en una función wrapper act
:
act(() => { botonSumar.click() })
eso permite que se registren correctamente.