Executando verificação de segurança...
7

React: Utilizando Ducks em Context API (Duck Pattern)

Pato gigante

Olá pessoal, esse é o meu primeiro post por aqui, espero que gostem!

Existem inúmeros gerenciadores de estado para o React hoje em dia, Redux, Pullstate, Recoil, MobX, entre muitos outros. Porém o que mais se questiona atualmente é quando devemos de fato utilizar uma dessas bibliotecas.
Sendo o mais popular e comumente utilizado com React ou Angular, o Redux é baseado na Flux Architecture e traz uma estrutura de gerenciamento de estado mais robusta, podendo inclusive integrar com Middlewares para disparar side effects como "consequência" de ações.


Mais porque estamos falando de Redux afinal de contas?

Assim como tudo hoje em dia no desenvolvimento, existem trade-offs em usar estruturas como Redux em seus projetos, por isso surgiram design patterns que ajudam na escalabilidade dessas estruturas.

Um dos mais famosos usados com Redux é o Duck Pattern, que traz uma estrutura de Ducks acumulando Reducers e Actions em um único arquivo em cada Store, além de otimizar a criação dessas Actions e Reducers.
Tentando aplicar esse conceito e inspirado nesse tipo de arquitetura, criei um simples packege no npm que gera actions e reducers para o Context API, tentando ganhar produtividade com projetos pequenos que usam somente o Context do React.

Para começar, vamos instalar o pacote context-spices:

npm install context-spices

ou com Yarn:

yarn add context-spices

Caso tenha dúvidas de como o pacote é composto e/ou sobre sua documentação, acessem o github.com/dhanielsales/context-spices.

E agora, o que fazemos com isso?

Agora fica muito simples para criarmos nossas actions para o context. Abaixo colocarei um exemplo pratico de uso.

Criaremos uma estrutura de pastas para os nossos contexts, eu utilizei o modelo abaixo, mas fique a vontade para fazer como preferir:

├── components
├── pages
├── stores
...
├── contexts
│   └── ButtonsContext
        └── ducks.ts
        └── index.tsx

Dentro da pasta do seu context, deve ser criado dois arquivos, ducks.ts e index.tsx.

No arquivo index.tsx vamos importar algumas coisas necessárias do React, mas também o o type Action:

import React, {
  createContext,
  useContext,
  useReducer
} from 'react'

import { Action } from 'context-spices'

Também no index.tsx vamos criar um objeto para o state do nosso context, fique livre para criá-lo como preferir. Nesse meu exemplo colocarei apenas um contador simples:

const INITIAL_DATA = {
  count: 0,
}
export type State = typeof INITIAL_DATA

Criaremos também uma interface para ser passada como type do nosso context, e então criaremos o nosso context de fato:

interface ContextProps {
  state: State;
  dispatch?: React.Dispatch<Action>;
}
const ButtonsContext = createContext<ContextProps>({
  state: INITIAL_DATA,
})

E finalmente, criaremos o nosso Provider juntamente com um Hook para usar o nosso context e no fim o arquivo ficará assim:

import React, {
  createContext,
  useContext,
  useReducer
} from 'react';
import { Action } from 'context-spices';

const INITIAL_DATA = {
  count: 0,
};

export type State = typeof INITIAL_DATA;

interface ContextProps {
  state: State;
  dispatch?: React.Dispatch<Action>;
};
 
const ButtonsContext = createContext<ContextProps>({
  state: INITIAL_DATA,
});

export const ButtonsProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(() => INITIAL_DATA, INITIAL_DATA);
  return <ButtonsContext.Provider value={{ state, dispatch }}>{children}</ButtonsContext.Provider>;
};

export const useButtons = () => useContext(ButtonsContext);

Na linha 24:40, está sendo passado como argumento uma arrow function temporariamente, ela retorna apenas o INITIAL_STATE pois ainda criaremos a nossa função que servirá como Dispatch.

Agora colocaremos algumas coisas no nosso ducks.ts , nele começaremos importando Action, createActions, createReducer do pacote e também o State do nosso index.tsx:

import { Action, createActions, createReducer} from 'context-spices'
import { State } from './index'

Em seguida, criaremos os types para nossas actions, começaremos com um enum com os nossos TypesNames, além disso criaremos para cada action, um type respectivo como descrito abaixo:

export enum TypesNames {
  INCREMENT = 'INCREMENT',
  DECREMENT = 'DECREMENT',
  SET_VALUE = 'SET_VALUE',
}
export type Increment = Action<TypesNames.INCREMENT>;
export type Decrement = Action<TypesNames.DECREMENT>;
export interface SetValue extends Action<TypesNames.SET_VALUE> {
  value: number;
}

Essa tipagem precisa ser extendida do type Action, que foi importada acima, e passar para o generic a opção do enum TypesNames que corresponde a ação. Além disso, no interior da interface das actions, coloque o que terá no payload da mesma, como no exempo SetValue. Caso sua action não tenha payload, basta deixar vazio.

Agora vamos criar nossas actions, para isso, basta criarmos como descrito abaixo:

export const Creators = createActions<{
  increment: () => Increment;
  decrement: () => Decrement;
  setValue: (value: number) => SetValue;
}>({
    increment: TypesNames.INCREMENT,
    decrement: TypesNames.DECREMENT,
    setValue: TypesNames.SET_VALUE,
  },
  {
    increment: null,
    decrement: null,
    setValue: ['value'],
  },
);

Na function createActions estamos criando três actions, é importante passarmos para o generic da função createActions uma interface com a tipagem de cada action, além disso passaremos dois argumentos, o primeiro sendo um objeto com as chaves iguais aos que colocamos no generic e o valor sendo as opções do TypesNames de cada uma das actions. Por ultimo, um objeto contendo também as chaves iguais aos que colocamos no generic e os valores sendo um Array de Strings e os valores dessas Strings precisam ser iguais aos argumentos definidos na função no generic.

Agora, precisamos criar nossos Reducers, criaremos um para cada uma das actions como abaixo:

const setValueReducer = (state: State, { value }: SetValue): State => {
  return {
    ...state,
    count: value,
  };
};
const incrementReducer = (state: State): State => {
  return {
    ...state,
    count: state.count + 1,
  };
};
const decrementReducer = (state: State): State => {
  return {
    ...state,
    count: state.count - 1,
  };
};

E em seguinda, finalizaremos com um objeto que ditará qual Action apontará para cada Reducer e criaremos o nosso Dispatcher com a createReducer, o arquivo ficará assim:

import { Action, createActions, createReducer } from 'context-spices';
import { State } from './index';

export enum TypesNames {
  INCREMENT = 'INCREMENT',
  DECREMENT = 'DECREMENT',
  SET_VALUE = 'SET_VALUE',
}

export type Increment = Action<TypesNames.INCREMENT>;

export type Decrement = Action<TypesNames.DECREMENT>;

export interface SetValue extends Action<TypesNames.SET_VALUE> {
  value: number;
}

export const Creators = createActions<{
  increment: () => Increment;
  decrement: () => Decrement;
  setValue: (value: number) => SetValue;
}>(
  {
    increment: TypesNames.INCREMENT,
    decrement: TypesNames.DECREMENT,
    setValue: TypesNames.SET_VALUE,
  },
  {
    increment: null,
    decrement: null,
    setValue: ['value'],
  },
);

const setValueReducer = (state: State, { value }: SetValue): State => {
  return {
    ...state,
    count: value,
  };
};

const incrementReducer = (state: State): State => {
  return {
    ...state,
    count: state.count + 1,
  };
};

const decrementReducer = (state: State): State => {
  return {
    ...state,
    count: state.count - 1,
  };
};

const reducerTypes = {
  [TypesNames.INCREMENT]: incrementReducer,
  [TypesNames.DECREMENT]: decrementReducer,
  [TypesNames.SET_VALUE]: setValueReducer,
};

export const ButtonsReducer = (state: State, action: Action) =>
  createReducer<State>(state, action, reducerTypes);

Antes de irmos para a finalização, precisamos voltar no arquivo de index.tsx e trocar a nossa função de dispatch que haviamos colocado temporáriamente, e importaremos o nosso Reducer criado a partir do createReducer:

...
import { ButtonsReducer } from './ducks.ts'

Na linha 24:40 trocaremos a () => INITIAL_DATA por ButtonsReducer, ficando como abaixo:

...
const [state, dispatch] = useReducer(ButtonsReducer, INITIAL_DATA);
...

E para finalizar nosso exemplo, basta você importar tudo em um component:

import React from 'react';

import { useButtons } from '~/contexts/ButtonsContext';
import { Creators } from '~/contexts/ButtonsContext/ducks';

const { setValue, increment, decrement } = Creators;

const Test: React.FC = () => {
  const { state, dispatch } = useButtons();

  return (
    <div>
      <div>{state.count}</div>
      <button onClick={() => dispatch(setValue(22))}>Definir contador para 22</button>
      <button onClick={() => dispatch(increment())}>Incrementar</button>
      <button onClick={() => dispatch(decrement())}>Decrementar</button>
    </div>
  );
};

export default Test;

Se surgirem dúvidas, deixarei um repositório abaixo com o exemplo completo, para que possa conseguir mais referências :)

Github Exemplo Completo — ducks-with-context

Demo — context-spices


Espero que lhe tenha sido útil. Se você tiver alguma dúvida ou tem alguma ideia bacana, adicione a dica como um comentário.

Se você gostou deste artigo, deixe um like ou um comentário.

Você pode me seguir no LinkedIn.

Referências

Carregando publicação patrocinada...