Esta semana quiero dar una visión general, simple y directa de lo fundamental en React.js y para ello incluiré algunas explicaciones pero sobre todo un ejemplo práctico que he preparado, ya que como dice mi padre: el movimiento se demuestra andando.
El diseño web con React.js se basa totalmente en jerarquías de componentes, que son preferiblemente pequeños fragmentos reutilizables en que podemos dividir una interfaz. No hay ninguna imposición sobre cuántos debemos crear o como de reutilizables deben ser, pero esto es clave a la hora de crear un código elegante y profesional.
Al programar con React usamos frecuentemente sintaxis de ECMAScript 6 o posteriores, por lo que te recomiendo que eches un ojillo a este artículo. Además de las novedades del estándar deberás también conocer el lenguaje de marcas JSX, que es prácticamente HTML5 pero con ciertas diferencias (como convertir todos los atributos a un formato camelCase), y lo usaremos para crear la "vista" de nuestros componentes.
Tipos de componente
Los componentes pueden ser de dos tipos:
- Con estado: Se dice que un componente tiene estado cuando usa una variable de tipo objeto donde almacena información sobre su situación actual (que varía en el tiempo). Por ejemplo: un componente que sea una caja de texto podría tener en su estado la longitud de caracteres del texto que contiene.
- Sin estado: Son aquellos que no necesitan tener su propio estado. Ya que o bien son completamente estáticos o reciben las variables que necesitan como propiedades desde su componente padre.
Una cosita importante: grábate en la cabeza la siguiente frase: Queremos tener el menor número de componentes con estado posible, ya que así el código será más fácil de mantener. Es mejor tener componentes principales con estado que pasen como parámetro/propiedades todo lo necesario a sus hijos.
Por lo dicho anteriormente se determinan dos posibles opciones para crear un componente:
-
Componente funcional: Este método solo sirve para crear componentes sin estado. Y básicamente se trata de una función que recoge unas propiedades/parámetros (opcionalmente) y devuelve su código JSX, así de simple.
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
- Componente basado en clase: Usando la sintaxis ES6 para definir clases se hereda de Component y se implementa un constructor donde recoge las propiedades y un método render, que debe retornar el código JSX que visualiza el componente. Si queremos disponer de estado o sobrescribir los métodos de su ciclo de vida, entonces tenemos que crear un componente basado en clase.
import React,{Component} from 'react';
class Welcome extends React.Component {
constructor(props){
super(props);
this.state = { //un ejemplo de cómo se define el estado desde el constructor.
op1: "prueba",
op2: 3
};
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Otro dato básico: cuando estemos escribiendo JSX podemos incluir en él variables javascript o llamadas a funciones que devuelvan JSX usando las llaves, como vemos con {this.props.name}
Jerarquía
La jerarquía de componentes se va creando incluyendo unos componentes (hijo) dentro de otros (padres), y esto se hace de forma muy sencilla indicando el componente a insertar como un tag html que puede además recibir parámetros en forma de atributos. Estos tag se incluyen directamente en el código JSX de los componentes, para incluir unos dentro de otros e ir componiendo la interfaz.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
Evidentemente deberá existir un primer padre o raíz a partir del cual vamos enganchando al resto de componentes. Esto se logra con la función ReactDOM.render, que como primer parámetro indica el componente raíz y como segundo el elemento del documento html donde vamos a montar toda la aplicación.
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Flujo de propiedades y eventos
Otro fundamento de React es el modo en que se comparte información entre los diferentes componentes. Acostumbrados a tener listeners en cualquier lugar en javascript tradicional, ahora se usan las propiedades para pasar información de padres a hijos, y los eventos se disparan y van subiendo hacía el padre donde en algún momento serán capturados y "manejados".
Las props se pasan de padres a hijos y los eventos se disparan de hijos a padres.
En el siguiente ejemplo vemos como el elemento hijo es un botón que recibe el texto que muestra como propiedad desde su padre, y por el contrario, cuando dispara el evento onClick (nota el camelCase de JSX), el evento viaja hasta el padre, en concreto hasta la función manejadora que también pasó por propiedades.
// PADRE
//////////////////////////////////////////////////////////////////////
handleOnClick(e){
this.setState({
loading: true
});
}
render(){
return(
<ChildComponent text="Click on me!" handleOnClick={this.handleOnClick} />
)
}
// HIJO
/////////////////////////////////////////////////////////////////////
function ChildComponent(props){
return(<button onClick={this.props.handleOnClick}>{this.props.text}</Button>);
}
Ejemplo práctico: generador de paletas de colores
No vamos a ver más teoría y vamos a pasar ya a un ejemplo que he preparado donde se puede ver cómo se aplican los conceptos. He decidido crear un generador de paletas cromáticas a partir de una foto, totalmente serverless. Usando Web workers para llevar los cálculos a un thread separado de la UI (pero eso ya lo veremos en otro artículo). Para el cálculo de la paleta de colores he decidido usar un algoritmo muy sencillito: El Algoritmo de Popularidad [Heckbert, 1982]. Podéis encontrar mucha más información de éste y otros métodos de Selección de los color es esenciales de una imagen digital en esta tesis de Sergio A. Márquez De Silva.
Todo el código fuente lo tengo en github y también lo he desplegado para que lo veas en funcionamiento aquí: https://davidinformatico.com/palettecreator/
He decidido que los componentes con estado sean AppComponent e ImageComponent. El primero contiene un flag para indicar que la aplicación está cargando y un objeto para guardar un elemento Image con la imagen seleccionada.
El segundo componente con estado posee un array de la paleta de colores predominantes, resultado de aplicar el algoritmo sobre la imagen.
Componentes
Voy a ir comentando el código de los diferentes componentes:
AppComponent
render(){
return(
<div className={"d-flex flex-column align-items-center "+styles.main}>
<h1 className={styles.h1}>Image palette generator</h1>
<ToastComponent />
<FileSelectorComponent onChange={this.handleFileOnChange}/>
<LoadingComponent loading = {this.state.loading} />
<ImageComponent img={this.state.image} loading = {this.state.loading} onStart={this.handleOnStartCalculation} onEnd={this.handleOnEndCalculation}/>
</div>
)
}
Vemos que en su método render incluye a sus hijos mediante un tag con el nombre que hemos usado para definir sus respectivas clases o funciones. También observamos que se establecen clases css, que en JSX debe hacerse con el atributo className en lugar de class.
En la asignación del className vemos que abrimos llaves para usar javascript y concatenamos un string con los estilos estáticos de bootstrap a los estilos que nos proporciona el objeto styles, importado gracias a un loader de webpack para disponer de css modules (cada componente tiene su espacio de nombres para las clases css). Si quieres entender bien qué es webpack o un loader mira el artículo de configuración del entorno.
Por último vemos que estamos pasando props a varios componentes: como por ejemplo, pasamos la imagen seleccionada al componente ImageComponent, o el manejador para el evento onChange al FileSelectorComponent, que se disparará en el input del hijo y subirá al padre donde lo manejaremos.
Es importante recordar que un componente solo debe renderizar un único nodo, o lo que es lo mismo, si tiene varios hijos tendrás que envolverlos con algún otro nodo, como por ejemplo un div, aunque no lo usemos para nada más.
class AppComponent extends Component{
constructor(props){
super(props);
this.state = {
image: null,
loading: false
}
this.handleFileOnChange = this.handleFileOnChange.bind(this);
this.handleOnEndCalculation = this.handleOnEndCalculation.bind(this);
this.handleOnStartCalculation = this.handleOnStartCalculation.bind(this);
}
handleFileOnChange(e){
let img = new Image();
if(e.target.files.length == 1){
img.src = URL.createObjectURL(e.target.files\[0\]); // seleccionamos el primero del array de ficheros seleccionados
img.onload = ()=>{
console.log("lectura finalizada");
this.setState({
image: img,
loading: true //indicamos que vamos a iniciar la lectura de todos los bytes de la imagen
});
};
}
}
handleOnEndCalculation(){
this.setState({
loading: false
});
}
handleOnStartCalculation(){
this.setState({
loading: true
});
}
En el constructor del AppComponent es donde asignamos el estado inicial y aprovechamos para bindear el contexto a sus métodos, para disponer de una referencia correcta cuando usemos this.
Los métodos se encargan de modificar el estado, para lo cual ya no podemos asignar como hicimos en el constructor, siempre deberemos usar el método setState (que es asíncrono, por cierto).
En handleOnFileChange estamos recibiendo el evento del selector de ficheros y aprovechamos para crear un elemento imagen a partir de dicha ruta y la guardamos en el estado una vez ha sido realizada la lectura. Además en ese mismo momento habilitamos el flag de carga ya que vamos a comenzar a procesar la imagen.
LoadingComponent
render(){
if (this.props.loading)
return(
<div className={styles.loading}>
<div className="spinner-border text-info" role="status">
<span className="sr-only">Cargando...</span>
</div>
</div>
)
return null;
}
Como acabamos de ver desde AppComponent pasamos el estado del flag loading como prop al componente stateless encargado de mostrar un spinner girando en mitad de la pantalla siempre que el valor del mismo sea verdadero.
FileSelectorComponent
render(){
return(
<div className={styles.fileSelector}>
Pick an image: <input type="file" name="image" accept="image/\*" onChange={this.props.onChange}/>
</div>
)
}
Este es otro componente sin estado muy sencillito, simplemente se trata de un input que va a disparar un evento onchange (onChange en JSX) hacia la función handleOnChange del padre, que previamente se le había pasado como la propiedad OnChange, un nombre decidido así por mí, pero que podría ser cualquiera.
ImageComponent
Llegamos al componente que más chicha tiene del ejemplo. Toda la lógica ocurre dentro del método componentDidUpdate de su ciclo de vida. Este método se ejecuta cada vez que se produce un cambio en las props o el estado del componente, y lo he sobreescrito para que en cierta condición realice todo el cáclulo de la aplicación.
Antes de nada vamos a ver cómo se recuperan los nodos de un elemento del DOM en React, sin usar el clásico getElementById ni jquery: En el constructor de cada componente podemos crear lo que se llaman referencias (mediante React.createRef), y éstas propiedades de la clase las enlazamos mediante el atributo ref dentro de su JSX correspondiente.
constructor(props){
super(props);
this.rgb2hex = this.rgb2hex.bind(this);
this.handleOnClick = this.handleOnClick.bind(this);
this.ref = React.createRef();
}
render(){
return(
<div className={styles.container}>
<canvas className={styles.canvas} ref={this.canvas}></canvas>
{
<div className= {styles.palette}>
{
this.state.colors && this.state.colors.map((e,index)=>(
<ColorComponent key={index} r={e.r} g={e.g} b={e.b} percentage={e.percentage}/>
))
}
</div>
}
</div>
)
}
componentDidUpdate(prevProps){
if(prevProps.loading==false && this.props.loading==true){
let ctx = this.canvas.current.getContext('2d');
let width = this.props.img.width;
let height = this.props.img.height;
let d\_height = config.WIDTH\_CANVAS \* height / width; //calculamos el alto proporcional
ctx.canvas.width = config.WIDTH\_CANVAS;
ctx.canvas.height = d\_height;
ctx.drawImage(this.props.img, 0, 0, width, height, 0, 0, config.WIDTH\_CANVAS, d\_height);
let imageData = ctx.getImageData(0, 0, config.WIDTH\_CANVAS, d\_height);
let worker = new Worker('worker.js');
worker.onmessage = this.onMessage;
worker.postMessage({image\_data:imageData, total\_pixels: config.WIDTH\_CANVAS\*d\_height});
}
}
Como observamos, el método componentDidUpdate permite realizar comparaciones de las propiedades y estado justo en el momento en que pasan de un valor a otro. En este caso actuamos cuando la propiedad loading pasa de false a true.
En este bloque condicional vamos a rescatar el elemento canvas que tenemos definido en el JSX y su contexto para dibujar en él nuestro objeto imagen, recibido por props. Lo dibujamos con un reescalado para optimizar los cálculos posteriores. En cualquier caso las constantes como siempre y para mantener buenas prácticas las tengo en un fichero config.
Ya solo queda obtener los bytes de dicha imagen con getImageData y crear un webWorker para externalizar el algoritmo de popularidad que uso para calcular la paleta de colores representativos a un hilo separado de los procesos renderizado de UI.
En este artículo veremos el web worker como una caja negra que recibe como parámetros un array con todos los bytes de la imagen y devuelve un array de 10 objetos que definen los colores medios más representativos de la imagen.
Aunque no es el objetivo del artículo, daré la explicación más breve posible del algoritmo:
- Lee todos los píxeles de la foto
- Posiciona sus componentes RGB en un espacio xyz delimitado por un cubo con un rango posible de valores de 0-255 (1 byte) para cada uno de los tres ejes de coordenadas.
- Ahora divide ese cubo en x divisiones resultando en 27 cubos o buckets por defecto (usando 3 divisiones por eje).
- Hace un conteo de cuántos pixeles contiene cada bucket y se queda únicamente con los 10 cubos que más puntos contienen, los más representativos.
- Por último hace la media de todos los píxeles de cada cubo para obtener cada uno de los colores finales de la paleta generada.
onMessage(e){
this.setState({
colors: e.data
}, this.props.onEnd());
}
render(){
return(
<div className={styles.container}>
<canvas className={styles.canvas} ref={this.canvas}></canvas>
{
<div className= {styles.palette}>
{
this.state.colors && this.state.colors.map((e,index)=>(
<ColorComponent key={index} r={e.r} g={e.g} b={e.b} percentage={e.percentage}/>
))
}
</div>
}
</div>
)
}
Cuando el web worker finaliza emite un evento onMessage que contiene un objeto con los datos de la paleta de colores generada, que recibe nuestro componente ImageComponent y almacena en su estado. Cuando el cambio de estado se ha ejecutado se llama al callback onEnd, en el padre. En dicho método simplemente se cambiará el estado de la aplicación de loading:true a loading:false.
Una cosa que no he dicho y debes saber: un cambio de estado fuerza una llamada al método render.
El componente imagen contiene un bucle usando map (programación funcional), que itera por todos los colores de la paleta y renderiza un ColorComponent por cada uno de ellos. NOTA: Cuando un mismo nodo contiene varios nodos hijo del mismo tipo, React necesita diferenciarlos, obligandonos a definir un atributo key en cada uno de ellos.
ColorComponent
Lo primero que nos llama la atención de este componente stateless es que hemos sobrescrito un método de su ciclo de vida: componentDidMount, que es llamado justo cuando se ha finalizado render. Por ello es el momento en que podemos acceder al DOM, en este caso usando bootstrap para definir un popover que muestre información del color al pasar por encima el cursor del ratón.
componentDidMount(){
$(this.ref.current).popover({content: \`R: ${this.props.r}, G: ${this.props.g}, B: ${this.props.b} (${this.props.percentage}%)\`, trigger: "hover", placement:"bottom"});
}
Por lo demás el componente no tiene nada especial, tan solo un par de métodos:
- rgb2hex: que convierte un color rgb a un string con el código hexadecimal correspondiente.
- handleOnClick: el manejador del evento onClick, que copia el código hexadecimal al portapapeles y muestra un Bootstrap Toast avisando al usuario de ello.
rgb2hex(r,g,b){
return "#" +
("0" + parseInt(r,10).toString(16)).slice(-2) +
("0" + parseInt(g,10).toString(16)).slice(-2) +
("0" + parseInt(b,10).toString(16)).slice(-2);
}
handleOnClick(e){
$('.toast').toast("show");
var range = document.createRange();
range.selectNode(this.ref.current);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
}
render(){
return(
<div className={styles.color} style={{backgroundColor: \`rgb(${this.props.r}, ${this.props.g}, ${this.props.b})\`}}
onClick={this.handleOnClick} ref={this.ref}>
{this.rgb2hex(this.props.r,this.props.g,this.props.b)}
</div>
)
}
Conclusión
Espero que este pequeño ejemplo sirva para orientar y resolver las primeras dudas que se encuentran todos aquellos que sienten curiosidad por conocer React.js. Realmente se trata de una librería muy agradable de usar y que otorga muy buen rendimiento a los desarrollos.
Este primer ejemplo no pretendía abarcar todo los temas importantes ni hacerlo en profundidad. Pero en futuros artículos probablemente siga ampliando este ejemplo con el uso de Redux y más adelante con un API que respalde todo en una base de datos. Tal vez sea interesante plantear un muro tipo pinterest donde mediante un sistema de usuarios se puedan subir y compartir paletas de colores generadas a partir de imágenes, con un sistema de likes y tags, añadir cálculo de colores análogos y complementarios..... Son ideas que me vienen así a bote pronto.