
Considere la implementación de solicitar datos a la API utilizando el nuevo amigo React Hooks y los viejos amigos Render Prop y HOC (Componente de orden superior). Averigua si un nuevo amigo es realmente mejor que los dos anteriores.
La vida no se detiene, Reaccionar está cambiando para mejor. En febrero de 2019, React Hooks apareció en React 16.8.0. Ahora en los componentes funcionales puede trabajar con el estado local y realizar efectos secundarios. Nadie creía que fuera posible, pero todos siempre lo quisieron. Si no está actualizado con los detalles, haga clic aquí para obtener más detalles.
Los React Hooks permiten finalmente abandonar patrones como HOC y Render Prop. Porque durante el uso, se han acumulado varias reclamaciones contra ellos:
Para no ser infundado, veamos un ejemplo de cómo React Hooks es mejor (o quizás peor) Render Prop. Consideraremos Render Prop, no HOC, ya que en la implementación son muy similares y HOC tiene más inconvenientes. Intentemos escribir una utilidad que procese la solicitud de datos a la API. Estoy seguro de que muchos han escrito esto en sus vidas cientos de veces, bueno, veamos si es posible aún mejor y más fácil.
Para esto usaremos la popular biblioteca axios. En el escenario más simple, debe procesar los siguientes estados:
- proceso de adquisición de datos (isFetching)
- datos recibidos con éxito (responseData)
- error al recibir datos (error)
- cancelación de la solicitud, si en el curso de su ejecución los parámetros de la solicitud han cambiado y necesita enviar un nuevo
- cancelar una solicitud si este componente ya no está en el DOM
1. Escenario simple
Escribiremos el estado predeterminado y una función (reductor) que cambia de estado según el resultado de la solicitud: éxito / error.
¿Qué es el reductor?Para referencia. Reducer nos llegó de la programación funcional, y para la mayoría de los desarrolladores de JS de Redux. Esta es una función que toma un estado y una acción anteriores y devuelve el siguiente estado.
const defaultState = { responseData: null, isFetching: true, error: null }; function reducer1(state, action) { switch (action.type) { case "fetched": return { ...state, isFetching: false, responseData: action.payload }; case "error": return { ...state, isFetching: false, error: action.payload }; default: return state; } }
Reutilizamos esta función en dos enfoques.
Render prop
class RenderProp1 extends React.Component { state = defaultState; axiosSource = null; tryToCancel() { if (this.axiosSource) { this.axiosSource.cancel(); } } dispatch(action) { this.setState(prevState => reducer(prevState, action)); } fetch = () => { this.tryToCancel(); this.axiosSource = axios.CancelToken.source(); axios .get(this.props.url, { cancelToken: this.axiosSource.token }) .then(response => { this.dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { this.dispatch({ type: "error", payload: error }); }); }; componentDidMount() { this.fetch(); } componentDidUpdate(prevProps) { if (prevProps.url !== this.props.url) { this.fetch(); } } componentWillUnmount() { this.tryToCancel(); } render() { return this.props.children(this.state); }
Reaccionar ganchos
const useRequest1 = url => { const [state, dispatch] = React.useReducer(reducer, defaultState); React.useEffect(() => { const source = axios.CancelToken.source(); axios .get(url, { cancelToken: source.token }) .then(response => { dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { dispatch({ type: "error", payload: error }); }); return source.cancel; }, [url]); return [state]; };
Por url, del componente utilizado, obtenemos los datos - axios.get (). Procesamos el éxito y el error, cambiando el estado a través del envío (acción). Estado de retorno al componente. Y no olvide cancelar la solicitud si la url cambia o si el componente se elimina del DOM. Es simple, pero puedes escribir de diferentes maneras. Destacamos los pros y los contras de los dos enfoques:
React Hooks le permite escribir menos código, y este es un hecho indiscutible. Esto significa que la efectividad de usted como desarrollador está creciendo. Pero tienes que dominar un nuevo paradigma.
Cuando hay nombres de ciclos de vida de componentes, todo está muy claro. Primero, obtenemos los datos después de que el componente apareció en la pantalla (componentDidMount), luego los obtenemos nuevamente si props.url ha cambiado y antes de eso no olvidamos cancelar la solicitud anterior (componentDidUpdate), si el componente se ha eliminado del DOM, luego cancele la solicitud (componentWillUnmount) .
Pero ahora causamos un efecto secundario directamente en el render, nos enseñaron que esto no es posible. Aunque pare, no realmente en el render. Y dentro de la función useEffect, que realizará algo asincrónicamente después de cada render, o más bien confirmará y renderizará el nuevo DOM.
Pero no necesitamos después de cada render, sino solo en el primer render y en caso de cambiar la url, lo que indicamos como el segundo argumento para usar Effect.
Nuevo paradigmaComprender cómo funciona React Hooks requiere ser consciente de las cosas nuevas. Por ejemplo, la diferencia entre las fases: commit y render. En la fase de representación, React calcula qué cambios aplicar en el DOM comparándolos con el resultado de la representación anterior. Y en la fase de confirmación, React aplica estos cambios al DOM. Es en la fase de confirmación que se llaman los métodos: componentDidMount y componentDidUpdate. Pero lo que está escrito en useEffect se llamará después de la confirmación de forma asincrónica y, por lo tanto, no bloqueará la representación DOM si de repente decide sincronizar accidentalmente muchas cosas en el efecto secundario.
Conclusión: use useEffect. Escribir menos y más seguro.
Y una gran característica más: useEffect puede limpiar después del efecto anterior y después de eliminar el componente del DOM. Gracias a Rx que inspiró al equipo React por este enfoque.
Usar nuestra utilidad con React Hooks también es mucho más conveniente.
const AvatarRenderProp1 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`}> {state => { if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; }} </RenderProp> );
const AvatarWithHook1 = ({ username }) => { const [state] = useRequest(`https://api.github.com/users/${username}`); if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; };
La opción React Hooks nuevamente se ve más compacta y obvia.
Contras Render Prop:
1) no está claro si se agrega diseño o solo lógica
2) si necesita procesar el estado desde Render Prop en el estado local o en los ciclos de vida del componente secundario, deberá crear un nuevo componente
Agregue una nueva funcionalidad: recibir datos con nuevos parámetros por acción del usuario. Quería, por ejemplo, un botón que obtenga un avatar de tu desarrollador favorito.
2) Actualización de datos de acción del usuario
Agregue un botón que envíe una solicitud con un nuevo nombre de usuario. La solución más simple es almacenar el nombre de usuario en el estado local del componente y transferir el nuevo nombre de usuario desde el estado, no los accesorios como están ahora. Pero luego tendremos que copiar y pegar donde necesitemos una funcionalidad similar. Entonces ponemos esta funcionalidad en nuestra utilidad.
Lo usaremos así:
const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... };
Escribamos una implementación. A continuación se escriben solo los cambios en comparación con la versión original.
function reducer2(state, action) { switch (action.type) { ... case "update url": return { ...state, isFetching: true, url: action.payload, defaultUrl: action.payload }; case "update url manually": return { ...state, isFetching: true, url: action.payload, defaultUrl: state.defaultUrl }; ... } }
Render prop
class RenderProp2 extends React.Component { state = { responseData: null, url: this.props.url, defaultUrl: this.props.url, isFetching: true, error: null }; static getDerivedStateFromProps(props, state) { if (state.defaultUrl !== props.url) { return reducer(state, { type: "update url", payload: props.url }); } return null; } ... componentDidUpdate(prevProps, prevState) { if (prevState.url !== this.state.url) { this.fetch(); } } ... update = url => { this.dispatch({ type: "update url manually", payload: url }); }; render() { return this.props.children(this.state, this.update); } }
Reaccionar ganchos
const useRequest2 = url => { const [state, dispatch] = React.useReducer(reducer, { url, defaultUrl: url, responseData: null, isFetching: true, error: null }); if (url !== state.defaultUrl) { dispatch({ type: "update url", payload: url }); } React.useEffect(() => { …(fetch data); }, [state.url]); const update = React.useCallback( url => { dispatch({ type: "update url manually", payload: url }); }, [dispatch] ); return [state, update]; };
Si mirabas detenidamente el código, notaste:
- url comenzó a almacenarse dentro de nuestra utilidad;
- defaultUrl pareció identificar que la url se actualizó mediante accesorios. Necesitamos monitorear el cambio de props.url, de lo contrario no se enviará una nueva solicitud;
- agregó la función de actualización, que regresamos al componente para enviar una nueva solicitud haciendo clic en el botón.
Tenga en cuenta que con Render Prop tuvimos que usar getDerivedStateFromProps para actualizar el estado local en caso de que props.url cambie. Y con React Hooks sin nuevas abstracciones, puede llamar inmediatamente a la actualización de estado en el render: ¡hurra, camaradas, finalmente!
La única complicación con React Hooks fue memorizar la función de actualización para que no cambiara entre actualizaciones de componentes. Cuando, como en Render Prop, la función de actualización es un método de clase.
3) Sondeo de la API en el mismo intervalo o sondeo
Agreguemos otra característica popular. A veces necesitas consultar constantemente la API. Nunca se sabe que su desarrollador favorito cambió la imagen de perfil, y no está al tanto. Agregue el parámetro de intervalo.
Uso:
const AvatarRenderProp3 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}> ...
const AvatarWithHook3 = ({ username }) => { const [state, update] = useRequest( `https://api.github.com/users/${username}`, 1000 ); ...
Implementación
function reducer3(state, action) { switch (action.type) { ... case "poll": return { ...state, requestId: state.requestId + 1, isFetching: true }; ... } }
Render prop
class RenderProp3 extends React.Component { state = { ... requestId: 1, } ... timeoutId = null; ... tryToClearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); } } poll = () => { this.tryToClearTimeout(); this.timeoutId = setTimeout(() => { this.dispatch({ type: 'poll' }); }, this.props.pollInterval); }; ... componentDidUpdate(prevProps, prevState) { ... if (this.props.pollInterval) { if ( prevState.isFetching !== this.state.isFetching && !this.state.isFetching ) { this.poll(); } if (prevState.requestId !== this.state.requestId) { this.fetch(); } } } componentWillUnmount() { ... this.tryToClearTimeout(); } ...
Reaccionar ganchos
const useRequest3 = (url, pollInterval) => { const [state, dispatch] = React.useReducer(reducer, { ... requestId: 1, }); React.useEffect(() => { …(fetch data) }, [state.url, state.requestId]); React.useEffect(() => { if (!pollInterval || state.isFetching) return; const timeoutId = setTimeout(() => { dispatch({ type: "poll" }); }, pollInterval); return () => { clearTimeout(timeoutId); }; }, [pollInterval, state.isFetching]); ... }
Ha aparecido un nuevo accesorio: pollInterval. Al completar la solicitud anterior a través de setTimeout, incrementamos requestId. Con los ganchos, tenemos otro useEffect, en el que llamamos setTimeout. Y nuestro viejo useEffect, que envía la solicitud, comenzó a monitorear otra variable: requestId, que nos dice que setTimeout funcionó, y es hora de enviar la solicitud de un nuevo avatar.
En Render Prop, tuve que escribir:
- Comparación de los valores requestId e isFetching anteriores y nuevos
- tiempo de espera claro Id en dos lugares
- agregue la propiedad timeoutId a la clase
React Hooks le permite escribir breve y claramente lo que solíamos describir con más detalle y no siempre es claro.
4) ¿Qué sigue?
Podemos continuar ampliando la funcionalidad de nuestra utilidad: aceptar diferentes configuraciones de parámetros de consulta, almacenar datos en caché, convertir una respuesta y errores, actualizar a la fuerza los datos con los mismos parámetros: operaciones de rutina en cualquier aplicación web grande. En nuestro proyecto, hace mucho tiempo lo hemos tomado en un componente separado (¡atención!). Sí, porque era un Render Prop. Pero con el lanzamiento de Hooks, reescribimos la función (useAxiosRequest) e incluso encontramos algunos errores en la implementación anterior. Puedes ver y probar aquí .