Avanzado

ImperativeHandle

¿Qué es useImperativeHandle en React?

El hook useImperativeHandle es un hook avanzado de React que permite personalizar y exponer ciertos métodos de un componente hijo cuando se usa una referencia (con React.forwardRef). Es útil cuando quieres exponer una API personalizada de un componente hijo para que el componente padre pueda interactuar con él de manera controlada.

En lugar de exponer todo el componente, puedes seleccionar y exponer solo algunas funciones específicas, evitando así que el padre tenga acceso a todo el DOM del hijo.

¿Cuándo usar useImperativeHandle?

  • Cuando necesites controlar imperativamente algunos métodos del componente hijo desde el componente padre.

  • Ejemplo típico: Un componente de entrada personalizado (como un input) donde deseas exponer métodos como focus() o clear().

Cómo se usa useImperativeHandle

La estructura básica es la siguiente:

useImperativeHandle(ref, () => ({
  método1,
  método2,
}));
  • ref: La referencia que se pasará al componente padre usando React.forwardRef.

  • Función retornada: Un objeto que contiene los métodos que quieres exponer al padre.

Ejemplo práctico

1. Componente hijo (Input personalizado)

En este ejemplo, tenemos un componente de entrada (CustomInput) que expone un método focus usando useImperativeHandle.

import React, { useRef, useImperativeHandle, forwardRef } from 'react';

// Usamos forwardRef para pasar la referencia del padre al hijo
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // Exponemos el método 'focus' usando useImperativeHandle
  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
    clear() {
      inputRef.current.value = '';
    }
  }));

  return <input ref={inputRef} placeholder="Escribe algo..." />;
});

export default CustomInput;

2. Componente padre

En el componente padre, usamos una referencia (useRef) para interactuar con los métodos expuestos del componente hijo (CustomInput).

import React, { useRef } from 'react';
import CustomInput from './CustomInput';

function ParentComponent() {
  const inputRef = useRef();

  const handleFocusClick = () => {
    inputRef.current.focus(); // Llama al método 'focus' del componente hijo
  };

  const handleClearClick = () => {
    inputRef.current.clear(); // Llama al método 'clear' del componente hijo
  };

  return (
    <div>
      <h2>useImperativeHandle Example</h2>
      <CustomInput ref={inputRef} />
      <button onClick={handleFocusClick}>Focus Input</button>
      <button onClick={handleClearClick}>Clear Input</button>
    </div>
  );
}

export default ParentComponent;

Explicación del ejemplo:

  1. forwardRef: Se usa para permitir que el componente CustomInput acepte una referencia (ref) desde el componente padre.

  2. useImperativeHandle: Permite exponer los métodos focus y clear del input.

  3. Interacción: En el componente padre, podemos llamar a inputRef.current.focus() o inputRef.current.clear() para interactuar directamente con el input.

¿Por qué usar useImperativeHandle?

  • Encapsulamiento: Ayuda a mantener el encapsulamiento. No expone todo el DOM del hijo, solo los métodos necesarios.

  • Control de acceso: Puedes controlar cuáles métodos se exponen al componente padre.

  • Uso imperativo: Proporciona una manera de interactuar imperativamente con componentes funcionales, similar a cómo se haría con métodos de clase.

Buenas prácticas

  • Úsalo solo cuando realmente necesites exponer métodos específicos al componente padre. El desarrollo de componentes React debe ser principalmente declarativo.

  • Acompáñalo siempre con forwardRef para poder pasar y recibir referencias de padres a hijos.

En resumen, useImperativeHandle es útil para crear APIs controladas y encapsuladas para componentes React, permitiendo una manipulación imperativa solo en los casos que lo ameritan.

Race Conditions

¿Qué es una Race Condition (Condición de Carrera) en React?

Una Race Condition o Condición de Carrera es un error de programación que ocurre cuando múltiples operaciones asíncronas compiten para modificarse o leerse al mismo tiempo, y el resultado final depende del orden en el que se completen esas operaciones. Esto puede llevar a comportamientos inesperados, inconsistencias de datos o errores en tu aplicación.

En React, las race conditions suelen ocurrir en situaciones donde múltiples efectos, peticiones de red o actualizaciones de estado ocurren de manera asíncrona, y el resultado depende del orden de finalización de esas operaciones.

Ejemplo de una Race Condition en React

Imagina que tienes un componente que hace una solicitud de datos a una API cada vez que cambia el valor de un input:

import React, { useState, useEffect } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [data, setData] = useState(null);

  useEffect(() => {
    // Realiza una petición de datos basada en el query
    const fetchData = async () => {
      const response = await fetch(`https://api.example.com/search?q=${query}`);
      const result = await response.json();
      setData(result);
    };

    if (query) {
      fetchData();
    }
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Buscar..."
      />
      <div>Resultado: {JSON.stringify(data)}</div>
    </div>
  );
}

export default SearchComponent;

¿Qué puede salir mal?

Si el usuario escribe rápidamente diferentes valores en el input, múltiples peticiones se envían al servidor, y debido a que estas peticiones son asíncronas, la respuesta de la API puede llegar en un orden diferente al que fueron enviadas. Esto significa que los datos mostrados en la interfaz pueden no corresponder al último input ingresado.

Por ejemplo:

  1. El usuario escribe "a" y se realiza una solicitud para ?q=a.

  2. Luego escribe "ab" y se realiza una solicitud para ?q=ab.

  3. Si la solicitud para ?q=a finaliza después de la solicitud para ?q=ab, el estado se actualizará con datos incorrectos, mostrando resultados para ?q=a en lugar de ?q=ab.

Cómo prevenir Race Conditions en React

Para evitar condiciones de carrera, podemos utilizar diferentes técnicas:

1. Cancelación de peticiones asíncronas

Una forma es utilizar un mecanismo de cancelación, como AbortController:

import React, { useState, useEffect } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchData = async () => {
      try {
        const response = await fetch(`https://api.example.com/search?q=${query}`, { signal });
        const result = await response.json();
        setData(result);
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Request was cancelled');
        }
      }
    };

    if (query) {
      fetchData();
    }

    // Limpiar el efecto para cancelar la solicitud anterior
    return () => controller.abort();
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Buscar..."
      />
      <div>Resultado: {JSON.stringify(data)}</div>
    </div>
  );
}

export default SearchComponent;

Explicación:

  • AbortController permite cancelar una solicitud fetch antes de que se complete.

  • Al limpiar el efecto (controller.abort()), la solicitud anterior se cancela si se realiza una nueva petición, evitando que las respuestas lleguen fuera de orden.

2. Uso de identificadores únicos para las solicitudes

Otra forma es generar un identificador único para cada solicitud y verificarlo antes de actualizar el estado:

useEffect(() => {
  let isCurrent = true; // Identificador para la solicitud actual

  const fetchData = async () => {
    const response = await fetch(`https://api.example.com/search?q=${query}`);
    const result = await response.json();
    if (isCurrent) {
      setData(result);
    }
  };

  if (query) {
    fetchData();
  }

  // Cambiar el identificador para cancelar la actualización de estado
  return () => {
    isCurrent = false;
  };
}, [query]);

Explicación:

  • isCurrent actúa como un identificador para la solicitud activa.

  • Si una nueva solicitud comienza, la variable isCurrent cambia a false, impidiendo que el estado se actualice para solicitudes previas.

Resumen

  • Race Condition ocurre cuando múltiples operaciones asíncronas compiten para modificar datos y el resultado final depende del orden de finalización.

  • En React, esto puede suceder cuando se actualizan estados basados en operaciones asíncronas, como solicitudes HTTP.

  • Técnicas para evitarlo:

    • Usar AbortController para cancelar solicitudes anteriores.

    • Usar identificadores únicos o banderas (isCurrent) para verificar qué solicitud es la actual.

Evitar race conditions es importante para asegurar que tu aplicación React se comporte de forma predecible y que los datos mostrados sean siempre correctos.

Last updated