Pasando funciones a componentes
¿Cómo puedo pasar un controlador de eventos (como onClick) a un componente?
Pasa controladores de eventos y otras funciones como props a componentes hijos:
<button onClick={this.handleClick}>
Si necesitas tener acceso al componente padre dentro del evento, también debes enlazar la funciones a la instancia del componente (ver abajo).
¿Cómo enlazo una función a la instancia de un componente?
Hay varias maneras de asegurarte que las funciones tengan acceso a los atributos del componente como this.props
y this.state
, dependiendo de qué tipo de sintaxis o
Enlazar dentro del constructor (ES2015)
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Se hizo click');
}
render() {
return <button onClick={this.handleClick}>Clickéame</button>;
}
}
Propiedades de clases (ES2022)
class Foo extends Component {
handleClick = () => {
console.log('Se hizo click');
}
render() {
return <button onClick={this.handleClick}>Clickéame</button>;
}
}
Enlazar en la renderización
class Foo extends Component {
handleClick() {
console.log('Se hizo click');
}
render() {
return <button onClick={this.handleClick.bind(this)}>Clickéame</button>;
}
}
Nota:
Al usar
Function.prototype.bind
dentro de la renderización se crea una nueva función cada vez que el componente se renderiza, lo cual podría implicar problemas de rendimiento (ver abajo).
Funciones flecha en renderización
class Foo extends Component {
handleClick() {
console.log('Se hizo click');
}
render() {
return <button onClick={() => this.handleClick()}>Clickéame</button>;
}
}
Nota:
Usar una función flecha en el renderizado crea una nueva función cada vez que se renderiza el componente, lo cual podría arruinar optimizaciones basadas en comparación estricta de identidad.
¿Está bien utilizar funciones flecha en los métodos de renderizado?
Generalmente hablando, si está bien y normalmente es la forma más fácil de pasar parámetros a funciones.
Si tienes problemas de rendimiento, ¡no dudes en optimizar!
¿Por qué tiene que ser necesario enlazar?
En JavaScript, estos dos fragmentos de código no son equivalentes.
obj.method();
var method = obj.method;
method();
Los métodos de enlace nos aseguran que el segundo fragmento funcione de la misma manera que el primero.
Con React, normalmente solo necesitamos enlazar los métodos que pasamos a otros componentes. Por ejemplo: <button onClick={this.handleClick}>
pasa this.handleClick
por ende, se debería enlazar. Sin embargo, es innecesario enlazar el método render
o los métodos de ciclo de vida: no los pasamos a otros componentes.
Este artículo creado por Yehuda Katz explica a detalle qué es enlazar, y cómo funcionan las funciones en JavaScript.
¿Por qué mi función está siendo llamada cada vez que mi componente renderiza?
Asegúrate que no estés llamando la función cuando la pases al componente:
render() {
// Incorrecto: ¡Se llama a handleClick en vez de ser pasado como una referencia!
return <button onClick={this.handleClick()}>Clickéame!</button>
}
En lugar de eso, pasa la función como tal (sin los paréntesis)
render() {
// Correcto: handleClick se pasa como una referencia!
return <button onClick={this.handleClick}>Clickéame</button>
}
¿Cómo paso un parámetro a un controlador de eventos o callback?
Puedes utilizar funciones flecha para envolver un controlador de eventos y pasar parámetros:
<button onClick={() => this.handleClick(id)} />
Esto es lo equivalente de llamar .bind
:
<button onClick={this.handleClick.bind(this, id)} />
Ejemplo: Pasar parámetros utilizando función flecha
const A = 65 // código ASCII del carácter.
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(letter) {
this.setState({ justClicked: letter });
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
Ejemplo: Pasando parámetros usando data-attributes
Alternativamente, puedes utilizar APIs del DOM para guardar los datos que necesitan los controladores de eventos. Considera esta propuesta si necesitas optimizar una gran cantidad de elementos o tu árbol de renderizado depende de las verificaciones de igualdad de React.PureComponent.
const A = 65 // código ASCII del carácter
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(e) {
this.setState({
justClicked: e.target.dataset.letter
});
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} data-letter={letter} onClick={this.handleClick}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
¿Cómo puede prevenir que una función sea llamada muy rápidamente o muchas veces seguidas?
Si tienes un controlador de eventos como onClick
o onScroll
y quieres prevenir que un callback sea disparado antes de tiempo, puedes limitar la tasa en la cual este callback es ejecutado. Se puede lograr usando:
- throttle: regula los cambios siguiendo una frecuencia basada en el tiempo (ej.
_.throttle
) - debounce: publica cambios después de un periodo de inactividad (ej.
_.debounce
) - throttle con
requestAnimationFrame
: regula los cambios en base arequestAnimationFrame
(ejraf-schd
)
Mira esta visualización para ver la comparación entre las funciones throttle
y debounce
.
Nota:
_.debounce
,_.throttle
yraf-schd
proporcionan el métodocancel
para cancelar callbacks retrasados. Puedes llamar este método dentro decomponentWillUnmount
o corrobora que el componente sigue montado dentro de la función retrasada.
Throttle
Throttle previene que una función sea llamada más de una vez según el tiempo determinado. El ejemplo que se encuentra debajo regula un controlador de evento tipo click para prevenir que se llame más de una vez por segundo.
import throttle from 'lodash.throttle';
class LoadMoreButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickThrottled = throttle(this.handleClick, 1000);
}
componentWillUnmount() {
this.handleClickThrottled.cancel();
}
render() {
return <button onClick={this.handleClickThrottled}>Load More</button>;
}
handleClick() {
this.props.loadMore();
}
}
Debounce
Debounce asegura que una función no va a ser ejecutada hasta que cierta cantidad de tiempo haya pasado desde la última vez que fue llamada. Esto puede ser muy útil cuando tienes que realizar operaciones demandantes como respuesta de un evento que puede ejecutarse muy rápido (ejemplo eventos de scroll o teclado). El siguiente ejemplo hace debounce de una entrada de texto con un demora de 250ms.
import debounce from 'lodash.debounce';
class Searchbox extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.emitChangeDebounced = debounce(this.emitChange, 250);
}
componentWillUnmount() {
this.emitChangeDebounced.cancel();
}
render() {
return (
<input
type="text"
onChange={this.handleChange}
placeholder="Search..."
defaultValue={this.props.value}
/>
);
}
handleChange(e) {
this.emitChangeDebounced(e.target.value);
}
emitChange(value) {
this.props.onChange(value);
}
}
requestAnimationFrame
throttling
requestAnimationFrame
es una forma de poner una función en cola para ser ejecutada por el navegador en un tiempo óptimo para el rendimiento del renderizado. Una función en cola con requestAnimationFrame
va a dispararse en el siguiente cuadro. El navegador se va a encargar de que hayan 60 cuadros por segundo (60fps). Sin embargo, si el navegador no puede, el mismo navegador naturalmente va a limitar la cantidad de cuadros por segundo. Por ejemplo, un dispositivo podría solo manejar 30 fps, por ende, solo tendrás 30 cuadros por segundo. Usando requestAnimationFrame
para throttle es una técnica muy útil ya que previene que tú mismo generes más de 60 actualizaciones por segundo. Si estás generando 100 actualizaciones por segundo, puedes crear esfuerzo adicional para el navegador que el usuario de todas formas no va a poder apreciar.
Nota:
Usando esta técnica podemos capturar el último valor capturado en un cuadro. Puedes ver a un ejemplo de cómo funciona este tipo de optimización en
MDN
import rafSchedule from 'raf-schd';
class ScrollListener extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
// Crea una nueva función para agendar actualizaciones
this.scheduleUpdate = rafSchedule(
point => this.props.onScroll(point)
);
}
handleScroll(e) {
// Cuando recibimos un evento tipo scroll, agenda una actualización.
// Si recibimos muchas actualizaciones dentro de un cuadro, solo vamos a publicar el último valor
this.scheduleUpdate({ x: e.clientX, y: e.clientY });
}
componentWillUnmount() {
// Cancela cualquier actualización pendiente ya que estamos 'unmounting'.
this.scheduleUpdate.cancel();
}
render() {
return (
<div
style={{ overflow: 'scroll' }}
onScroll={this.handleScroll}
>
<img src="/my-huge-image.jpg" />
</div>
);
}
}
Probando tu límite de cuadros
Cuando probamos límites de cuadros de forma correcta, es muy útil tener la habilidad de adelantar el tiempo. Si estás utilizando jest
puedes usar mock timers
para adelantar el tiempo. Si estás utilizando throttle con requestAnimationFrame
podrías encontrar útil raf-stub
para controlar la frecuencia de los cuadros de animación.