
Considere a implementação de solicitar dados à API usando o novo amigo React Hooks e os bons velhos amigos Render Prop e HOC (Higher Order Component). Descubra se um novo amigo é realmente melhor que os dois antigos.
A vida não pára, o React está mudando para melhor. Em fevereiro de 2019, o React Hooks apareceu no React 16.8.0. Agora, nos componentes funcionais, você pode trabalhar com o estado local e executar efeitos colaterais. Ninguém acreditava que era possível, mas todos sempre queriam. Se você não estiver atualizado com os detalhes, clique aqui para obter detalhes.
Os React Hooks possibilitam finalmente abandonar padrões como HOC e Render Prop. Porque durante o uso, várias reivindicações foram acumuladas contra elas:
Para não ser infundado, vejamos um exemplo de como o React Hooks é melhor (ou talvez pior) Render Prop. Vamos considerar o Render Prop, não o HOC, pois na implementação eles são muito semelhantes e o HOC tem mais desvantagens. Vamos tentar escrever um utilitário que processa a solicitação de dados na API. Tenho certeza de que muitos escreveram isso em suas vidas centenas de vezes, bem, vamos ver se é possível ainda melhor e mais fácil.
Para isso, usaremos a popular biblioteca axios. No cenário mais simples, você precisa processar os seguintes estados:
- processo de aquisição de dados (isFetching)
- dados recebidos com sucesso (responseData)
- erro ao receber dados (erro)
- cancelamento da solicitação, se no curso de sua execução os parâmetros da solicitação tiverem sido alterados e um novo
- cancelando uma solicitação se este componente não estiver mais no DOM
1. Cenário simples
Escreveremos o estado padrão e uma função (redutor) que muda de estado dependendo do resultado da solicitação: sucesso / erro.
O que é redutor?Para referência. O redutor chegou até nós da programação funcional e para a maioria dos desenvolvedores de JS do Redux. Essa é uma função que executa um estado e ação anteriores e retorna o próximo 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 essa função em duas abordagens.
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); }
Ganchos de reação
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, do componente usado, obtemos os dados - axios.get (). Processamos sucesso e erro, mudando de estado por meio de despacho (ação). Retorne o estado ao componente. E não esqueça de cancelar a solicitação se o URL mudar ou se o componente for removido do DOM. É simples, mas você pode escrever de maneiras diferentes. Destacamos os prós e os contras das duas abordagens:
O React Hooks permite que você escreva menos código, e esse é um fato indiscutível. Portanto, a eficácia de você como desenvolvedor está aumentando. Mas você tem que dominar um novo paradigma.
Quando existem nomes de ciclos de vida dos componentes, tudo fica muito claro. Primeiro, obtemos os dados depois que o componente apareceu na tela (componentDidMount) e, em seguida, obtemos novamente se props.url mudou e, antes disso, não esquecemos de cancelar a solicitação anterior (componentDidUpdate), se o componente foi removido do DOM, cancelamos a solicitação (componentWillUnmount) .
Mas agora, como causamos um efeito colateral diretamente na renderização, fomos ensinados que isso não é possível. Embora pare, não realmente na renderização. E dentro da função useEffect, que executará algo de forma assíncrona após cada renderização, ou melhor, confirmar e renderizar o novo DOM.
Mas não precisamos depois de cada renderização, mas apenas na primeira renderização e no caso de alterar a URL, que indicamos como o segundo argumento para usar o Effect.
Novo paradigmaPara entender como o React Hooks funciona, é preciso ter consciência de coisas novas. Por exemplo, a diferença entre as fases: confirmar e renderizar. Na fase de renderização, o React calcula quais alterações serão aplicadas no DOM comparando com o resultado da renderização anterior. E na fase de confirmação, o React aplica essas alterações ao DOM. É na fase de consolidação que os métodos são chamados: componentDidMount e componentDidUpdate. Mas o que está escrito em useEffect será chamado após a confirmação de forma assíncrona e, portanto, não bloqueará a renderização do DOM se você decidir acidentalmente acidentalmente sincronizar muitas coisas no efeito colateral.
Conclusão - use useEffect. Escrever menos e mais seguro.
E mais um ótimo recurso: useEffect pode limpar após o efeito anterior e após remover o componente do DOM. Agradecemos a Rx, que inspirou a equipe React para essa abordagem.
O uso do nosso utilitário com React Hooks também é muito mais 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" />; };
A opção React Hooks novamente parece mais compacta e óbvia.
Contras Render Prop:
1) não está claro se o layout foi adicionado ou apenas a lógica
2) se você precisar processar o estado do Render Prop no estado local ou nos ciclos de vida do componente filho, será necessário criar um novo componente
Adicione uma nova funcionalidade - recebimento de dados com novos parâmetros por ação do usuário. Eu queria, por exemplo, um botão que receba um avatar do seu desenvolvedor favorito.
2) Atualizando dados de ação do usuário
Adicione um botão que envia uma solicitação com um novo nome de usuário. A solução mais simples é armazenar o nome de usuário no estado local do componente e transferir o novo nome de usuário do estado, não props como é agora. Mas teremos copiar e colar sempre que precisarmos de funcionalidade semelhante. Então, colocamos essa funcionalidade em nosso utilitário.
Vamos usá-lo assim:
const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... };
Vamos escrever uma implementação. Abaixo estão escritas apenas as alterações comparadas com a versão 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); } }
Ganchos de reação
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]; };
Se você analisou cuidadosamente o código, notou:
- O URL começou a ser armazenado dentro do nosso utilitário;
- defaultUrl pareceu identificar que o URL foi atualizado por meio de adereços. Precisamos monitorar a alteração de props.url, caso contrário, uma nova solicitação não será enviada;
- adicionou a função de atualização, que retornamos ao componente para enviar uma nova solicitação clicando no botão
Observe que, com o Render Prop, tivemos que usar getDerivedStateFromProps para atualizar o estado local, caso as alterações de props.url. E com React Hooks sem novas abstrações, você pode chamar imediatamente a atualização de estado na renderização - viva, camaradas, finalmente!
A única complicação com o React Hooks foi memorizar a função de atualização para que ela não mudasse entre as atualizações de componentes. Quando, como no Render Prop, a função de atualização é um método de classe.
3) Polling da API no mesmo intervalo ou Polling
Vamos adicionar outro recurso popular. Às vezes, você precisa consultar constantemente a API. Você nunca sabe que seu desenvolvedor favorito alterou a imagem do perfil e não sabe disso. Adicione o parâmetro interval.
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 ); ...
Implementação:
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(); } ...
Ganchos de reação
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]); ... }
Um novo suporte apareceu - pollInterval. Após a conclusão da solicitação anterior via setTimeout, incrementamos requestId. Com ganchos, temos outro useEffect, no qual chamamos setTimeout. E nosso antigo useEffect, que envia uma solicitação, começou a monitorar outra variável - requestId, que nos diz que setTimeout funcionou, e é hora de enviar a solicitação para um novo avatar.
No Render Prop, eu tinha que escrever:
- comparando os valores requestId e isFetching anteriores e novos
- limpar timeoutId em dois lugares
- adicione a propriedade timeoutId à classe
Os React Hooks permitem que você escreva de forma breve e clara o que costumávamos descrever com mais detalhes e nem sempre é claro.
4) O que vem depois?
Podemos continuar expandindo a funcionalidade do nosso utilitário: aceitando diferentes configurações de parâmetros de consulta, armazenando em cache dados, convertendo uma resposta e erros, atualizando à força os dados com os mesmos parâmetros - operações de rotina em qualquer aplicativo da Web grande. Em nosso projeto, há muito tempo levamos isso para um componente (atenção!) Separado. Sim, porque era um suporte de renderização. Mas com o lançamento do Hooks, reescrevemos a função (useAxiosRequest) e até encontramos alguns erros na implementação antiga. Você pode ver e tentar aqui .