¿Por qué estoy decepcionado con los ganchos?

La traducción del artículo se preparó antes del inicio del curso "Desarrollador React.js" .










¿Qué utilidad tienen los ganchos?



Antes de decirles qué y por qué me decepcionó, quiero declarar oficialmente que, de hecho, soy un fanático de los ganchos .



A menudo escucho que los ganchos se crean para reemplazar los componentes de la clase. Desafortunadamente, la publicación "Introducción a Hooks" publicada en el sitio web oficial de React anuncia esta innovación, francamente, desafortunada: los



Hooks son una innovación en React 16.8 que le permite usar el estado y otras características de React sin clases de escritura.



El mensaje que veo aquí es algo como esto: "¡Las clases no son geniales!". No es suficiente para motivarte a usar ganchos. En mi opinión, los ganchos nos permiten resolver problemas de funcionalidad transversal de forma más elegante que los enfoques anteriores: mixins , componentes de orden superior y accesorios de renderizado .



Las funciones de registro y autenticación son comunes a todos los componentes, y los enlaces permiten que estas funciones reutilizables se adjunten a los componentes.



¿Qué pasa con los componentes de la clase?



Hay una belleza incomprensible en un componente sin estado (es decir, un componente sin un estado interno) que toma props como entrada y devuelve un elemento React. Esta es una función pura, es decir, una función sin efectos secundarios.



export const Heading: React.FC<HeadingProps> = ({ level, className, tabIndex, children, ...rest }) => {
  const Tag = `h${level}` as Taggable;

  return (
    <Tag className={cs(className)} {...rest} tabIndex={tabIndex}>
      {children}
    </Tag>
  );
};


Desafortunadamente, la falta de efectos secundarios limita el uso de componentes sin estado. Después de todo, la manipulación estatal es esencial. En React, esto significa que se agregan efectos secundarios a los beans de clase que tienen estado. También se denominan componentes de contenedores. Realizan efectos secundarios y pasan apoyos a funciones puras: componentes sin estado.



Hay algunos problemas conocidos con los eventos del ciclo de vida basados ​​en clases. Mucha gente no está contenta de tener que repetir la lógica componentDidMounty los métodos componentDidUpdate.



async componentDidMount() {
  const response = await get(`/users`);
  this.setState({ users: response.data });
};

async componentDidUpdate(prevProps) {
  if (prevProps.resource !== this.props.resource) {
    const response = await get(`/users`);
    this.setState({ users: response.data });
  }
};


Tarde o temprano, todos los desarrolladores se enfrentan a este problema.



Este código de efectos secundarios se puede ejecutar en un solo componente utilizando un gancho de efectos.



const UsersContainer: React.FC = () => {
  const [ users, setUsers ] = useState([]);
  const [ showDetails, setShowDetails ] = useState(false);

 const fetchUsers = async () => {
   const response = await get('/users');
   setUsers(response.data);
 };

 useEffect( () => {
    fetchUsers(users)
  }, [ users ]
 );

 // etc.


Hook useEffecthace la vida mucho más fácil, pero priva de esa función pura, el componente sin estado, que usamos antes. Esto es lo primero que me decepcionó.



Otro paradigma de JavaScript para conocer



Tengo 49 años y soy fan de React. Después de desarrollar una aplicación de ascuas con este observador y la locura de la propiedad computada, siempre tendré un sentimiento cálido por el flujo de datos unidireccional.



El problema con los ganchos useEffecty similares es que no se usan en ningún otro lugar del panorama de JavaScript. Es inusual y generalmente extraño. Solo veo una manera de domarlo: usar este gancho en la práctica y sufrir. Y ningún ejemplo de contadores me inducirá a codificar desinteresadamente durante toda la noche. Soy autónomo y no solo uso React, sino también otras bibliotecas, y ya estoy cansado.realizar un seguimiento de todas estas innovaciones. Tan pronto como pienso que necesito instalar el complemento eslint, que me pondrá en el camino correcto, este nuevo paradigma comienza a tensarme.



Las matrices de dependencia son el infierno



El gancho useEffect puede tomar un segundo argumento opcional llamado matriz de dependencia , que le permite volver a llamar al efecto cuando lo necesite. Para determinar si se ha producido un cambio, React compara los valores entre sí utilizando el método Object.is . Si algún elemento ha cambiado desde el último ciclo de renderizado, el efecto se aplicará a los nuevos valores.



La comparación es excelente para manejar tipos de datos primitivos. Pero si uno de los elementos es un objeto o una matriz, pueden surgir problemas. Object.is compara objetos y matrices por referencia y no puede hacer nada al respecto. No se puede aplicar el algoritmo de comparación personalizado.



Validar objetos por referencia es un obstáculo conocido. Echemos un vistazo a una versión simplificada de un problema que encontré recientemente.



const useFetch = (config: ApiOptions) => {
  const  [data, setData] = useState(null);

  useEffect(() => {
    const { url, skip, take } = config;
    const resource = `${url}?$skip=${skip}&take=${take}`;
    axios({ url: resource }).then(response => setData(response.data));
  }, [config]); // <-- will fetch on each render

  return data;
};

const App: React.FC = () => {
  const data = useFetch({ url: "/users", take: 10, skip: 0 });
  return <div>{data.map(d => <div>{d})}</div>;
};


En la línea 14 , se useFetchpasará un nuevo objeto a la función para cada render, a menos que lo hagamos para que se use el mismo objeto cada vez. En este escenario, querrá verificar los campos del objeto, no la referencia a él.



Entiendo por qué React no hace comparaciones profundas de objetos como esta solución . Por lo tanto, debe utilizar el gancho con cuidado, de lo contrario pueden surgir problemas graves con el rendimiento de la aplicación. Me pregunto constantemente qué se puede hacer al respecto y ya he encontrado varias opciones . Para objetos más dinámicos, tendrá que buscar más soluciones.



Hay un complemento eslint para corregir errores automáticamenteencontrado durante la validación del código. Es adecuado para cualquier editor de texto. Para ser honesto, estoy molesto por todas estas nuevas funciones que requieren la instalación de un complemento externo para probarlas.



La mera existencia de complementos como use-deep-object-compare y use-memo-one sugiere que realmente hay un problema (o al menos un lío).



React se basa en el orden en que se llaman los ganchos



Los primeros enlaces personalizados fueron implementaciones múltiples de una función useFetchpara realizar solicitudes a una API remota. La mayoría de ellos no resuelven el problema de realizar solicitudes de API remotas desde un controlador de eventos, porque los ganchos solo se pueden usar al comienzo de un componente funcional.



Pero, ¿qué pasa si hay enlaces a sitios paginados en los datos y queremos volver a ejecutar el efecto cuando el usuario hace clic en el enlace? Aquí hay un caso de uso simple useFetch:



const useFetch = (config: ApiOptions): [User[], boolean] => {
  const [data, setData] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { skip, take } = config;

    api({ skip, take }).then(response => {
      setData(response);
      setLoading(false);
    });
  }, [config]);

  return [data, loading];
};

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<ApiOptions>({
    take: 10,
    skip: 0
  });

  const [users, loading] = useFetch(currentPage);

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {users.map((u: User) => (
        <div>{u.name}</div>
      ))}
      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li>
            <button onClick={() => console.log(' ?')}>{n + 1}</button>
          </li>
        ))}
      </ul>
    </>
  );
};


En la línea 23, el gancho useFetchse llamará una vez en el primer render. En las líneas 35 a 38, representamos los botones de paginación. Pero, ¿cómo llamaríamos al gancho useFetchdel controlador de eventos para estos botones?



Las reglas de los



ganchos establecen claramente: no use ganchos dentro de bucles, condicionales o funciones anidadas; en su lugar, use siempre ganchos solo en el nivel superior de las funciones de React.



Los ganchos se llaman en el mismo orden cada vez que se procesa el componente. Hay varias razones para esto, que puede conocer en esta excelente publicación.



No puedes hacer esto:



<button onClick={() => useFetch({ skip: n + 1 * 10, take: 10 })}>
  {n + 1}
</button>


Llamar a un gancho useFetchdesde un controlador de eventos viola las reglas de los ganchos, porque el orden de su llamada cambia con cada representación.



Devolver una función ejecutable desde un gancho



Estoy familiarizado con dos soluciones a este problema. Adoptan el mismo enfoque y me gustan ambos. El complemento react-async-hook devuelve una función del gancho execute:



import { useAsyncCallback } from 'react-async-hook';

const AppButton = ({ onClick, children }) => {
  const asyncOnClick = useAsyncCallback(onClick);
  return (
    <button onClick={asyncOnClick.execute} disabled={asyncOnClick.loading}>
      {asyncOnClick.loading ? '...' : children}
    </button>
  );
};

const CreateTodoButton = () => (
  <AppButton
    onClick={async () => {
      await createTodoAPI('new todo text');
    }}
  >
    Create Todo
  </AppButton>
);


Llamar al gancho useAsyncCallbackdevolverá un objeto con las propiedades de carga, error y resultado esperadas, así como una función executeque se puede llamar desde un controlador de eventos.



React-hooks-async es un complemento con un enfoque similar. Utiliza una función useAsyncTask.



Aquí hay un ejemplo completo con una versión simplificada useAsyncTask:



const createTask = (func, forceUpdateRef) => {
  const task = {
    start: async (...args) => {
      task.loading = true;
      task.result = null;
      forceUpdateRef.current(func);
      try {
        task.result = await func(...args);
      } catch (e) {
        task.error = e;
      }
      task.loading = false;
      forceUpdateRef.current(func);
    },
    loading: false,
    result: null,
    error: undefined
  };
  return task;
};

export const useAsyncTask = (func) => {
  const forceUpdate = useForceUpdate();
  const forceUpdateRef = useRef(forceUpdate);
  const task = useMemo(() => createTask(func, forceUpdateRef), [func]);

  useEffect(() => {
    forceUpdateRef.current = f => {
      if (f === func) {
        forceUpdate({});
      }
    };
    const cleanup = () => {
      forceUpdateRef.current = () => null;
    };
    return cleanup;
  }, [func, forceUpdate]);

  return useMemo(
    () => ({
      start: task.start,
      loading: task.loading,
      error: task.error,
      result: task.result
    }),
    [task.start, task.loading, task.error, task.result]
  );
};


La función createTask devuelve un objeto de tarea en la siguiente forma.



interface Task {
  start: (...args: any[]) => Promise<void>;
  loading: boolean;
  result: null;
  error: undefined;
}


El trabajo tiene estados , y lo que esperamos. Pero la función también devuelve una función startque se puede llamar más tarde. El trabajo creado con la función createTaskno afecta la actualización. La actualización se activa mediante funciones forceUpdatey forceUpdateRefen useAsyncTask.



Ahora tenemos una función startque se puede llamar desde un controlador de eventos o desde otro fragmento de código, no necesariamente desde el comienzo de un componente funcional.



Pero perdimos la capacidad de llamar al gancho en la primera ejecución del componente funcional. Es bueno que el complemento react-hooks-async contenga una función useAsyncRun; esto facilita las cosas:



export const useAsyncRun = (
  asyncTask: ReturnType<typeof useAsyncTask>,
  ...args: any[]
) => {
  const { start } = asyncTask;
  useEffect(() => {
    start(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asyncTask.start, ...args]);
  useEffect(() => {
    const cleanup = () => {
      //   
    };
    return cleanup;
  });
};


La función startse ejecutará siempre que cambie alguno de los argumentos args. Ahora el código con ganchos se ve así:



const App: React.FC = () => {
  const asyncTask = useFetch(initialPage);
  useAsyncRun(asyncTask);

  const { start, loading, result: users } = asyncTask;

  if (loading) {
    return <div>loading....</div>;
  }

  return (
    <>
      {(users || []).map((u: User) => (
        <div>{u.name}</div>
      ))}

      <ul>
        {[...Array(4).keys()].map((n: number) => (
          <li key={n}>
            <button onClick={() => start({ skip: 10 * n, take: 10 })}>
              {n + 1}
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};


De acuerdo con las reglas de los ganchos, usamos un gancho useFetchal comienzo de un componente funcional. La función useAsyncRunllama a la API desde el principio, y startusamos la función en el controlador onClickpara los botones de paginación.



Ahora el gancho useFetchse puede utilizar para el propósito previsto, pero, desafortunadamente, debe recurrir a soluciones alternativas. También usamos un cierre, que, debo admitir, me asusta un poco.



Control de ganchos en programas de aplicación



En las aplicaciones, todo debería funcionar según lo previsto. Si planea realizar un seguimiento de los problemas relacionados con los componentes Y las interacciones del usuario con componentes específicos, puede usar LogRocket .







LogRocket es una especie de grabadora de video de aplicación web que registra casi todo lo que sucede en el sitio. El complemento LogRocket para React le permite encontrar sesiones de usuario durante las cuales el usuario hizo clic en un componente específico de su aplicación. Comprenderá cómo los usuarios interactúan con los componentes y por qué algunos componentes no representan nada.



LogRocket registra todas las acciones y estados de la tienda Redux. Es un conjunto de herramientas para su aplicación que le permite registrar solicitudes / respuestas con encabezados y cuerpos. Escriben HTML y CSS en la página, proporcionando una representación píxel por píxel incluso para las aplicaciones de una sola página más complejas.



LogRocket ofrece un enfoque moderno para depurar aplicaciones React; pruébelo gratis .



Conclusión



Creo que el ejemplo c useFetchexplica mejor por qué me frustran los ganchos.



Alcanzar el resultado deseado resultó no ser tan fácil como esperaba, pero aún entiendo por qué es tan importante usar los ganchos en un orden específico. Desafortunadamente, nuestras capacidades están muy limitadas debido al hecho de que los ganchos solo se pueden llamar al comienzo de un componente funcional, y tendremos que buscar más soluciones alternativas. La solución es useFetchbastante complicada. Además, al usar ganchos, no puede prescindir de cierres. Los cierres son continuas sorpresas que han dejado muchas cicatrices en mi alma.



Cierres (como los pasados useEffectyuseCallback) puede tomar versiones anteriores de accesorios y valores de estado. Esto sucede, por ejemplo, cuando una de las variables capturadas falta en la matriz de entrada por alguna razón; pueden surgir dificultades.



El estado obsoleto que se produce después de ejecutar código en un cierre es uno de los problemas para los que está diseñado el hook linter. Hay muchas preguntas en Stack Overflow sobre ganchos obsoletos useEffecty similares. He envuelto funciones useCallbacky retorcido las matrices de dependencia de esta manera para deshacerme del estado obsoleto o la repetición infinita de un problema de renderizado. No puede ser de otra manera, pero es un poco molesto. Este es un problema real que tienes que resolver para demostrar tu valía.



Al comienzo de esta publicación, dije que en general me gustan los ganchos. Pero parecen muy complicados. No hay nada parecido en el panorama actual de JavaScript. Llamar a hooks cada vez que se renderiza un componente funcional crea problemas que los mixins no crean. La necesidad de un linter para usar este patrón no es muy creíble y los cierres son un problema.



Espero haber entendido mal este enfoque. Si es así, escríbalo en los comentarios.





Lee mas:






All Articles