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.

Estou usando Hooks desde a versão Alpha, e também comecei a usar Redux Hooks em Alpha. AMBOS EM PRODUÇÃO, PORQUE AQUI É VIDA LOUCA.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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();
  }, []);
};

Agora sim eu tenho um hook que entrega algo mais semelhante a quem está acostumado com classes, porém sem o this. O useConnect faz um comportamento bem similar ao do connect mas não cria um componente wrapper para passar props da sua store. Show.

Tá, mas só isso?

Não...

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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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ó

Era isso que eu queria demonstrar hoje, da próxima vez que ver um artigo Usando hooks para matar o Redux, pense bem em como vc pode utilizar as duas tecnologias para os seu próprio bem e que elas não são concorrentes, e sim tecnologias complementares que juntas irão proporcionar uma produtividade maior a você e sua equipe.