> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/state-management.md).

# Gestão de Estado

Esta página cobre a criação de slices do Redux para gerenciamento de estado de UI e local usando o `createSlice` e `createEntityAdapter`.

## Quando usar Slices vs RTK Query

Escolha a ferramenta certa para seu estado:

| Tipo de Estado                                 | Ferramenta          | Exemplos                                                         |
| ---------------------------------------------- | ------------------- | ---------------------------------------------------------------- |
| **Dados remotos** (de propriedade do servidor) | RTK Query           | Perfis de usuários, NFTs, itens de catálogo, pedidos             |
| **Estado de UI** (de propriedade do cliente)   | createSlice         | Filtros, modais, preferências de exibição, estado de formulários |
| **Coleções normalizadas**                      | createEntityAdapter | Listas ordenadas/filtradas, atualizações otimistas               |

{% hint style="warning" %}
**Não duplique os mesmos dados** em um slice e no RTK Query. Escolha uma única fonte da verdade.
{% endhint %}

## Criando um Slice Básico

Use `createSlice` para estado simples de UI:

```tsx
// src/features/ui/ui.slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '@/app/store';

interface UIState {
  sidebarOpen: boolean;
  modalOpen: boolean;
  viewMode: 'grid' | 'list';
  theme: 'light' | 'dark';
}

const initialState: UIState = {
  sidebarOpen: true,
  modalOpen: false,
  viewMode: 'grid',
  theme: 'light',
};

const uiSlice = createSlice({
  name: 'ui',
  initialState,
  reducers: {
    // Alternadores booleanos
    sidebarToggled(state) {
      state.sidebarOpen = !state.sidebarOpen;
    },
    
    // Definir valor específico
    modalOpened(state) {
      state.modalOpen = true;
    },
    
    modalClosed(state) {
      state.modalOpen = false;
    },
    
    // Ações com payload
    viewModeChanged(state, action: PayloadAction<'grid' | 'list'>) {
      state.viewMode = action.payload;
    },
    
    themeChanged(state, action: PayloadAction<'light' | 'dark'>) {
      state.theme = action.payload;
    },
    
    // Várias propriedades
    uiReset() {
      return initialState;
    },
  },
});

// Exportar ações
export const {
  sidebarToggled,
  modalOpened,
  modalClosed,
  viewModeChanged,
  themeChanged,
  uiReset,
} = uiSlice.actions;

// Exportar reducer
export default uiSlice.reducer;

// Exportar seletores
export const selectSidebarOpen = (state: RootState) => state.ui.sidebarOpen;
export const selectModalOpen = (state: RootState) => state.ui.modalOpen;
export const selectViewMode = (state: RootState) => state.ui.viewMode;
export const selectTheme = (state: RootState) => state.ui.theme;
```

## Usando Entity Adapters

Para coleções normalizadas (listas com IDs), use `createEntityAdapter`:

```tsx
// src/features/credits/credits.slice.ts
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '@/app/store';

export type CreditTransaction = {
  id: string;
  address: string;
  amount: number;
  type: 'grant' | 'spend';
  timestamp: number;
  description?: string;
};

// Criar entity adapter
const txAdapter = createEntityAdapter<CreditTransaction>({
  selectId: (tx) => tx.id,
  sortComparer: (a, b) => b.timestamp - a.timestamp, // Mais recentes primeiro
});

// Criar slice com o estado inicial do adapter
const creditsSlice = createSlice({
  name: 'credits',
  initialState: txAdapter.getInitialState({
    sending: false,
    error: null as string | null,
  }),
  reducers: {
    // Adicionar uma única transação
    txAdded: txAdapter.addOne,
    
    // Adicionar múltiplas transações
    txsAdded: txAdapter.addMany,
    
    // Atualizar uma transação
    txUpdated: txAdapter.updateOne,
    
    // Remover uma transação
    txRemoved: txAdapter.removeOne,
    
    // Limpar todas as transações
    txsCleared: txAdapter.removeAll,
    
    // Reducer customizado com estado extra
    sendingStarted(state) {
      state.sending = true;
      state.error = null;
    },
    
    sendingSucceeded(state, action: PayloadAction<CreditTransaction>) {
      state.sending = false;
      txAdapter.addOne(state, action.payload);
    },
    
    sendingFailed(state, action: PayloadAction<string>) {
      state.sending = false;
      state.error = action.payload;
    },
  },
});

// Exportar ações
export const {
  txAdded,
  txsAdded,
  txUpdated,
  txRemoved,
  txsCleared,
  sendingStarted,
  sendingSucceeded,
  sendingFailed,
} = creditsSlice.actions;

// Exportar reducer
export default creditsSlice.reducer;

// Criar seletores
const selectCreditsState = (state: RootState) => state.credits;

export const creditsSelectors = txAdapter.getSelectors(selectCreditsState);

// Seletores customizados adicionais
export const selectIsSending = (state: RootState) => state.credits.sending;
export const selectError = (state: RootState) => state.credits.error;

// Seletores memoizados
export const selectTotalCredits = (state: RootState) => {
  const txs = creditsSelectors.selectAll(state);
  return txs.reduce((total, tx) => {
    return total + (tx.type === 'grant' ? tx.amount : -tx.amount);
  }, 0);
};
```

## Métodos do Entity Adapter

### Mutações de Estado

```tsx
// Adicionar
txAdapter.addOne(state, entity)
txAdapter.addMany(state, entities)

// Atualizar
txAdapter.updateOne(state, { id, changes })
txAdapter.updateMany(state, updates)

// Upsert (adicionar ou atualizar)
txAdapter.upsertOne(state, entity)
txAdapter.upsertMany(state, entities)

// Remover
txAdapter.removeOne(state, id)
txAdapter.removeMany(state, ids)
txAdapter.removeAll(state)

// Set (substituir tudo)
txAdapter.setAll(state, entities)
txAdapter.setOne(state, entity)
txAdapter.setMany(state, entities)
```

### Seletores Gerados

```tsx
const selectors = txAdapter.getSelectors(selectState);

// Selecionar todas as entidades como array
selectors.selectAll(state)

// Selecionar entidades como { [id]: entity }
selectors.selectEntities(state)

// Selecionar todos os IDs como array
selectors.selectIds(state)

// Selecionar contagem total
selectors.selectTotal(state)

// Selecionar entidade única por ID
selectors.selectById(state, id)
```

## Exemplo de Estado Complexo

Combinando múltiplas preocupações em um único slice:

```tsx
// src/features/land/land.slice.ts
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '@/app/store';

export type LandFilter = {
  owner?: string;
  minPrice?: number;
  maxPrice?: number;
  types?: ('parcel' | 'estate')[];
};

export type SelectedParcel = {
  x: number;
  y: number;
  id?: string;
};

const selectedParcelsAdapter = createEntityAdapter<SelectedParcel>({
  selectId: (p) => `${p.x},${p.y}`,
});

interface LandState {
  // Estado de visualização
  mapCenter: { x: number; y: number };
  mapZoom: number;
  
  // Estado de filtros
  filters: LandFilter;
  
  // Estado de seleção (usando adapter)
  selectedParcels: ReturnType<typeof selectedParcelsAdapter.getInitialState>;
  
  // Estado de UI
  showGrid: boolean;
  highlightOwned: boolean;
}

const initialState: LandState = {
  mapCenter: { x: 0, y: 0 },
  mapZoom: 1,
  filters: {},
  selectedParcels: selectedParcelsAdapter.getInitialState(),
  showGrid: true,
  highlightOwned: false,
};

const landSlice = createSlice({
  name: 'land',
  initialState,
  reducers: {
    // Controles do mapa
    mapCenterChanged(state, action: PayloadAction<{ x: number; y: number }>) {
      state.mapCenter = action.payload;
    },
    
    mapZoomed(state, action: PayloadAction<number>) {
      state.mapZoom = action.payload;
    },
    
    // Filtros
    filtersUpdated(state, action: PayloadAction<Partial<LandFilter>>) {
      state.filters = { ...state.filters, ...action.payload };
    },
    
    filtersCleared(state) {
      state.filters = {};
    },
    
    // Seleção
    parcelSelected(state, action: PayloadAction<SelectedParcel>) {
      selectedParcelsAdapter.addOne(state.selectedParcels, action.payload);
    },
    
    parcelDeselected(state, action: PayloadAction<string>) {
      selectedParcelsAdapter.removeOne(state.selectedParcels, action.payload);
    },
    
    selectionCleared(state) {
      selectedParcelsAdapter.removeAll(state.selectedParcels);
    },
    
    // Alternadores de UI
    gridToggled(state) {
      state.showGrid = !state.showGrid;
    },
    
    ownedHighlightToggled(state) {
      state.highlightOwned = !state.highlightOwned;
    },
  },
});

export const {
  mapCenterChanged,
  mapZoomed,
  filtersUpdated,
  filtersCleared,
  parcelSelected,
  parcelDeselected,
  selectionCleared,
  gridToggled,
  ownedHighlightToggled,
} = landSlice.actions;

export default landSlice.reducer;

// Seletores
export const selectMapCenter = (state: RootState) => state.land.mapCenter;
export const selectMapZoom = (state: RootState) => state.land.mapZoom;
export const selectFilters = (state: RootState) => state.land.filters;
export const selectShowGrid = (state: RootState) => state.land.showGrid;
export const selectHighlightOwned = (state: RootState) => state.land.highlightOwned;

// Seletores de seleção
const selectSelectedParcelsState = (state: RootState) => state.land.selectedParcels;
export const selectedParcelsSelectors = selectedParcelsAdapter.getSelectors(
  selectSelectedParcelsState
);
```

## Seletores Memoizados

Use `createSelector` do Reselect para estado computado/derivado:

```tsx
// src/features/land/land.selectors.ts
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '@/app/store';
import { selectFilters } from './land.slice';

// Lógica de filtragem custosa - memoizada
export const selectActiveFiltersCount = createSelector(
  [selectFilters],
  (filters) => {
    return Object.values(filters).filter(Boolean).length;
  }
);

// Combinar múltiplos seletores
export const selectHasActiveFilters = createSelector(
  [selectActiveFiltersCount],
  (count) => count > 0
);

// Múltiplas entradas
export const selectFilteredParcels = createSelector(
  [
    (state: RootState) => state.land.allParcels, // assumindo que isto exista
    selectFilters,
  ],
  (parcels, filters) => {
    return parcels.filter((parcel) => {
      if (filters.owner && parcel.owner !== filters.owner) return false;
      if (filters.minPrice && parcel.price < filters.minPrice) return false;
      if (filters.maxPrice && parcel.price > filters.maxPrice) return false;
      if (filters.types && !filters.types.includes(parcel.type)) return false;
      return true;
    });
  }
);
```

## Lógica Assíncrona com Extra Reducers

Trate respostas do RTK Query ou thunks assíncronos no seu slice:

```tsx
import { createSlice } from '@reduxjs/toolkit';
import { creditsClient } from './credits.client';

const slice = createSlice({
  name: 'credits',
  initialState: { lastGranted: null as number | null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addMatcher(
        creditsClient.endpoints.grantCredits.matchFulfilled,
        (state, action) => {
          state.lastGranted = action.payload.newBalance;
        }
      )
      .addMatcher(
        creditsClient.endpoints.grantCredits.matchRejected,
        (state) => {
          state.lastGranted = null;
        }
      );
  },
});
```

## Boas Práticas

### 1. Mantenha o Estado Mínimo

```tsx
// ✅ Bom: Armazenar apenas o que você precisa
interface State {
  userId: string | null;
  isAuthenticated: boolean;
}

// ❌ Ruim: Armazenar valores derivados/computados
interface State {
  userId: string | null;
  isAuthenticated: boolean;
  hasUserId: boolean; // Pode ser computado
  userIdLength: number; // Pode ser computado
}
```

### 2. Use Mutações Compatíveis com Immer

```tsx
// ✅ Bom: Mutação direta (Immer lida com isso)
reducers: {
  itemAdded(state, action) {
    state.items.push(action.payload);
    state.count += 1;
  }
}

// ❌ Ruim: Spread manual (desnecessário)
reducers: {
  itemAdded(state, action) {
    return {
      ...state,
      items: [...state.items, action.payload],
      count: state.count + 1,
    };
  }
}
```

### 3. Organize Reducers Logicamente

```tsx
// ✅ Bom: Agrupados por funcionalidade
reducers: {
  // Controles de modal
  modalOpened(state) { ... },
  modalClosed(state) { ... },
  
  // Controles de filtro
  filterApplied(state, action) { ... },
  filterCleared(state) { ... },
  
  // Reset
  stateReset() { return initialState; },
}
```

### 4. Tipar Ações Apropriadamente

```tsx
// ✅ Bom: Tipo de payload explícito
userUpdated(state, action: PayloadAction<{ id: string; name: string }>) {
  state.user = action.payload;
}

// ❌ Ruim: Payload sem tipo
userUpdated(state, action) {
  state.user = action.payload; // Sem segurança de tipo
}
```

## Testando Slices

```tsx
// land.slice.test.ts
import reducer, { mapCenterChanged, mapZoomed } from './land.slice';

describe('land slice', () => {
  const initialState = {
    mapCenter: { x: 0, y: 0 },
    mapZoom: 1,
    // ... outro estado
  };

  it('should handle mapCenterChanged', () => {
    const newCenter = { x: 10, y: 20 };
    const actual = reducer(initialState, mapCenterChanged(newCenter));
    expect(actual.mapCenter).toEqual(newCenter);
  });

  it('should handle mapZoomed', () => {
    const actual = reducer(initialState, mapZoomed(2));
    expect(actual.mapZoom).toBe(2);
  });
});
```

## Próximos Passos

* Rever [Padrões de Componentes](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/component-patterns.md) para usar slices em componentes
* Aprenda sobre [Integração Web3](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/web3-integration.md) para estado de blockchain
* Veja [Testes & Performance](/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/testing-and-performance.md) para dicas de otimização


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/contributor/contributor-pt/guias-do-contribuidor/padroes-de-ui/state-management.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
