Drawing Right Side
By Emmanuel (Last update November 26, 2022)

February 01, 2022Edit this file (Private, ask me for access if you wanna contribute)

Botando React Redux de joelho no milho

Você ai já teve aquela fase em que ouvia a palavra 'redux' e sentava em posição fetal chorando? Então esse post é para você. Bora simplificar o estado global e implementar um gg 10/10 izi

image

Primeiros passos

Vou assumir que você já tem um ambiente configurado. Caso não tenha, em breve rolará um post por aqui pra te ajudar com isso.

Bora começar criando um projeto com o Create React App.

npx create-react-app global-state-izipizi

Quando finalizar a criação, a gente entra na pasta e já podemos fazer uma limpezinha para eliminar alguns arquivos que não vamos utilizar e começar a instalar as dependências

cd global-state-izipizi

Deletei todos os arquivos que estavam soltos em src/ menos App.js e index.js, e removi as importações nos referidos arquivos.

9547c4c3-eefb-46ad-8412-1c8379e40dd8..png

Pro nosso projetinho vamos precisar apenas de algumas dependências

npm install react-redux redux redux-persist redux-saga --save

Configurando

Em /src vamos criar uma pasta /store onde irá ficar todo o core do estado da nossa aplicação. Vamos começar criando uma pasta dentro de store para separar nosso state. Nesse post, irei fazer um estado para armazenar cursos. Nós teremos uma estrutura como:

src
  /store
    /courses

Dentro da pasta courses iremos criar 3 arquivos.

  1. courses.actions.js

  2. courses.utils.js

  3. courses.reducer.js

    Você pode criar mais arquivos para armazenar os tipos, os seletores etc.

Cada dado da store deve ter o seu próprio reducer e este é encarregado de lidar com todas as ações relacionadas a esse estado e ditam como esse estado irá mudar de acordo com a ação enviada para o store

Dito isso, vamos começar a escrever nosso reducer para courses.

const INITIAL_STATE = {
  courses: [],
};

function CoursesReducer(state = INITIAL_STATE, action) {
  switch (action.type) {
    case "LOAD_COURSES":
      return {
        ...state,
        courses: action.payload.data,
      };
    case "ADD_COURSE":
      return {
        ...state,
        courses: action.payload
      };
    case "REMOVE_COURSE":
      return {
        ...state,
        courses: action.payload
      };
    default:
      return state;
  }
}

export default CoursesReducer;

Já deixei 3 ações definidas como sendo LOAD_COURSES, ADD_COURSE e REMOVE_COURSE. Para uma maior flexibilidade, iremos criar funções utils para realizar a manipulação do estado.

No arquivo courses.utils.js:

import { v4 as uuidv4 } from "uuid";

export function addCourse(state, courseItem) {
  return [...state?.courses, { id: uuidv4(), ...courseItem }];
}

export function removeCourse(state, id) {
  return state?.courses.filter((course) => course.id !== id);
}

Criei duas funções que recebem o estado atual dos cursos e um payload, a partir disso retornam um novo estado. Vamos chamar essa função no nosso reducer.

import { addCourse, removeCourse } from "./courses.utils.js"

const INITIAL_STATE = {
  courses: [],
};

function CoursesReducer(state = INITIAL_STATE, action) {
  switch (action.type) {
    case "LOAD_COURSES":
      return {
        ...state,
        courses: ,
      };
    case "ADD_COURSE":
      return {
        ...state,
        courses:  addCourse(state, action.payload)
      };
    case "REMOVE_COURSE":
      return {
        ...state,
        courses: removeCourse(state, action.payload)
      };
    default:
      return state;
  }
}

export default CoursesReducer;

Supimpa irmão, estamos quase lá. Vamos criar nossas actions

No arquivo courses.actions.js:

export function addCourseAction(course) {
  return { type: "ADD_COURSE", payload: course };
}

export function removeCourseAction(id) {
  return { type: "REMOVE_COURSE", payload: id };
}

Deveras brutal não é mesmo?

Com nosso primeiro reducer criado, nós precisamos criar nosso store.

Na pasta store iremos criar um arquivo chamado root-reducer.js, ele será o responsável por reunir todos os reducers da aplicação para formar um único estado. E já vamos persistir os dados dos cursos com o redux-persist.

import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";

import CoursesReducer from "./courses/courses.reducer";

const persistConfig = {
  key: "@root-courses",
  storage,
  whitelist: ["courses"],
};

const rootReducer = combineReducers({
  courses: CoursesReducer,
});

export default persistReducer(persistConfig, rootReducer);

Estamos configurando o persistConfig utilizando o localstorage como banco para persistir os dados. Na configuração em persistConfig iremos passar a key do estado que iremos armazenar, passamos qual o tipo de storage (que optamos por localStorage) e uma whitelist ou blacklist, que nada mais são que um array com todos os nomes dos reducers que queremos persistir (whitelist) ou que não queremos persistir (blacklist).

Bora criar agora nosso arquivo principal. Na pasta store vamos criar um index.js

import { createStore, applyMiddleware } from "redux";
import createMiddleware from "redux-saga";
import { persistStore } from "redux-persist";

import rootReducer from "./root-reducer";

export const store = createStore(rootReducer);

export const persistor = persistStore(store);

Nosso store já está pegando todos os reducers que passaram pelo rootReducer, formando um grande objeto de estado. Maneiro né?

Bora fazer funcionar agora.

Vamos voltar para a pasta src e no arquivo index.js onde fazemos a injeção do App.js na DOM:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "./store";

ReactDOM.render(
  <Provider store={store}>
    <PersistGate persistor={persistor} loading={null}>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </PersistGate>
  </Provider>,
  document.getElementById("root")
);

Cabô dev, mamão com açúcar. Você já tem acesso ao seu store por toda a aplicação.

De brinde, bora aplicar um middleware para permitir os dispatchs de forma assíncrona no estado. Voltando ao arquivo src/store/index.js vamos utilizar o redux-saga.

import { createStore, applyMiddleware } from "redux";
import createMiddleware from "redux-saga";
import { persistStore } from "redux-persist";

import rootReducer from "./root-reducer";
import rootSagas from "./sagas";

export const sagaMiddleware = createMiddleware();
export const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(rootSagas);

export const persistor = persistStore(store);

Middlware configurado, agora todos os dispatchs irão passar pelo saga antes de irem ao reducer.

Agora vamos criar o arquivo rootSagas:

import { all, takeLatest, put, call } from "redux-saga/effects";
import { v4 as uuidv4 } from "uuid";

async function fetchCourses() {
  const res = await fetch("http://your-api/v2/courses");
  const toJson = await res.json();

  const data = toJson.slice(0, 10);
  const formated = data.map((item) => {
    return {
      id: uuidv4(),
      ...item,
    };
  });

  return formated;
}

function* asyncGetCourses() {
  const res = yield call(fetchCourses);

  yield put({
    type: "LOAD_COURSES",
    payload: {
      data: res,
    },
  });
}

export default function* root() {
  yield all([takeLatest("ASYNC_LOAD_COURSES", asyncGetCourses)]);
}

Aqui nós estamos criando uma ação do tipo "ASYNC_LOAD_COURSES" que não é ouvida por nosso reducer de courses mas será ouvida pelo saga e ela fará o dispatch.

Exemplo

Irei deixar um repositório pessoal onde fiz toda configuração apresentada e que apresenta uma interface com um exemplo simples. Você pode conferir aqui Redux-Study

Indicação do post

Goodnight Julia.

See you space cowboy!

See also

Files in the same folder:

Anonymous message

The form above is anonymous, although the following data is collected:

  • Current page URL.
  • Your message.
  • Your username if provided.
  • Don't forget to include contact info if you are asking something, otherwise I'll never be able to reach you back!

If you prefer, you can reach me directly on GitHub or E-mail.

This form is powered by Discord Webhook API, so it is totally hackable, enjoy.