Aprende Javascript con MentoringJS - Step 10
Esta es la segunda parte de mi revisión del tutorial de react-express.
La primera parte se puede ver en mi blog.
Índice de ejercicios
7.Event Handling I
8.Event Handling II
9.Custom Components and Events
10.Input Handling
11.Conditional Rendering I y II
12.Conditional Rendering III
13.Lists and keys
14.Refs and the DOM
import React, { Component } from 'react'
import { render } from 'react-dom'
class CounterButton extends Component {
state = {count: 0}
render() {
const {count} = this.state
return (
<button type='button' onClick={() => this.setState({count: count + 1})}>
Click HERE to increment: {count}
</button>
)
}
}
render(<CounterButton />, document.querySelector('#app'))
Para empezar, están los dos imports habituales, el component de react y el render de react-dom.
Después se crea un componente con forma de clase: CounterButton.
En este componente se añade el método state con una propiedad, en este caso count. Esta propiedad count se inicializa con un valor de 0. En el método render se declara la variable count y se iguala a this.state para poder utilizarla directamente como {count} sin necesidad de escribir todas las veces this.state.count.
En la parte del return se añade un botón en JSX con el evento onClick para que cada vez que se pinche se cambie el estado de la variable count sumándole una unidad. El cambio se hace con this.setState que es la manera de cambiar el estado establecido en setState en React.
Por último,indicar que para añadir un callback a un evento, se debe pasar una función como un atributo de un elemento React. Esto es lo que pasa en el botón con onClick.
Al final se renderiza el componente CounterButton.
import React, { Component } from 'react'
import { render } from 'react-dom'
class CounterButton extends Component {
state = {count: 0}
handleClick = () => {
const {count} = this.state
this.setState({count: count + 1})
}
render() {
const {count} = this.state
return (
<button type='button' onClick={this.handleClick}>
Click HERE to increment: {count}
</button>
)
}
}
render(<CounterButton />, document.querySelector('#app'))
Esta es una versión mejorada, como se dice en el tutorial.
En el ejemplo anterior se declara una función dentro de los props del elemento React. Esto no es lo mejor ya que cada vez que se llama a render se crea una función nueva ya que el componente compara los props y ve que el prop de onClick ha cambiado. Esto puede causar innecesarios re-renders y un mal rendimiento.
Para evitar dicha situación se puede crear una función handleClick en el cuerpo del componente. Ahí se debe poner la variable const y también el this.setState para ir cambiando el valor de const.
Por último, en el return, el evento onClick llama a la función handleClick.
9. Custom Components and Events
import React, { Component } from 'react'
import { render } from 'react-dom'
class CounterButton extends Component {
render() {
const {onPress, children} = this.props
return (
<button type='button' onClick={onPress}>
{children}
</button>
)
}
}
class App extends Component {
state = {count: 0}
handlePress = () => {
const {count} = this.state
this.setState({count: count + 1})
}
render() {
const {count} = this.state
return (
<CounterButton
count={count}
onPress={this.handlePress}
>
Click HERE to increment: {count}
</CounterButton>
)
}
}
render(<App />, document.querySelector('#app'))
Este ejemplo tiene el mismo resultado que los dos anteriores. La única diferencia es que se hace un componente personalizado con el evento onClick.
Como solo los componentes DOM pueden controlar eventos como onClick, el componente personalizado (en este caso CounterButton) debe renderizar un componente DOM y pasarle el prop onClick. El componente CounterButton simplemente es un puente por el que pasa el evento onClick.
class CounterButton extends Component {
render() {
const {onPress, children} = this.props
return (
<button type='button' onClick={onPress}>
{children}
</button>
)
}
}
Este el componente CounterButton. Aquí se añaden dos constantes, onPress y children que se igualan a this.state para poder utilizarlas directamente por sus nombres sin necesidad de escribir todas las veces this.state.count.
En la parte del return se devuelve un botón JSX con el evento onClick que llama a la constante onPress, que más abajo se definirá.
class App extends Component {
state = {count: 0}
handlePress = () => {
const {count} = this.state
this.setState({count: count + 1})
}
render() {
const {count} = this.state
return (
<CounterButton
count={count}
onPress={this.handlePress}
>
Click HERE to increment: {count}
</CounterButton>
)
}
}
En el componente App se añade el método state con la constante count inicializándola a 0. Después se crea la función handlePress que es dónde se cambia el valor de count añadiéndole una unidad.
En el método render se devuelve contenido JSX llamando a la constante count y a la constante onPress. Esta última llama a la función handlePress.
Con todo ello se consigue que cada vez que se pinche en el botón creado en el componente de CounterButton este llame a la constante onPress que a su vez llama la función handlePress que es dónde se añade una unidad a la constante count que también se añade al DOM.
Tradicionalmente, en el desarrollo web los input de los usuarios se guardan en el DOM y la información que escribe el usuario es extraída del DOM para ser usada en la aplicación. React simplifica esto tratando los elementos input como sin estado. Un elemento input tiene dos props, una value y otra onChange y estas dos props te dan el control total de los inputs sin tener que tocar el DOM.
El siguiente ejemplo muestra que cada vez que se renderiza el input, se pasa el actual valor de value del estado del componente. Cada vez que el usuario escribe en el campo del input, se actualiza el estado para incluir el nuevo estado, lo que lanza una re-renderización. Con esto se puede controlar el valor del campo input sin preocuparse de las operaciones del DOM, que hace React por debajo.
import React, { Component } from 'react'
import { render } from 'react-dom'
class Input extends Component {
state = {value: ''}
handleChange = (e) => {
this.setState({value: e.target.value})
}
render() {
const {value} = this.state
return (
<div>
<label htmlFor={'id'}>
Enter value
</label>
<input
id={'id'}
type={'text'}
value={value}
placeholder={'Placeholder'}
onChange={this.handleChange}
/>
<br />
<br />
My value: {value}
</div>
)
}
}
render(<Input />, document.querySelector('#app'))
Respecto al código en sí, en la clase Input se añade un state vacío y una función handleChange donde se actualiza el valor de value que pasará el usuario más adelante (en el return en el apartado JSX).
En el return se añade código JSX con un label y un input que es dónde irá casi toda la información, incluyendo la prop onChange que recibe el contenido del usuario y lo actualiza llamando a la función handleChange. Por último, se renderiza el valor de value.
11. Conditional Rendering I y II
En este ejemplo se muestra cómo trabajar con condicionales en React.
import React, { Component } from 'react'
import { render } from 'react-dom'
class Card extends Component {
render() {
const {title, subtitle} = this.props
return (
<div style={styles.card}>
<h1 style={styles.title}>{title}</h1>
{subtitle && (
<h2 style={styles.subtitle}>{subtitle}</h2>
)}
</div>
)
}
}
class App extends Component {
render() {
return (
<div>
<Card title={'Title'} />
<Card title={'Title'} subtitle={'Subtitle'} />
</div>
)
}
}
const styles = {
card: {
padding: 20,
margin: 20,
textAlign: 'center',
color: 'white',
backgroundColor: 'skyblue',
border: '1px solid rgba(0,0,0,0.15)',
},
title: {
fontSize: 18,
lineHeight: '24px',
},
subtitle: {
fontSize: 14,
lineHeight: '18px',
},
}
render(<App />, document.querySelector('#app'))
El ejemplo concreto renderiza contenido con el condicional && en función de si existe un prop o no. Si existe el prop subtitulo lo muestra y, si no existe, no lo muestra.
class Card extends Component {
render() {
const {title, subtitle} = this.props
return (
<div style={styles.card}>
<h1 style={styles.title}>{title}</h1>
{subtitle && (
<h2 style={styles.subtitle}>{subtitle}</h2>
)}
</div>
)
}
}
En el componente Card se añaden dos props: title y subtitle. Se devuelve código JSX con estilos CSS (el genérico de card y el de title y subtitle para cada uno) y el condicional && para mostrar contenido en función de si se recibe el prop subtitle.
class App extends Component {
render() {
return (
<div>
<Card title={'Title'} />
<Card title={'Title'} subtitle={'Subtitle'} />
</div>
)
}
}
El componente App, que es el que se renderiza al final, devuelve código JSX con dos llamadas al componente Card. La primera solo con una prop y la segunda con dos. Este resultado muestra dos recuadros, el primero solo muestra el título en h1 mientras que el segundo muestra el título en h1 y el subtítulo en h2.
El condicional && funciona porque como no se le pasa un subtítulo se interpreta la parte izquierda del condicional como undefined por lo que React no renderiza nada más, es decir, lo que hay a la derecha de &&. Cuando se le pasa algo más la parte izquierda es truthy con lo que sí que se revisa lo de la parte derecha de la condición y se renderiza el subtítulo.
Al final se declara un método de estilos CSS con varias propiedades.
También se puede utilizar otro operador para el condicional, como por ejemplo el operador ternario
class Card extends Component {
render() {
const {title, subtitle} = this.props
return (
<div style={styles.card}>
<h1 style={styles.title}>{title}</h1>
{subtitle ? (
<h2 style={styles.subtitle}>{subtitle}</h2>
) : (
<h3 style={styles.empty}>No subtitle</h3>
)}
</div>
)
}
}
En este caso, se renderiza el subtítulo si existe y si no existe se le pasa No subtitle con unos estilos CSS específcos.
El último caso de condicionales es con un if/else.
import React, { Component } from 'react'
import { render } from 'react-dom'
class Card extends Component {
renderContent() {
const {title, subtitle} = this.props
return (
<div>
<h1 style={styles.title}>{title}</h1>
{subtitle ? (
<h2 style={styles.subtitle}>{subtitle}</h2>
) : (
<h3 style={styles.empty}>No subtitle</h3>
)}
</div>
)
}
render() {
const {loading, error} = this.props
let content
if (error) {
content = 'Error'
} else if (loading) {
content = (
<h3 style={styles.empty}>Loading...</h3>
)
} else {
content = this.renderContent()
}
return (
<div style={styles.card}>
{content}
</div>
)
}
}
class App extends Component {
render() {
return (
<div>
<Card loading={true} />
<Card error={true} />
<Card title={'Title'} subtitle={'Subtitle'} />
</div>
)
}
}
const styles = {
card: {
padding: 20,
margin: 20,
textAlign: 'center',
color: 'white',
backgroundColor: 'skyblue',
border: '1px solid rgba(0,0,0,0.15)',
},
title: {
fontSize: 18,
lineHeight: '24px',
},
subtitle: {
fontSize: 14,
lineHeight: '18px',
},
empty: {
fontSize: 12,
lineHeight: '15px',
opacity: 0.5,
}
}
render(<App />, document.querySelector('#app'))
Este ejemplo tiene la primera parte de código igual al anterior ejemplo. Cambia en que declara un método renderContent donde declara las dos constantes title y subtitle:
renderContent() {
const {title, subtitle} = this.props
Después, crea otro método render() y añade dos constantes más y un condicional con if, else if y else.
Los casos son, si la constante error es verdadera, la variable content declarada antes tendrá el contenido Error. Si la constante loading es verdadera, el contenido a mostrar es un código JSX con Loading y estilos CSS (empty). Si alguno no es verdadero se llama al contenido de this._renderContent() que en este caso devolverá “No subtitle” ya que no tienen subtítulo.
render() {
const {loading, error} = this.props
let content
if (error) {
content = 'Error'
} else if (loading) {
content = (
<h3 style={styles.empty}>Loading...</h3>
)
} else {
content = this.renderContent()
}
return (
<div style={styles.card}>
{content}
</div>
)
}
}
El contenido a renderizar y dónde se pueden modificar lo que se muestre está en la clase App.
class App extends Component {
render() {
return (
<div>
<Card loading={true} />
<Card error={true} />
<Card title={'Title'} subtitle={'Subtitle'} />
</div>
)
}
}
A cada componente se le puede pasar una prop especial llamada key. React utiliza dicha key para determinar la identidad del elemento renderizado.
En componentes individuales, a diferencia de las listas de componentes, React automáticamente asigna una key a los elementos basado en el orden de renderizado. Un ejemplo sería:
(
<div>
<h1>Title</h1>
<h2>Subtitle</h2>
</div>
)
La asignación por React de las keys sería la siguiente
div: 0
h1: 0.0
h2: 0.1
En las listas funciona diferente ya que se gestiona mapeando un array con el método map():
import React, { Component } from 'react'
import { render } from 'react-dom'
const data = [
{id: 'a', name: 'Devin'},
{id: 'b', name: 'Gabe' },
{id: 'c', name: 'Kim'},
]
class List extends Component {
render() {
return (
<div>
{data.map(item => <div key={item.id}>{item.name}</div>)}
</div>
)
}
}
render(<List />, document.querySelector('#app'))
En este ejemplo se declara un array llamado “data” con un id y con un name. Para extraer esos datos se utiliza el método map() en formato arrow_functions y con un parámetro, item. Ahí se le pasa contenido JSX con el contenido dinámico de id y name del array. El resultado interno es que se muestra los tres nombres pero además con el id asignado correctamente en cada ítem.
(
<div>
{[
<div key={'a'}>{'Devin'}</div>
<div key={'b'}>{'Gabe'}</div>
<div key={'c'}>{'Kim'}</div>
]}
</div>
)
Esto se puede mejorar utilizando el índex del propio map como key:
(
<div>
{data.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
)
En este caso, se añade el parámetro index a la función que tiene por objetivo utilizar el propio índex de map. El ejemplo sería igual exceptuando que se debería quitar la propiedad id del array. Quedaría así el array:
const data = [
{name: 'Devin'},
{name: 'Gabe' },
{name: 'Kim'},
]
El resultado sería:
(
<div>
{[
<div key={0}>{'Devin'}</div>
<div key={1}>{'Gabe'}</div>
<div key={2}>{'Kim'}</div>
]}
</div>
)
A veces cuando trabajas con React es necesario acceder directamente a los nodos del DOM subyacentes que renderizas, ya que es posible que se quiera medir un nodo o tener la posición de scroll. O incluso es posible que se necesite interactuar con una librería diferente que modifica directamente el DOM. React proporciona una solución para esto con la prop llamada ref.
Trabajando con ref:
Se puede pasar una función callback como ref que será llamada por la instancia del componente antes de la renderización inicial. Se puede guardar la referencia para usarla dentro del ciclo de vida de React. La instancia o bien será un Custom Component o un nodo DOM. En cada caso se puede llamar a métodos de esta instacia.
import React, { Component } from 'react'
import { render } from 'react-dom'
class Card extends Component {
state = {
width: null,
height: null,
}
saveRef = (ref) => this.containerNode = ref
measure() {
const {clientWidth, clientHeight} = this.containerNode
this.setState({
width: clientWidth,
height: clientHeight,
})
}
componentDidMount() {
this.measure()
}
componentDidUpdate() {
this.measure()
}
shouldComponentUpdate(nextProps, nextState) {
return (
this.state.width !== nextState.width ||
this.state.height !== nextState.height
)
}
render() {
const {width, height} = this.state
return (
<div
style={styles.card}
ref={this.saveRef}
>
<h2 style={styles.subtitle}>My dimensions are:</h2>
{width && height && (
<h1 style={styles.title}>{width} x {height}</h1>
)}
</div>
)
}
}
const styles = {
card: {
padding: 20,
margin: 20,
textAlign: 'center',
color: 'white',
backgroundColor: 'skyblue',
border: '1px solid rgba(0,0,0,0.15)',
},
title: {
fontSize: 18,
lineHeight: '24px',
},
subtitle: {
fontSize: 14,
lineHeight: '18px',
},
}
render(<Card />, document.querySelector('#app'))
El funcionamiento general de este ejemplo es que, después del primer renderizado, se guarda el inicial width y height con setState. Esto hace disparar una segunda renderización que muestra estas dos características lo que cambiará el valor de ambas de nuevo. Ese cambio lanza una tercera renderización que mostrará el último valor de width y height, y con fortuna, no se cambiarán las dimensiones de nuevo. Si así pasase, se podría entrar en un bucle infinito. Para evitar el bucle se añade al ciclo de vida shouldComponentUpdate.
Próximo y último capítulo en breve…