Padroes de Gerenciamento de Estado no Frontend - Filosofias de Design do Redux, Zustand, Jotai e Recoil

Avancado | 2025.12.02

A Necessidade do Gerenciamento de Estado

A medida que as aplicacoes frontend se tornam mais complexas, o compartilhamento de estado entre componentes se torna um desafio. A escolha do padrao adequado de gerenciamento de estado impacta diretamente a manutenibilidade e escalabilidade da aplicacao.

flowchart TB
    subgraph PropDrilling["Prop Drilling (Inferno de Passagem de Props)"]
        App["App<br/>state: { user, theme, cart }"]
        Layout["Layout<br/>← props: user, theme"]
        Header["Header<br/>← props: user, theme"]
        Avatar["Avatar<br/>← props: user (finalmente usado)"]

        App --> Layout --> Header --> Avatar
    end

    Note["Problema: Componentes intermediarios apenas repassam props desnecessarias"]

Classificacao das Arquiteturas de Gerenciamento de Estado

flowchart TB
    subgraph Patterns["Classificacao dos Padroes de Gerenciamento de Estado"]
        subgraph Flux["1. Padrao Flux/Redux"]
            F1["Store Unica + Action + Reducer<br/>Redux, Zustand"]
        end

        subgraph Atomic["2. Padrao Atomic"]
            A1["Unidades de estado pequenas e distribuidas (Atom)<br/>Jotai, Recoil"]
        end

        subgraph Proxy["3. Padrao Proxy-based"]
            P1["Rastreamento automatico via Proxy<br/>Valtio, MobX"]
        end

        subgraph Signal["4. Padrao Signal"]
            S1["Reatividade de granularidade fina<br/>Solid.js Signals, Preact Signals"]
        end
    end

Padrao Flux/Redux

Conceitos Basicos

flowchart LR
    View["View"] -->|Action| Dispatcher["Dispatcher"]
    Dispatcher -->|dispatch| Store["Store<br/>(Reducer)"]
    Store -->|notify| State["State<br/>(somente leitura)"]
    State --> View

Principios:

  • Fluxo de dados unidirecional
  • Estado e somente leitura (Imutavel)
  • Mudancas apenas atraves de Actions
  • Reducers sao funcoes puras

Implementacao Redux

// store/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserState {
  currentUser: User | null;
  isLoading: boolean;
  error: string | null;
}

const initialState: UserState = {
  currentUser: null,
  isLoading: false,
  error: null,
};

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    loginStart: (state) => {
      state.isLoading = true;
      state.error = null;
    },
    loginSuccess: (state, action: PayloadAction<User>) => {
      state.currentUser = action.payload;
      state.isLoading = false;
    },
    loginFailure: (state, action: PayloadAction<string>) => {
      state.error = action.payload;
      state.isLoading = false;
    },
    logout: (state) => {
      state.currentUser = null;
    },
  },
});

export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;
export default userSlice.reducer;

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// hooks/useAppDispatch.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from '../store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// components/UserProfile.tsx
function UserProfile() {
  const { currentUser, isLoading, error } = useAppSelector((state) => state.user);
  const dispatch = useAppDispatch();

  const handleLogout = () => {
    dispatch(logout());
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!currentUser) return <div>Please login</div>;

  return (
    <div>
      <h1>{currentUser.name}</h1>
      <button onClick={handleLogout}>Logout</button>
    </div>
  );
}

Implementacao Zustand

// store/useStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserStore {
  // State
  currentUser: User | null;
  isLoading: boolean;
  error: string | null;

  // Actions
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  updateProfile: (updates: Partial<User>) => void;
}

export const useUserStore = create<UserStore>()(
  devtools(
    persist(
      immer((set, get) => ({
        currentUser: null,
        isLoading: false,
        error: null,

        login: async (email, password) => {
          set((state) => {
            state.isLoading = true;
            state.error = null;
          });

          try {
            const response = await fetch('/api/auth/login', {
              method: 'POST',
              body: JSON.stringify({ email, password }),
            });
            const user = await response.json();

            set((state) => {
              state.currentUser = user;
              state.isLoading = false;
            });
          } catch (error) {
            set((state) => {
              state.error = error.message;
              state.isLoading = false;
            });
          }
        },

        logout: () => {
          set((state) => {
            state.currentUser = null;
          });
        },

        updateProfile: (updates) => {
          set((state) => {
            if (state.currentUser) {
              Object.assign(state.currentUser, updates);
            }
          });
        },
      })),
      { name: 'user-storage' }
    ),
    { name: 'UserStore' }
  )
);

// Selector (Otimizacao de Performance)
export const useCurrentUser = () => useUserStore((state) => state.currentUser);
export const useIsLoading = () => useUserStore((state) => state.isLoading);

// components/UserProfile.tsx
function UserProfile() {
  const currentUser = useCurrentUser();
  const logout = useUserStore((state) => state.logout);

  if (!currentUser) return null;

  return (
    <div>
      <h1>{currentUser.name}</h1>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Padrao Atomic

Implementacao Jotai

// atoms/userAtoms.ts
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

interface User {
  id: string;
  name: string;
  email: string;
}

// Atom basico
export const currentUserAtom = atom<User | null>(null);

// Atom persistido
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');

// Atom derivado (somente leitura)
export const isLoggedInAtom = atom((get) => get(currentUserAtom) !== null);

// Atom derivado (leitura e escrita)
export const userNameAtom = atom(
  (get) => get(currentUserAtom)?.name ?? 'Guest',
  (get, set, newName: string) => {
    const user = get(currentUserAtom);
    if (user) {
      set(currentUserAtom, { ...user, name: newName });
    }
  }
);

// Atom assincrono
export const userProfileAtom = atom(async (get) => {
  const user = get(currentUserAtom);
  if (!user) return null;

  const response = await fetch(`/api/users/${user.id}/profile`);
  return response.json();
});

// Atom de acao
export const loginAtom = atom(
  null,
  async (get, set, { email, password }: { email: string; password: string }) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    });
    const user = await response.json();
    set(currentUserAtom, user);
  }
);

export const logoutAtom = atom(null, (get, set) => {
  set(currentUserAtom, null);
});

// components/UserProfile.tsx
import { useAtom, useAtomValue, useSetAtom } from 'jotai';

function UserProfile() {
  const currentUser = useAtomValue(currentUserAtom);
  const [userName, setUserName] = useAtom(userNameAtom);
  const logout = useSetAtom(logoutAtom);

  if (!currentUser) return null;

  return (
    <div>
      <input
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Implementacao Recoil

// atoms/userState.ts
import { atom, selector, selectorFamily } from 'recoil';

interface User {
  id: string;
  name: string;
  email: string;
}

// Atom
export const currentUserState = atom<User | null>({
  key: 'currentUser',
  default: null,
});

// Selector (estado derivado)
export const isLoggedInState = selector({
  key: 'isLoggedIn',
  get: ({ get }) => get(currentUserState) !== null,
});

// Selector assincrono
export const userProfileState = selector({
  key: 'userProfile',
  get: async ({ get }) => {
    const user = get(currentUserState);
    if (!user) return null;

    const response = await fetch(`/api/users/${user.id}/profile`);
    return response.json();
  },
});

// Selector com parametros (SelectorFamily)
export const userPostsState = selectorFamily({
  key: 'userPosts',
  get: (userId: string) => async () => {
    const response = await fetch(`/api/users/${userId}/posts`);
    return response.json();
  },
});

// components/UserProfile.tsx
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil';

function UserProfile() {
  const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
  const isLoggedIn = useRecoilValue(isLoggedInState);
  const profile = useRecoilValue(userProfileState);

  const logout = () => setCurrentUser(null);

  if (!isLoggedIn) return <div>Please login</div>;

  return (
    <div>
      <h1>{currentUser?.name}</h1>
      <p>{profile?.bio}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}
flowchart TB
    subgraph FluxPattern["Flux (Redux/Zustand)"]
        Store["Single Store<br/>user | cart | ui | ..."]
        Store --> CA["Component A"]
        Store --> CB["Component B"]
        Store --> CC["Component C"]
    end

    subgraph AtomicPattern["Atomic (Jotai/Recoil)"]
        A1["Atom"] --> CompA["Component A"]
        A2["Atom"] --> Derived["Derived Atom"]
        A3["Atom"] --> Derived
        Derived --> CompB["Component B"]
        A4["Atom"] --> CompC["Component C"]
    end

Caracteristicas:

  • Flux: Centralizado, previsivel, facil de depurar
  • Atomic: Distribuido, granularidade fina, adequado para code splitting

Padrao Proxy-based

Implementacao Valtio

// store/userStore.ts
import { proxy, useSnapshot } from 'valtio';
import { devtools } from 'valtio/utils';

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserStore {
  currentUser: User | null;
  isLoading: boolean;
  error: string | null;
}

export const userStore = proxy<UserStore>({
  currentUser: null,
  isLoading: false,
  error: null,
});

// Habilitar DevTools
devtools(userStore, { name: 'userStore' });

// Acoes (modificacao direta possivel)
export const userActions = {
  async login(email: string, password: string) {
    userStore.isLoading = true;
    userStore.error = null;

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      });
      userStore.currentUser = await response.json();
    } catch (error) {
      userStore.error = error.message;
    } finally {
      userStore.isLoading = false;
    }
  },

  logout() {
    userStore.currentUser = null;
  },

  updateName(name: string) {
    if (userStore.currentUser) {
      userStore.currentUser.name = name;  // Modificacao direta possivel
    }
  },
};

// components/UserProfile.tsx
function UserProfile() {
  // Obter snapshot somente leitura com useSnapshot
  const snap = useSnapshot(userStore);

  if (snap.isLoading) return <div>Loading...</div>;
  if (!snap.currentUser) return null;

  return (
    <div>
      <input
        value={snap.currentUser.name}
        onChange={(e) => userActions.updateName(e.target.value)}
      />
      <button onClick={userActions.logout}>Logout</button>
    </div>
  );
}

Comparacao de Bibliotecas de Gerenciamento de Estado

CaracteristicaRedux ToolkitZustandJotaiRecoilValtio
Tamanho do BundleGrandePequenoPequenoMedioPequeno
Curva de AprendizadoAltaBaixaBaixaMediaBaixa
BoilerplateMuitoPoucoPoucoMedioPouco
DevToolsCompletoDisponivelDisponivelDisponivelDisponivel
TypeScriptBomExcelenteExcelenteBomExcelente
Suporte SSRBomBomExcelenteComplexoBom
Uso fora do ReactPossivelPossivelNaoNaoPossivel

Criterios de Escolha

Guia de Selecao de Biblioteca de Gerenciamento de Estado

Escala do Projeto/Equipe:

EscalaRecomendacaoMotivo
Grande/Muitas pessoasRedux ToolkitConvencoes claras, ecossistema rico
Media/Poucas pessoasZustandSimples, baixa curva de aprendizado
Pequena/PrototipoJotai/ValtioConfiguracao minima

Requisitos de Arquitetura:

RequisitoRecomendacao
PrevisibilidadeRedux/Zustand
Atualizacoes granularesJotai/Recoil
Operacoes mutaveisValtio
Server ComponentsJotai (ideal)

Melhores Praticas

// 1. Normalizacao do estado
interface NormalizedState {
  users: {
    byId: Record<string, User>;
    allIds: string[];
  };
  posts: {
    byId: Record<string, Post>;
    allIds: string[];
  };
}

// 2. Uso de estado derivado
const selectUserPosts = (state: RootState, userId: string) =>
  state.posts.allIds
    .map(id => state.posts.byId[id])
    .filter(post => post.authorId === userId);

// 3. Separacao de processamento assincrono
// Estado do servidor com TanStack Query / SWR
// Estado do cliente com Zustand / Jotai

// 4. Divisao em granularidade adequada
// ERRADO: Store unica gigante
// CERTO: Stores divididas por funcionalidade
← Voltar para a lista