Capítulo 4. Excel funcional
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
¿Recuerdas los componentes de función? En algún momento del Capítulo 2, tan pronto como el estado entró en escena, los componentes de función desaparecieron de la discusión. Es hora de recuperarlos.
Un rápido repaso: Componentes de función frente a componentes de clase
En su forma más simple, un componente de clase sólo necesita un método render()
. Aquí es donde construyes la interfaz de usuario, utilizando opcionalmente this.props
y this.state
:
class
Widget
extends
React
.
Component
{
render
()
{
let
ui
;
// fun with this.props and this.state
return
<
div
>
{
ui
}
</
div
>;
}
}
En un componente de función todo el componente es la función y la interfaz de usuario es lo que devuelve la función. Los accesorios se pasan a la función cuando se construye el componente:
function
Widget
(
props
)
{
let
ui
;
// fun with props but where's the state?
return
<
div
>
{
ui
}
</
div
>;
}
La utilidad de los componentes de función terminó con React v16.8: sólo puedes utilizarlos para componentes que no mantienen el estado( componentessin estado). Pero con la adición de ganchos en v16.8, ahora es posible utilizar componentes de función en todas partes. A lo largo del resto de este capítulo verás cómo el componente Excel
del Capítulo 3 puede implementarse como un componente de función.
Renderización de los datos
El primer paso es renderizar los datos pasados al componente(Figura 4-1). La forma de utilizar el componente no cambia. En otras palabras, un desarrollador de que utilice tu componente no necesita saber si es un componente de clase o de función. Los componentes initialData
y headers
tienen el mismo aspecto. Incluso las definiciones de propTypes
son iguales.
function
Excel
(
props
)
{
// implement me...
}
Excel
.
propTypes
=
{
headers
:
PropTypes
.
arrayOf
(
PropTypes
.
string
)
,
initialData
:
PropTypes
.
arrayOf
(
PropTypes
.
arrayOf
(
PropTypes
.
string
))
,
};
const
headers
=
[
'Book'
,
'Author'
,
'Language'
,
'Published'
,
'Sales'
];
const
data
=
[
[
'A Tale of Two Cities'
,
'Charles Dickens'
,
// ...
]
,
// ...
];
ReactDOM
.
render
(
<
Excel
headers
=
{
headers
}
initialData
=
{
data
}
/>,
document
.
getElementById
(
'app'
)
,
);
Implementar el cuerpo del componente función es en gran medida copiar-pegar el cuerpo del método render()
del componente clase:
function
Excel
({
headers
,
initialData
})
{
return
(
<
table
>
<
thead
>
<
tr
>
{
headers
.
map
((
title
,
idx
)
=>
(
<
th
key
=
{
idx
}
>
{
title
}
</
th
>
))}
</
tr
>
</
thead
>
<
tbody
>
{
initialData
.
map
((
row
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
{
row
.
map
((
cell
,
idx
)
=>
(
<
td
key
=
{
idx
}
>
{
cell
}
</
td
>
))}
</
tr
>
))}
</
tbody
>
</
table
>
);
}
En el código anterior puedes ver que en lugar de function Excel(props){}
puedes utilizar la sintaxis de desestructuración function Excel({headers, initialData}){}
para ahorrarte escribir props.headers
y props.initialData
más adelante.
El gancho del Estado
Para poder mantener el estado en tus componentes de función, necesitas ganchos. ¿Qué es un gancho? Es una función prefijada con la palabra use*
que te permite utilizar varias funciones de React, como las herramientas para gestionar el estado y los ciclos de vida de los componentes. También puedes crear tus propios ganchos. Al final de este capítulo aprenderás a utilizar varios ganchos incorporados, así como a escribir los tuyos propios.
Empecemos con el gancho de estado . Es una función llamada useState()
que está disponible como propiedad del objeto React
(React.useState()
). Toma un valor, el valor inicial de una variable de estado (un dato que quieres gestionar), y devuelve una matriz de dos elementos (una tupla). El primer elemento es la variable de estado y el segundo es una función para cambiar esta variable. Veamos un ejemplo.
En un componente de clase , en el constructor()
defines el valor inicial así:
this
.
state
=
{
data
:
initialData
;
};
Más adelante, cuando quieras cambiar el estado de data
, puedes hacer lo siguiente:
this
.
setState
({
data
:
newData
,
});
En un componente de la función , defines el estado inicial y obtienes una función actualizadora:
const
[
data
,
setData
]
=
React
.
useState
(
initialData
);
Nota
Fíjate en la sintaxis de desestructuración de la matriz , en la que asignas los dos elementos de la matriz devueltos por useState()
a dos variables: data
y setData
. Es una forma más corta y limpia de obtener los dos valores de retorno, a diferencia de, por ejemplo:
const
stateArray
=
React
.
useState
(
initialData
);
const
data
=
stateArray
[
0
];
const
setData
=
stateArray
[
1
];
Para la renderización, ahora puedes utilizar la variable data
. Cuando quieras actualizar esta variable, utiliza
setData
(
newData
);
Reescribir el componente para utilizar el gancho de estado puede tener ahora este aspecto:
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
React
.
useState
(
initialData
);
return
(
<
table
>
<
thead
>
<
tr
>
{
headers
.
map
((
title
,
idx
)
=>
(
<
th
key
=
{
idx
}
>
{
title
}
</
th
>
))}
</
tr
>
</
thead
>
<
tbody
>
{
data
.
map
((
row
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
{
row
.
map
((
cell
,
idx
)
=>
(
<
td
key
=
{
idx
}
>
{
cell
}
</
td
>
))}
</
tr
>
))}
</
tbody
>
</
table
>
);
}
Aunque este ejemplo (ver 04.02.fn.estado-tabla.html) no utiliza setData()
, puedes ver cómo está utilizando el estado data
. Pasemos a ordenar la tabla, donde necesitarás los medios para cambiar el estado.
Ordenar la tabla
En un componente de clase , todos los fragmentos de estado van al objeto this.state
, una bolsa de piezas de información a menudo sin relación entre sí. Utilizando el gancho de estado puedes seguir haciendo lo mismo, pero también puedes decidir guardar trozos de estado en variables diferentes. Cuando se trata de ordenar una tabla, la data
contenida en la tabla es una pieza de información, mientras que la información auxiliar específica de la ordenación es otra pieza. En otras palabras, puedes utilizar el gancho de estado tantas veces como quieras.
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
React
.
useState
(
initialData
);
const
[
sorting
,
setSorting
]
=
React
.
useState
({
column
:
null
,
descending
:
false
,
});
// ....
}
El data
es lo que muestras en la tabla; el objeto sorting
es un asunto aparte. Se trata de cómo ordenas (ascendente o descendente) y por qué columna (título, autor, etc.).
La función que realiza la ordenación está ahora en línea dentro de la función Excel
:
function
Excel
({
headers
,
initialData
})
{
// ..
function
sort
(
e
)
{
// implement me
}
return
(
<
table
>
{
/* ... */
}
</
table
>
);
}
La función sort()
averigua por qué columna ordenar (utilizando su índice) y si la ordenación es descendente:
const
column
=
e
.
target
.
cellIndex
;
const
descending
=
sorting
.
column
===
column
&&
!
sorting
.
descending
;
Después, clona la matriz data
porque sigue siendo mala idea modificar el estado directamente:
const
dataCopy
=
clone
(
data
);
Nota
Un recordatorio de que la función clone()
sigue siendo la forma rápida y sucia de codificar/decodificar JSON en copia profunda:
function
clone
(
o
)
{
return
JSON
.
parse
(
JSON
.
stringify
(
o
));
}
La clasificación real es la misma que antes:
dataCopy
.
sort
((
a
,
b
)
=>
{
if
(
a
[
column
]
===
b
[
column
])
{
return
0
;
}
return
descending
?
a
[
column
]
<
b
[
column
]
?
1
:
-
1
:
a
[
column
]
>
b
[
column
]
?
1
:
-
1
;
});
Y, por último, la función sort()
tiene que actualizar los dos trozos de estado con los nuevos valores:
setData
(
dataCopy
);
setSorting
({
column
,
descending
});
Y eso es todo en cuanto a la ordenación . Lo que queda es actualizar la interfaz de usuario (el valor de retorno de la función Excel()
) para reflejar qué columna se utiliza para ordenar y para gestionar los clics en cualquiera de las cabeceras:
<
thead
onClick
=
{
sort
}
>
<
tr
>
{
headers
.
map
((
title
,
idx
)
=>
{
if
(
sorting
.
column
===
idx
)
{
title
+=
sorting
.
descending
?
' \u2191'
:
' \u2193'
;
}
return
<
th
key
=
{
idx
}
>
{
title
}
</
th
>;
})}
</
tr
>
</
thead
>
Puedes ver el resultado con la flecha de ordenación en la Figura 4-2.
Te habrás dado cuenta de otra cosa buena de utilizar ganchos de estado : no es necesario vincular ninguna función de devolución de llamada como se hace en el constructor de un componente de clase. Nada de este asunto de this.sort = this.sort.bind(this)
. No this
, no constructor()
. Una función es todo lo que necesitas para definir un componente .
Editar datos
Como recordarás del Capítulo 3, la funcionalidad de edición de consiste en los siguientes pasos:
-
Haces doble clic en una celda de la tabla y se convierte en un formulario de introducción de texto.
-
Escribe en el formulario de introducción de texto.
-
Cuando hayas terminado, pulsa Intro para enviar el formulario.
Para hacer un seguimiento de este proceso , vamos a añadir un objeto de estado edit
. Es null
cuando no hay edición; en caso contrario, almacena los índices de fila y columna de la celda que se está editando.
const
[
edit
,
setEdit
]
=
useState
(
null
);
En la interfaz de usuario necesitas manejar los dobles clics (onDoubleClick={showEditor}
) y, si el usuario está editando, mostrar un formulario. Si no, muestra sólo los datos. Cuando el usuario pulsa Intro, atrapas el evento de envío (onSubmit={save}
).
<
tbody
onDoubleClick
=
{
showEditor
}
>
{
data
.
map
((
row
,
rowidx
)
=>
(
<
tr
key
=
{
rowidx
}
data
-
row
=
{
rowidx
}
>
{
row
.
map
((
cell
,
columnidx
)
=>
{
if
(
edit
&&
edit
.
row
===
rowidx
&&
edit
.
column
===
columnidx
)
{
cell
=
(
<
form
onSubmit
=
{
save
}
>
<
input
type
=
"text"
defaultValue
=
{
cell
}
/>
</
form
>
);
}
return
<
td
key
=
{
columnidx
}
>
{
cell
}
</
td
>;
})}
</
tr
>
))}
</
tbody
>
Quedan dos funciones cortas por implementar: showEditor()
y save()
.
El showEditor()
se invoca al hacer doble clic en una celda del cuerpo de la tabla. Allí se actualiza el estado edit
(a través de setEdit()
) con los índices de fila y columna, para que el renderizado sepa qué celdas sustituir por un formulario.
function
showEditor
(
e
)
{
setEdit
({
row
:
parseInt
(
e
.
target
.
parentNode
.
dataset
.
row
,
10
)
,
column
:
e
.
target
.
cellIndex
,
});
}
La función save()
atrapa el evento de envío del formulario, impide el envío y actualiza el estado data
con el nuevo valor de la celda que se está editando. También llama asetEdit()
pasando null
como nuevo estado de edición, lo que significa que la edición ha finalizado.
function
save
(
e
)
{
e
.
preventDefault
();
const
input
=
e
.
target
.
firstChild
;
const
dataCopy
=
clone
(
data
);
dataCopy
[
edit
.
row
][
edit
.
column
]
=
input
.
value
;
setEdit
(
null
);
setData
(
dataCopy
);
}
Y con esto, la funcionalidad de edición está terminada. Consulta 04.04.fn.table-edit.html en el repositorio del libro para ver el código completo .
Buscando en
Buscar/filtrar los datos de no plantea nuevos retos en lo que respecta a React y los ganchos. Puedes intentar implementarlo tú mismo y hacer referencia a la implementación en 04.05.fn.table-search.html en el repositorio del libro.
Necesitarás dos nuevas piezas de estado:
-
El booleano
search
para indicar si el usuario está filtrando o sólo mirando los datos -
La copia de
data
comopreSearchData
, porque ahoradata
se convierte en un subconjunto filtrado de todos los datos
const
[
search
,
setSearch
]
=
useState
(
false
);
const
[
preSearchData
,
setPreSearchData
]
=
useState
(
null
);
Debes tener cuidado de mantener preSearchData
actualizado, ya que data
(el subconjunto filtrado) puede actualizarse cuando el usuario está editando a la vez que filtra. Consulta el Capítulo 3 para refrescar la memoria.
Pasemos a implementando la función de repetición, que ofrece la oportunidad de familiarizarse con dos nuevos conceptos:
-
Utilizar ganchos del ciclo de vida
-
Escribir tus propios anzuelos
Ciclos de vida en un mundo de ganchos
La función de repetición del capítulo 3 utiliza dos métodos del ciclo de vida de la clase Excel
:
componentDidMount()
y componentWillUnmount()
.
Problemas con los métodos del ciclo de vida
Si vuelves a visitar en el ejemplo 03.14.table-fetch.html, te darás cuenta de que cada una de ellas tiene dos tareas, no relacionadas entre sí:
componentDidMount
()
{
document
.
addEventListener
(
'keydown'
,
this
.
keydownHandler
);
fetch
(
'https://www...'
)
.
then
(
/*...*/
)
.
then
((
initialData
)
=>
{
/*...*/
this
.
setState
({
data
});
});
}
componentWillUnmount
()
{
document
.
removeEventListener
(
'keydown'
,
this
.
keydownHandler
);
clearInterval
(
this
.
replayID
);
}
En componentDidMount()
configuras un oyente keydown
para iniciar la repetición y también obtienes datos de un servidor. En componentWillUnmount()
eliminas el oyente keydown
y también limpias un identificador setInterval()
. Esto ilustra dos problemas relacionados con el uso de métodos de ciclo de vida en componentes de clase (que se resuelven al utilizar ganchos):
- Las tareas no relacionadas se ejecutan juntas
-
Por ejemplo, realizar la obtención de datos y configurar los escuchadores de eventos de en un solo lugar. Esto hace que los métodos del ciclo de vida crezcan en longitud mientras realizan las tareas no relacionadas. En componentes sencillos esto está bien, pero en los más grandes tienes que recurrir a comentarios de código o mover trozos de código a otras funciones, para poder dividir las tareas no relacionadas y hacer el código más legible.
- Las tareas relacionadas están repartidas
-
Por ejemplo, considera la posibilidad de añadir y eliminar el mismo oyente de eventos. A medida que los métodos del ciclo de vida crecen en tamaño, es más difícil considerar las piezas separadas de la misma preocupación de un vistazo, porque simplemente no caben en la misma pantalla de código cuando lo lees más tarde.
utilizarEfecto()
El gancho incorporado en que sustituye a los dos métodos del ciclo de vida anteriores es React.useEffect()
.
Nota
La palabra "efecto" significa "efecto secundario" en , es decir, un tipo de trabajo que no está relacionado con la tarea principal pero que ocurre al mismo tiempo. La tarea principal de cualquier componente React es renderizar algo basándose en el estado y los props. Pero renderizar al mismo tiempo (en la misma función) junto con algunos trabajos secundarios (como obtener datos de un servidor o configurar escuchadores de eventos) puede ser necesario.
En el componente Excel
, por ejemplo, configurar un gestor de keydown
es un efecto secundario de la tarea principal de representar los datos en una tabla.
El gancho useEffect()
toma dos argumentos :
-
Una función de devolución de llamada que React llama en el momento oportuno
-
Una matriz opcional de dependencias
La lista de dependencias contiene variables que se comprobarán antes de invocar la llamada de retorno y que dictan si ésta debe invocarse.
-
Si los valores de las variables dependientes no han cambiado, no es necesario invocar la llamada de retorno.
-
Si la lista de dependencias es una matriz vacía, la llamada de retorno se llama una sola vez, de forma similar a
componentDidMount()
. -
Si se omiten las dependencias, la llamada de retorno se invoca en cada renderización
useEffect
(()
=>
{
// logs only if `data` or `headers` have changed
console
.
log
(
Date
.
now
());
},
[
data
,
headers
]);
useEffect
(()
=>
{
// logs once, after initial render, like `componentDidMount()`
console
.
log
(
Date
.
now
());
},
[]);
useEffect
(()
=>
{
// called on every re-render
console
.
log
(
Date
.
now
());
},
/* no dependencies here */
);
Limpieza de los efectos secundarios
Ahora ya sabes cómo utilizar ganchos para lograr lo que componentDidMount()
ofrece en componentes de clase. Pero, ¿qué hay de un equivalente a componentWillUnmount()
? Para esta tarea, utilizas el valor de retorno de la función de devolución de llamada que pasas a useEffect()
:
useEffect
(()
=>
{
// logs once, after initial render, like `componentDidMount()`
console
.
log
(
Date
.
now
());
return
()
=>
{
// log when the component will be removed form the DOM
// like `componentDidMount()`
console
.
log
(
Date
.
now
());
};
},
[]);
Veamos un ejemplo más completo(04.06.useEffect.html en el repositorio):
function
Example
()
{
useEffect
(()
=>
{
console
.
log
(
'Rendering <Example/>'
,
Date
.
now
());
return
()
=>
{
// log when the component will be removed form the DOM
// like `componentDidMount()`
console
.
log
(
'Removing <Example/>'
,
Date
.
now
());
};
}
,
[]);
return
<
p
>
I
am
an
example
child
component
.
</
p
>;
}
function
ExampleParent
()
{
const
[
visible
,
setVisible
]
=
useState
(
false
);
return
(
<
div
>
<
button
onClick
=
{()
=>
setVisible
(
!
visible
)}
>
Hello
there
,
press
me
{
visible
?
'again'
:
''
}
</
button
>
{
visible
?
<
Example
/>
:
null
}
</
div
>
);
}
Si haces clic en el botón una vez, se crea un componente hijo y si lo vuelves a hacer, se elimina. Como puedes ver en la Figura 4-3, el valor de retorno de useEffect()
(que es una función) se invoca cuando se elimina el componente del DOM.
Ten en cuenta que la función de limpieza (también conocida como "teardown") se ejecuta cuando se elimina el componente del DOM, porque la matriz de dependencias está vacía. Si hubiera un valor en la matriz de dependencias, se llamaría a la función de desmontaje cada vez que cambiara el valor de la dependencia .
Ciclos de vida sin problemas
Si consideras de nuevo el caso de uso de de configurar y borrar escuchadores de eventos, puede implementarse así:
useEffect
(()
=>
{
function
keydownHandler
()
{
// do things
}
document
.
addEventListener
(
'keydown'
,
keydownHandler
);
return
()
=>
{
document
.
removeEventListener
(
'keydown'
,
keydownHandler
);
};
},
[]);
El patrón anterior resuelve el segundo problema con los métodos de ciclo de vida basados en clases que se mencionó anteriormente: el problema de repartir tareas relacionadas por todo el componente. Aquí puedes ver cómo el uso de ganchos te permite tener la función manejadora, su configuración y su eliminación, todo en el mismo lugar.
En cuanto al primer problema (tener tareas no relacionadas en el mismo lugar), se resuelve teniendo varias llamadas a useEffect
, cada una dedicada a una tarea específica. Del mismo modo que puedes tener piezas separadas de estado en lugar de un único objeto, también puedes tener llamadas separadas a useEffect
, cada una de ellas dedicada a un asunto distinto, en lugar de un único método de clase que se ocupe de todo:
function
Example
()
{
const
[
data
,
setData
]
=
useState
(
null
);
useEffect
(()
=>
{
// fetch() and then call setData()
});
useEffect
(()
=>
{
// event handlers
});
return
<
div
>
{
data
}
<
/div>;
}
usarEfectoDisposición()
Para terminar la discusión de sobre useEffect()
consideremos otro gancho incorporado llamado useLayoutEffect()
.
Nota
Sólo hay unos pocos ganchos incorporados, así que no te preocupes por tener que memorizar una larga lista de nuevas API.
useLayoutEffect()
funciona como useEffect()
, con la única diferencia de que se invoca antes de que React termine de pintar todos los nodos DOM de un renderizado. En general, debes utilizar useEffect()
a menos que necesites medir algo en la página (tal vez las dimensiones de un componente renderizado o la posición de desplazamiento tras una actualización) y luego volver a renderizar basándote en esta información. Cuando no sea necesario nada de esto, useEffect()
es mejor, ya que es asíncrono y además indica al lector de tu código que las mutaciones del DOM no son relevantes para tu componente.
Como useLayoutEffect()
se llama antes, puedes recalcular y volver a renderizar y el usuario sólo ve la última renderización. De lo contrario, ven primero el renderizado inicial y luego el segundo renderizado. Dependiendo de lo complicado que sea el uso del diseño, los usuarios pueden percibir un parpadeo entre los dos renders.
El siguiente ejemplo(04.07.useLayoutEffect.html en el repositorio) representa una tabla larga con anchuras de celda aleatorias (sólo para ponérselo más difícil al navegador). A continuación, la anchura de la tabla se establece en un gancho de efecto.
function
Example
({
layout
})
{
if
(
layout
===
null
)
{
return
null
;
}
if
(
layout
)
{
useLayoutEffect
(()
=>
{
const
table
=
document
.
getElementsByTagName
(
'table'
)[
0
];
console
.
log
(
table
.
offsetWidth
);
table
.
width
=
'250px'
;
}
,
[]);
}
else
{
useEffect
(()
=>
{
const
table
=
document
.
getElementsByTagName
(
'table'
)[
0
];
console
.
log
(
table
.
offsetWidth
);
table
.
width
=
'250px'
;
}
,
[]);
}
return
(
<
table
>
<
thead
>
<
tr
>
<
th
>
Random
</
th
>
</
tr
>
</
thead
>
<
tbody
>
{
Array
.
from
(
Array
(
10000
)).
map
((
_
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
<
td
width
=
{
Math
.
random
()
*
800
}
>
{
Math
.
random
()}
</
td
>
</
tr
>
))}
</
tbody
>
</
table
>
);
}
function
ExampleParent
()
{
const
[
layout
,
setLayout
]
=
useState
(
null
);
return
(
<
div
>
<
button
onClick
=
{()
=>
setLayout
(
false
)}
>
useEffect
</
button
>
{
' '
}
<
button
onClick
=
{()
=>
setLayout
(
true
)}
>
useLayoutEffect
</
button
>
{
' '
}
<
button
onClick
=
{()
=>
setLayout
(
null
)}
>
clear
</
button
>
<
Example
layout
=
{
layout
}
/>
</
div
>
);
}
Dependiendo de si activas la ruta useEffect()
o useLayoutEffect()
, puede que veas un parpadeo mientras se redimensiona la tabla desde su valor aleatorio (unos 600 px) hasta los 250 px codificados (ver Figura 4-4).
Ten en cuenta que, en ambos casos, puedes obtener la geometría de la tabla (por ejemplo, table.offsetWidth
), por lo que si sólo lo necesitas con fines informativos y no vas a rerenderizar, te conviene más el asíncrono useEffect()
. useLayoutEffect()
debería reservarse para evitar el parpadeo en casos en los que necesites actuar (rerenderizar) en función de algo que midas, por ejemplo, posicionar un componente tooltip elegante en función del tamaño del elemento al que está apuntando.
Un gancho a medida
Volvamos a Excel
y veamos cómo implementar la función de repetición. En el caso de los componentes de clase, era necesario crear un logSetState()
y luego sustituir todas las llamadas a this.setState()
por this.logSetState()
. Con los componentes de función puedes sustituir todas las llamadas al gancho useState()
por useLoggedState()
. Esto es un poco más cómodo, ya que sólo hay unas pocas llamadas (para cada bit independiente de estado) y todas están en la parte superior de la función.
// before
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
useState
(
initialData
);
const
[
edit
,
setEdit
]
=
useState
(
null
);
// ... etc
}
// after
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
useLoggedState
(
initialData
,
true
);
const
[
edit
,
setEdit
]
=
useLoggedState
(
null
);
// ... etc
}
No hay ningún gancho useLoggedState()
incorporado, pero no pasa nada. Puedes crear tus propios ganchos personalizados. Al igual que los ganchos incorporados, un gancho personalizado no es más que una función que empieza por use*()
. He aquí un ejemplo:
function
useLoggedState
(
initialValue
,
isData
)
{
// ...
}
La firma del gancho puede ser la que tú quieras. En este caso, hay un argumento adicional isData
. Su finalidad es ayudar a diferenciar el estado de datos del estado de no datos. En el ejemplo del componente de clase del Capítulo 3, todo el estado es un único objeto, pero aquí hay varias partes del estado. En la función de repetición, el objetivo principal es mostrar los cambios en los datos y luego mostrar que toda la información de apoyo (ordenación, descendente, etc.) es secundaria. Como la repetición se actualiza cada segundo, no será tan divertido ver cómo cambian individualmente los datos de apoyo; la repetición sería demasiado lenta. Así que vamos a tener un registro principal (dataLog
array) y uno auxiliar (auxLog
array). Además, es útil incluir una bandera que indique si el estado cambia debido a la interacción del usuario o (automáticamente) durante la repetición:
let
dataLog
=
[];
let
auxLog
=
[];
let
isReplaying
=
false
;
El objetivo del gancho personalizado es no interferir en las actualizaciones regulares del estado, por lo que delega esta responsabilidad en el useState
original. El objetivo es registrar el estado junto con una referencia a la función que sabe cómo actualizar este estado durante la repetición. La función tiene un aspecto similar al siguiente
function
useLoggedState
(
initialValue
,
isData
)
{
const
[
state
,
setState
]
=
useState
(
initialValue
);
// fun here...
return
[
state
,
setState
];
}
El código anterior utiliza el predeterminado useState
. Pero ahora tienes las referencias a un trozo de estado y los medios para actualizarlo. Necesitas registrarlo. Aprovechemos aquí del gancho useEffect()
:
function
useLoggedState
(
initialValue
,
isData
)
{
const
[
state
,
setState
]
=
useState
(
initialValue
);
useEffect
(()
=>
{
// todo
}
,
[
state
]);
return
[
state
,
setState
];
}
Este método garantiza que el registro sólo se produzca cuando cambie el valor de state
. Es posible que se llame a la función useLoggedState()
varias veces durante varias redirecciones, pero puedes ignorar estas llamadas a menos que impliquen un cambio en una parte interesante del estado de .
En la llamada de retorno de useEffect()
tú:
-
No hagas nada si el usuario está reproduciendo.
-
Registra cada cambio en el estado de los datos en
dataLog
. -
Registra cada cambio en los datos de apoyo en
auxLog
, indexado por el cambio asociado en los datos.
useEffect
(()
=>
{
if
(
isReplaying
)
{
return
;
}
if
(
isData
)
{
dataLog
.
push
([
clone
(
state
)
,
setState
]);
}
else
{
const
idx
=
dataLog
.
length
-
1
;
if
(
!
auxLog
[
idx
])
{
auxLog
[
idx
]
=
[];
}
auxLog
[
idx
].
push
([
state
,
setState
]);
}
}
,
[
state
]);
¿Por qué existen los ganchos personalizados? Te ayudan a aislar y empaquetar ordenadamente una parte de la lógica que se utiliza en un componente y que a menudo se comparte entre componentes. El gancho personalizado useLoggedState()
puede colocarse en cualquier componente que pueda beneficiarse del registro de su estado. Además, los ganchos personalizados pueden llamar a otros ganchos, cosa que no pueden hacer las funciones normales (que no son ganchos ni componentes).
Concluyendo la repetición
Ahora que tienes un gancho personalizado que registra los cambios en varios fragmentos de estado, es hora de que conecte la función de reproducción.
La función replay()
no es un aspecto emocionante de la discusión sobre React, pero establece un ID de intervalo. Necesitas ese ID para limpiar el intervalo en caso de que Excel
se elimine del DOM durante la reproducción. En la repetición, los cambios de datos se reproducen cada segundo, mientras que los auxiliares se vacían juntos:
function
replay
()
{
isReplaying
=
true
;
let
idx
=
0
;
replayID
=
setInterval
(()
=>
{
const
[
data
,
fn
]
=
dataLog
[
idx
];
fn
(
data
);
auxLog
[
idx
]
&&
auxLog
[
idx
].
forEach
((
log
)
=>
{
const
[
data
,
fn
]
=
log
;
fn
(
data
);
});
idx
++
;
if
(
idx
>
dataLog
.
length
-
1
)
{
isReplaying
=
false
;
clearInterval
(
replayID
);
return
;
}
},
1000
);
}
La última parte de la fontanería consiste en configurar un gancho de efectos. Después de renderizar Excel
, el gancho se encarga de configurar escuchas que monitoreen la combinación de teclas concreta para iniciar el espectáculo de repetición. Este es también el lugar para limpiar después de que se destruya el componente.
useEffect
(()
=>
{
function
keydownHandler
(
e
)
{
if
(
e
.
altKey
&&
e
.
shiftKey
&&
e
.
keyCode
===
82
)
{
// ALT+SHIFT+R(eplay)
replay
();
}
}
document
.
addEventListener
(
'keydown'
,
keydownHandler
);
return
()
=>
{
document
.
removeEventListener
(
'keydown'
,
keydownHandler
);
clearInterval
(
replayID
);
dataLog
=
[];
auxLog
=
[];
};
},
[]);
Para ver el código en su totalidad, consulta 04.08.fn.table-replay.html en el repositoriode del libro.
useReducer
Terminemos el capítulo con un gancho incorporado más llamado useReducer()
. Utilizar un reductor es una alternativa a useState()
. En lugar de que varias partes del componente llamen al cambio de estado, todos los cambios pueden gestionarse en un único lugar.
Un reductor es sólo una función JavaScript que toma dos entradas -el estado anterior y una acción- y devuelve el nuevo estado. Piensa en la acción como algo que ha ocurrido en la aplicación, tal vez un clic, una obtención de datos o un tiempo de espera. Algo ha ocurrido y requiere un cambio. Las tres variables (nuevo estado, antiguo estado, acción) pueden ser de cualquier tipo, aunque lo más habitual es que sean objetos.
Funciones reductoras
Una función reductora en su forma más simple tiene el siguiente aspecto:
function
myReducer
(
oldState
,
action
)
{
const
newState
=
{};
// do something with `oldState` and `action`
return
newState
;
}
Imagina que la función reductora se encarga de dar sentido a la realidad cuando ocurre algo en el mundo. El mundo es un mess
, entonces ocurre un event
. La función que debe makeSense()
del mundo reconcilia el desorden con el nuevo acontecimiento y reduce toda la complejidad a un estado agradable o order
:
function
makeSense
(
mess
,
event
)
{
const
order
=
{};
// do something with mess and event
return
order
;
}
Otra analogía procede del mundo de la cocina. Algunas salsas y sopas también se llaman reducciones, producidas por el proceso de reducción (espesar, intensificar el sabor). El estado inicial es una olla de agua, luego diversas acciones (hervir, añadir ingredientes, remover) alteran el estado del contenido de la olla con cada acción.
Acciones
La función reductora puede tomar cualquier cosa (una cadena, un objeto), pero una implementación común es un objeto event
con:
-
Un
type
(por ejemplo,click
en el mundo DOM) -
Opcionalmente, alguna
payload
de otra información sobre el evento
A continuación se "despachan" las acciones. Cuando se envía la acción , React llama a la función reductora adecuada con el estado actual y tu nuevo evento (acción).
Con useState
tienes:
const
[
data
,
setData
]
=
useState
(
initialData
);
Que se puede sustituir por el reductor:
const
[
data
,
dispatch
]
=
useReducer
(
myReducer
,
initialData
);
El data
se sigue utilizando de la misma manera para renderizar el componente. Pero cuando ocurre algo, en lugar de hacer un poco de trabajo seguido de una llamada a setData()
, llama a la función dispatch()
devuelta por useReducer()
. A partir de ahí, el reductor toma el control y devuelve la nueva versión de data
. No hay ninguna otra función a la que llamar para establecer el nuevo estado; el nuevo data
es utilizado por React para volver a renderizar el componente.
La Figura 4-5 muestra un diagrama de este proceso.
Un ejemplo de Reductor
Veamos un ejemplo rápido y aislado del uso de un reductor. Digamos que tiene una tabla de datos aleatorios junto con botones que pueden refrescar los datos o cambiar los colores de fondo y primer plano de la tabla a colores aleatorios (como se muestra en la Figura 4-6).
Inicialmente, no hay datos y se utilizan los colores blanco y negro por defecto:
const
initialState
=
{
data
:
[],
color
:
'black'
,
background
:
'white'
};
El reductor se inicializa en la parte superior del componente <RandomData>
:
function
RandomData
()
{
const
[
state
,
dispatch
]
=
useReducer
(
myReducer
,
initialState
);
// ...
}
En este caso, volvemos a state
como un objeto que contiene varias piezas de estado (aunque no tiene por qué ser así). El resto del componente sigue como siempre, renderizando en base a state
, con una diferencia. Mientras que antes hacía que el controlador de un botón onClick
fuera una función que actualizaba el estado, ahora todos los controladores simplemente llaman adispatch()
enviando información sobre el evento:
return
(
<
div
>
<
div
className
=
"toolbar"
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'newdata'
})}
>
Get
data
</
button
>
{
' '
}
<
button
onClick
=
{()
=>
dispatch
({
type
:
'recolor'
,
payload
:
{
what
:
'color'
}})}
>
Recolor
text
</
button
>
{
' '
}
<
button
onClick
=
{
()
=>
dispatch
({
type
:
'recolor'
,
payload
:
{
what
:
'background'
}})
}
>
Recolor
background
</
button
>
</
div
>
<
table
style
=
{{
color
,
background
}}
>
<
tbody
>
{
data
.
map
((
row
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
{
row
.
map
((
cell
,
idx
)
=>
(
<
td
key
=
{
idx
}
>
{
cell
}
</
td
>
))}
</
tr
>
))}
</
tbody
>
</
table
>
</
div
>
);
Cada objeto de evento/acción enviado tiene una propiedad type
, para que la función reductora pueda identificar lo que hay que hacer. Puede haber o no un payload
que especifique más detalles del evento.
Por último, el reductor. Tiene un número de sentencias if
/else
(o una switch
, si así lo prefieres) que comprueban qué tipo de evento se ha enviado. Luego se manipulan los datos según la acción y se devuelve una nueva versión del estado:
function
myReducer
(
oldState
,
action
)
{
const
newState
=
clone
(
oldState
);
if
(
action
.
type
===
'recolor'
)
{
newState
[
action
.
payload
.
what
]
=
`rgb(
${
rand
(
256
)
}
,
${
rand
(
256
)
}
,
${
rand
(
256
)
}
)`
;
}
else
if
(
action
.
type
===
'newdata'
)
{
const
data
=
[];
for
(
let
i
=
0
;
i
<
10
;
i
++
)
{
data
[
i
]
=
[];
for
(
let
j
=
0
;
j
<
10
;
j
++
)
{
data
[
i
][
j
]
=
rand
(
10000
);
}
}
newState
.
data
=
data
;
}
return
newState
;
}
// couple of helpers
function
clone
(
o
)
{
return
JSON
.
parse
(
JSON
.
stringify
(
o
));
}
function
rand
(
max
)
{
return
Math
.
floor
(
Math
.
random
()
*
max
);
}
Observa cómo el estado antiguo se clona utilizando el rápido y sucio clone()
que ya conoces. Con useState()/setState()
esto no era estrictamente necesario en muchos casos. A menudo podías arreglártelas modificando una variable existente y pasándola a setState()
. Pero aquí, si no clonas y te limitas a modificar el mismo objeto en memoria, React verá que el estado antiguo y el nuevo apuntan al mismo objeto y se saltará la renderización, pensando que nada ha cambiado. Puedes probar por ti mismo: elimina la llamada a clone()
y observa que no se produce el rerenderizado .
Pruebas unitarias Reductores
Cambiar a useReducer()
para la gestión del estado facilita mucho la escritura de pruebas unitarias. No necesitas configurar el componente ni sus propiedades y estado. No necesitas involucrar a un navegador ni encontrar otra forma de simular eventos de clic. Ni siquiera necesitas involucrar a React. Para probar la lógica de estado, todo lo que tienes que hacer es pasar el estado anterior y una acción a la función reductora y comprobar si se devuelve el nuevo estado deseado. Esto es puro JavaScript: dos objetos entran, un objeto sale. Las pruebas unitarias no deberían ser mucho más complicadas que probar el ejemplo canónico:
function
add
(
a
,
b
)
{
return
a
+
b
;
}
Más adelante en el libro hablaremos de las pruebas, pero para que te hagas una idea, un ejemplo de prueba podría ser así:
const
initialState
=
{
data
:
[],
color
:
'black'
,
background
:
'white'
};
it
(
'produces a 10x10 array'
,
()
=>
{
const
{
data
}
=
myReducer
(
initialState
,
{
type
:
'newdata'
});
expect
(
data
.
length
).
toEqual
(
10
);
expect
(
data
[
0
].
length
).
toEqual
(
10
);
});
Componente Excel con Reductor
Para un último ejemplo de uso de reductores, veamos cómo puedes pasar deuseState()
a useReducer()
en el componente Excel
.
En el ejemplo de la sección anterior, el estado gestionado por el reductor era de nuevo un objeto de datos no relacionados. No tiene por qué ser así. Puedes tener varios reductores para separar tus preocupaciones. Incluso puedes mezclar y combinar useState()
con useReducer()
. Intentémoslo con Excel
.
Anteriormente la data
de la tabla era gestionada por useState()
:
const
[
data
,
setData
]
=
useState
(
initialData
);
// ...
const
[
edit
,
setEdit
]
=
useState
(
null
);
const
[
search
,
setSearch
]
=
useState
(
false
);
Cambiar a useReducer()
para gestionar data
dejando el resto intacto tiene el siguiente aspecto:
const
[
data
,
dispatch
]
=
useReducer
(
reducer
,
initialData
);
// ...
const
[
edit
,
setEdit
]
=
useState
(
null
);
const
[
search
,
setSearch
]
=
useState
(
false
);
Como data
es lo mismo que , no es necesario cambiar nada en la sección de renderizado. Los cambios sólo son necesarios en los manejadores de acciones de . Por ejemplo, filter()
se utiliza para hacer el filtrado y llamar a setData()
:
function
filter
(
e
)
{
const
needle
=
e
.
target
.
value
.
toLowerCase
();
if
(
!
needle
)
{
setData
(
preSearchData
);
return
;
}
const
idx
=
e
.
target
.
dataset
.
idx
;
const
searchdata
=
preSearchData
.
filter
((
row
)
=>
{
return
row
[
idx
].
toString
().
toLowerCase
().
indexOf
(
needle
)
>
-
1
;
});
setData
(
searchdata
);
}
La versión reescrita envía una acción en su lugar. El evento tiene un type
de "búsqueda" y alguna carga útil adicional (¿qué está buscando el usuario y dónde?):
function
filter
(
e
)
{
const
needle
=
e
.
target
.
value
;
const
column
=
e
.
target
.
dataset
.
idx
;
dispatch
({
type
:
'search'
,
payload
:
{
needle
,
column
},
});
setEdit
(
null
);
}
Otro ejemplo de sería alternar los campos de búsqueda:
// before
function
toggleSearch
()
{
if
(
search
)
{
setData
(
preSearchData
);
setSearch
(
false
);
setPreSearchData
(
null
);
}
else
{
setPreSearchData
(
data
);
setSearch
(
true
);
}
}
// after
function
toggleSearch
()
{
if
(
!
search
)
{
dispatch
({
type
:
'startSearching'
});
}
else
{
dispatch
({
type
:
'doneSearching'
});
}
setSearch
(
!
search
);
}
Aquí puedes ver la mezcla de setSearch()
y dispatch()
para gestionar el estado. El conmutador!search
es una bandera para que la interfaz de usuario muestre u oculte los cuadros de entrada, mientras que el botón dispatch()
es para gestionar los datos.
Por último, echemos un vistazo en a la función reducer()
. Aquí es donde se produce ahora todo el filtrado y manipulación de datos. Se trata de nuevo de una serie de bloques if
/else
, cada uno de los cuales maneja un tipo de acción diferente :
let
originalData
=
null
;
function
reducer
(
data
,
action
)
{
if
(
action
.
type
===
'sort'
)
{
const
{
column
,
descending
}
=
action
.
payload
;
return
clone
(
data
).
sort
((
a
,
b
)
=>
{
if
(
a
[
column
]
===
b
[
column
])
{
return
0
;
}
return
descending
?
a
[
column
]
<
b
[
column
]
?
1
:
-
1
:
a
[
column
]
>
b
[
column
]
?
1
:
-
1
;
});
}
if
(
action
.
type
===
'save'
)
{
data
[
action
.
payload
.
edit
.
row
][
action
.
payload
.
edit
.
column
]
=
action
.
payload
.
value
;
return
data
;
}
if
(
action
.
type
===
'startSearching'
)
{
originalData
=
data
;
return
originalData
;
}
if
(
action
.
type
===
'doneSearching'
)
{
return
originalData
;
}
if
(
action
.
type
===
'search'
)
{
return
originalData
.
filter
((
row
)
=>
{
return
(
row
[
action
.
payload
.
column
]
.
toString
()
.
toLowerCase
()
.
indexOf
(
action
.
payload
.
needle
.
toLowerCase
())
>
-
1
);
});
}
}
Get React: Up & Running, 2ª Edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.