Hooks + Redux
Unido os hooks ao estado global
Introdução
Desde que saiu a versão estável de Hooks para React, eu vejo muitos artigos com a ideia de Usando Hooks para eliminar o Redux da sua aplicação. Alguns desses têm até umas ideias interessantes, mas não é esse o propósito de Hooks, se você quer criar uma biblioteca que substitua o Redux, você deverá estudar ContextAPI e não Hooks.
Hooks e Redux Hooks foram adotados ainda em Alpha — ambos em produção.
Hands on
Logo no começo, tive alguma dificuldade em migrar a forma de uso e acabei parando para analisar Como juntar Hooks com Redux? O ideal seria não precisar do connect, mas também ter a mesma facilidade que ele oferece. Esse foi o primeiro tradeoff que encarei no uso. Apesar dos Hooks do Redux oferecerem bons Hooks com o useSelector e o useDispatch, eu não tinha algumas otimizações. A própria documentação do Redux nos dá formas de fazer essas otimizações, porém considero algo um pouco cru, podemos fazer algo mais robusto para trabalhar nas nossas aplicações. Vamos começar com a minha primeira ideia para otimizar isso:
// useReselect.js
import { useMemo } from "react";
import { shallowEqual, useDispatch, useSelector } from "react-redux";
import { createSelector } from "reselect";
const state = (fn) =>
createSelector(
(_: GlobalState) => fn(_),
(state) => state
);
const useReselect = (states, dispatches, comparator = shallowEqual) => {
const dispatch = useDispatch();
const memoDispatch = Object.keys(dispatches).reduce((acc, fn) => {
return {
...acc,
[fn]: useMemo(
() =>
(...params) =>
dispatch(dispatches[fn](...params)),
[fn]
),
};
}, {});
return [useSelector(state(states), comparator), memoDispatch];
};
export default useReselect;
// Demonstração de uso
const mapStateToProps = (state) => ({
clients: state.ClientReducer.clients,
});
const mapDispatchToProps = { getClients };
const Component = () => {
const [globalState, dispatches] = useReselect();
useEffect(() => {
dispatches.getClients();
}, []);
};
Isso quebrou um galhão, mas eu queria entregar algo mais parecido com o approach de classes para um time que estava mais habituado, e iniciando em componentes funcionais + Hooks. Então eu apenas transformei em um objeto o que era um array
// useConnect.js - O código acima será reutilizado
import useReselect from "./useReselect";
import { shallowEqual } from "react-redux";
const useConnect = (
state,
dispatches,
props = {},
comparator = shallowEqual
) => {
const [globalState, globalDispatches] = useReselect(
state,
dispatches,
comparator
);
return { ...globalState, ...globalDispatches, ...props };
};
// Demonstração de uso
const mapStateToProps = (state) => ({
clients: state.ClientReducer.clients,
});
const mapDispatchToProps = { getClients };
const Component = (externalProps) => {
const props = useConnect(mapStateToProps, mapDispatchToProps, externalProps);
useEffect(() => {
props.getClients();
}, []);
};
O resultado é um hook que entrega algo mais semelhante ao uso com classes, porém sem o this. O useConnect tem comportamento similar ao connect, mas sem criar um componente wrapper para passar props da store.
Apenas isso?
Não apenas isso.
Com os hooks básicos: useState e useEffect, podemos criar ações que iram abstrair bastante do código e ao invés de entregar um valor de forma mais "mastigada" pra quem consumir os hooks. Dois exemplos que vou mostrar aqui são Filtrar listas (com o uso do Redux) e Exibir o um loading (sem o uso do Redux).
// useClientFilter.js - Filtrar uma lista de clientes, dado uma chave e valor
import { useState, useEffect } from "react";
import useConnect from "./";
const stateToProps = (state) => ({ clients: state.ClientReducer.clients });
const useClientFilter = (key, value) => {
const props = useConnect(stateToProps, {});
const [list, setList] = useState(props.clients);
/* Esse efeito vai executar toda vez que:
- A lista mudar de tamanho
- A propriedade key mudar
- A propriedade value mudar
*/
useEffect(() => {
const filterList = props.clients.filter((client) => {
const clientValue = client[key];
return !!clientValue.match(new RegExp(value, "gi"));
});
setList(filterList);
}, [key, value, props.clients.length]);
return list;
};
export default useClientFilter;
Com esse pequeno hook, ao invés de criar o filtro no componente, podemos usa-lo para abstrair o trabalho e replicar seu uso em diversos componentes, sem repetir código.
import React, { useState } from "react";
import useClientFilter from "./hooks";
const ClientList = () => {
const [input, setInput] = useState("");
const clients = useClientFilter("name", input);
const onChange = (e) => setInput(e.target.value);
return (
<Page>
<Input onChange={onChange} value={input} />
{clients.map((client) => (
<Row key={client.name}>
<Text>{client.name}</Text>
<Text>- Status: {client.status}</Text>
</Row>
))}
</Page>
);
};
Demonstrando o hook de filtro para clientes Exemplo cortado :(
Nesse componente, teremos a lista filtrada sempre que o método onChange for executado, pois a regra do nosso useClientFilter diz que se o nosso value mudar, ele irá executar o callback do useEffect;
O segundo exemplo que irei mostrar é para casos onde sua ação não terá impacto na store do seu redux, exceto setar atributos como loading. A partir de agora, essas ações você poderá tratar de uma nova forma, usando-as num hook. Dessa forma, cada ação terá um loading próprio e não irá acontecer problemas de o usuário executar uma ação que faça um trigger de loading no mesmo reducer.
Vamos fazer um exemplo para inativar clientes e atualizar a lista após o deletar ser efetuado com sucesso.
import useConnect from "./hooks";
import { useState } from "react";
const mapStateToProps = () => ({}); //objetos que precisar
const mapDispatchToProps = {}; //ações que precisar
const useDeleteClient = () => {
const [state, setState] = useState({ loading: false, success: false });
const callback = (id) => {
setState({ loading: true, success: false });
fetch("https://api.awesomeurl.dev/client/" + id, { method: "DELETE" })
.then((e) => {
if (e.ok) {
setState({ loading: false, success: true });
// ações do redux serão realizadas aqui
// em caso de sucesso
} else {
setState({ loading: false, success: false });
// ações do redux serão realizadas aqui
// em caso de falha da requisição
}
})
.catch((e) => {
setState({ loading: false, success: false });
// Algum outro erro como de conexão, por exemplo
});
};
return [state, callback];
};
export default useDeleteClient;
// Demonstração
const DeleteClient42Button = () => {
const [state,callback] = useDeleteClient();
const delete = () => callback(42);
if(state.success){
// Um componente de notificação qualquer
Notification.show("O cliente 42 foi deletado")
}
return (
<Button
disabled={state.loading}
onClick={delete}>
Deletar o Cliente 42
</Button>
)
};
E assim podemos ter uma ação com estado próprio, sem a necessidade de criar actions do redux para o controle de fluxo, apenas usando o Hooks.
Por hoje é só
Este artigo demonstra que ao deparar com um artigo do tipo Usando hooks para eliminar o Redux, vale refletir sobre como as duas tecnologias podem ser usadas em conjunto. Elas não são concorrentes, mas sim complementares — juntas proporcionam maior produtividade para o time. Obrigado pelo seu tempo, tamo junto e até a próxima