# RTK Query

RTK Query é a poderosa camada de busca e cache de dados do Redux Toolkit. Elimina a necessidade de escrever action creators assíncronos e gerencia estados de carregamento, cache e sincronização de dados automaticamente.

## Configuração básica do Client

Todos os endpoints do RTK Query estendem a partir de uma única instância base do client.

### Configuração do Base Query

```tsx
// src/services/baseQuery.ts
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { RootState } from '@/app/store';

export const baseQuery = fetchBaseQuery({
  baseUrl: process.env.NEXT_PUBLIC_API_URL, // ex., https://api.decentraland.org
  
  prepareHeaders: (headers, { getState }) => {
    const state = getState() as RootState;
    
    // Adicionar token de autenticação
    const token = state.user.session?.authToken;
    if (token) {
      headers.set('authorization', `Bearer ${token}`);
    }
    
    // Adicionar chain ID para contexto web3
    const chainId = state.user.chainId;
    if (chainId) {
      headers.set('x-chain-id', String(chainId));
    }
    
    // Headers padrão
    headers.set('accept', 'application/json');
    headers.set('content-type', 'application/json');
    
    return headers;
  },
  
  credentials: 'omit', // ou 'include' se o backend exigir cookies
});
```

### Instância do Client

```tsx
// src/services/client.ts
import { createApi } from '@reduxjs/toolkit/query/react';
import { baseQuery } from './baseQuery';

export const client = createApi({
  reducerPath: 'client',
  baseQuery,
  
  // Defina todos os tipos de tag possíveis para invalidação de cache
  tagTypes: [
    'User',
    'Profile',
    'Parcels',
    'Estates',
    'Credits',
    'Orders',
    'Sales',
    'NFTs',
  ],
  
  // Configuração de cache
  keepUnusedDataFor: 60,         // Manter dados não usados por 60 segundos
  refetchOnFocus: true,           // Rebuscar quando a janela voltar a ter foco
  refetchOnReconnect: true,       // Rebuscar ao reconectar
  refetchOnMountOrArgChange: 30,  // Rebuscar se os dados tiverem mais de 30s
  
  // Endpoints serão injetados em arquivos de feature
  endpoints: () => ({}),
});
```

## Convenções de Tags

Tags são usadas para invalidação de cache e sincronização. Siga estas convenções:

### Nomeação de Tag

| Tipo de Recurso     | Tags de Query                        | Mutação Invalida               |
| ------------------- | ------------------------------------ | ------------------------------ |
| **Coleções**        | `'Parcels'` (plural)                 | `'Parcels'`                    |
| **Entidade única**  | `{type: 'Parcels', id: '123'}`       | `{type: 'Parcels', id: '123'}` |
| **Lista + Detalhe** | `['Parcels', {type: 'Parcels', id}]` | `['Parcels']` ou id específico |

### Exemplos de Tags

```tsx
// Coleção: fornece tag de lista
providesTags: ['Parcels']

// Entidade única: fornece tag específica + tag de lista
providesTags: (result) => 
  result 
    ? [{ type: 'Parcels', id: result.id }, 'Parcels']
    : ['Parcels']

// Mutação: invalida tanto a lista quanto a entidade específica
invalidatesTags: (result, error, arg) => [
  { type: 'Parcels', id: arg.id },
  'Parcels'
]
```

## Criando Endpoints

Endpoints DEVERIAM ficar co-localizados com sua feature em `feature.client.ts` arquivos.

### Endpoint de Query (Leitura)

```tsx
// src/features/land/land.client.ts
import { client } from '@/services/client';

export type Tile = {
  x: number;
  y: number;
  type: 'parcel' | 'road' | 'plaza';
  owner?: string;
};

export type Parcel = {
  id: string;
  x: number;
  y: number;
  owner: string;
  name?: string;
  description?: string;
};

export const landClient = client.injectEndpoints({
  endpoints: (build) => ({
    // Obter todos os tiles
    getTiles: build.query<Record<string, Tile>, void>({
      query: () => '/v1/tiles',
      providesTags: ['Parcels'],
    }),
    
    // Obter parcel por coordenadas
    getParcelByCoords: build.query<Parcel, { x: number; y: number }>({
      query: ({ x, y }) => `/v1/lands/${x}/${y}`,
      providesTags: (result, error, arg) =>
        result
          ? [{ type: 'Parcels', id: result.id }, 'Parcels']
          : ['Parcels'],
    }),
    
    // Obter parcels por proprietário
    getParcelsByOwner: build.query<Parcel[], { owner: string }>({
      query: ({ owner }) => `/v1/lands/owner/${owner}`,
      providesTags: (result) =>
        result
          ? [
              ...result.map(({ id }) => ({ type: 'Parcels' as const, id })),
              'Parcels',
            ]
          : ['Parcels'],
    }),
  }),
  overrideExisting: false,
});

// Exportar hooks
export const {
  useGetTilesQuery,
  useGetParcelByCoordsQuery,
  useGetParcelsByOwnerQuery,
} = landClient;
```

### Endpoint de Mutation (Escrita)

```tsx
// src/features/land/land.client.ts (continuação)
export const landClient = client.injectEndpoints({
  endpoints: (build) => ({
    // ... endpoints de query ...
    
    // Atualizar nome do parcel
    updateParcelName: build.mutation<
      Parcel,
      { id: string; name: string }
    >({
      query: ({ id, name }) => ({
        url: `/v1/lands/${id}`,
        method: 'PATCH',
        body: { name },
      }),
      invalidatesTags: (result, error, arg) => [
        { type: 'Parcels', id: arg.id },
        'Parcels',
      ],
    }),
    
    // Transferir parcel
    transferParcel: build.mutation<
      { ok: boolean },
      { id: string; to: string }
    >({
      query: ({ id, to }) => ({
        url: `/v1/lands/${id}/transfer`,
        method: 'POST',
        body: { to },
      }),
      invalidatesTags: (result, error, arg) => [
        { type: 'Parcels', id: arg.id },
        'Parcels', // Invalidar lista para atualizar filtros de proprietário
      ],
    }),
  }),
});

export const {
  useUpdateParcelNameMutation,
  useTransferParcelMutation,
} = landClient;
```

## Atualizações Otimistas

Use `onQueryStarted` para atualizações otimistas de UI com rollback automático em caso de falha.

```tsx
// src/features/credits/credits.client.ts
import { client } from '@/services/client';

export type CreditsBalance = {
  address: string;
  amount: number;
  lastUpdated: string;
};

export const creditsClient = client.injectEndpoints({
  endpoints: (build) => ({
    getBalance: build.query<CreditsBalance, { address: string }>({
      query: ({ address }) => `/v1/credits/${address}`,
      providesTags: (result, error, arg) => [
        { type: 'Credits', id: arg.address }
      ],
    }),
    
    grantCredits: build.mutation<
      { ok: true; newBalance: number },
      { address: string; amount: number }
    >({
      query: (body) => ({
        url: `/v1/credits/grant`,
        method: 'POST',
        body,
      }),
      
      // Atualização otimista
      async onQueryStarted({ address, amount }, { dispatch, queryFulfilled }) {
        // Atualizar o cache otimisticamente
        const patchResult = dispatch(
          client.util.updateQueryData('getBalance', { address }, (draft) => {
            draft.amount += amount;
            draft.lastUpdated = new Date().toISOString();
          })
        );
        
        try {
          // Aguardar a conclusão da mutation
          const { data } = await queryFulfilled;
          
          // Atualizar com a resposta do servidor
          dispatch(
            client.util.updateQueryData('getBalance', { address }, (draft) => {
              draft.amount = data.newBalance;
            })
          );
        } catch {
          // Rollback em caso de falha
          patchResult.undo();
        }
      },
      
      // Também invalidar para garantir consistência
      invalidatesTags: (result, error, arg) => [
        { type: 'Credits', id: arg.address }
      ],
    }),
  }),
});

export const { useGetBalanceQuery, useGrantCreditsMutation } = creditsClient;
```

## Opções Avançadas de Query

### Polling

```tsx
// Poll a cada 10 segundos
const { data } = useGetBalanceQuery(
  { address },
  { pollingInterval: 10000 }
);
```

### Pular Query

```tsx
// Pular query se o address não estiver disponível
const { data } = useGetBalanceQuery(
  { address: address! },
  { skip: !address }
);
```

### Query Preguiçosa

```tsx
const [trigger, result] = useLazyGetParcelByCoordsQuery();

// Disparar manualmente
const handleClick = () => {
  trigger({ x: 10, y: 20 });
};
```

### Transformar Response

```tsx
getParcel: build.query<Parcel, string>({
  query: (id) => `/v1/lands/${id}`,
  transformResponse: (response: ApiResponse<Parcel>) => response.data,
})
```

### Serialização Customizada

Para paginação ou busca, personalize a serialização da chave de cache:

```tsx
searchParcels: build.query<Parcel[], { q: string; owner?: string; page?: number }>({
  query: (args) => ({
    url: '/v1/parcels/search',
    params: args,
  }),
  
  // Chave de cache customizada para lidar com params opcionais
  serializeQueryArgs: ({ endpointName, queryArgs }) => {
    const { q, owner = 'any', page = 1 } = queryArgs;
    return `${endpointName}-${q}-${owner}-${page}`;
  },
  
  // Mesclar resultados para paginação
  merge(currentCache, newItems, { arg }) {
    if (arg.page === 1) {
      return newItems;
    }
    return [...currentCache, ...newItems];
  },
  
  // Forçar refetch quando os args mudarem
  forceRefetch({ currentArg, previousArg }) {
    return JSON.stringify(currentArg) !== JSON.stringify(previousArg);
  },
  
  providesTags: ['Parcels'],
})
```

## Tratamento de Erros

### Tratamento de Erro Customizado

```tsx
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';

export function isFetchBaseQueryError(
  error: unknown
): error is FetchBaseQueryError {
  return typeof error === 'object' && error != null && 'status' in error;
}

export function isErrorWithMessage(
  error: unknown
): error is { message: string } {
  return (
    typeof error === 'object' &&
    error != null &&
    'message' in error &&
    typeof (error as any).message === 'string'
  );
}
```

Uso em componentes:

```tsx
const { data, error } = useGetParcelQuery({ id });

if (error) {
  if (isFetchBaseQueryError(error)) {
    const errMsg = 'error' in error ? error.error : JSON.stringify(error.data);
    return <div>Error: {errMsg}</div>;
  } else if (isErrorWithMessage(error)) {
    return <div>Error: {error.message}</div>;
  }
}
```

## Gerenciamento de Cache

### Atualizações manuais de cache

```tsx
// Atualizar cache diretamente
dispatch(
  client.util.updateQueryData('getBalance', { address }, (draft) => {
    draft.amount = 1000;
  })
);
```

### Invalidar Cache

```tsx
// Invalidar todas as queries Credits
dispatch(client.util.invalidateTags(['Credits']));

// Invalidar entidade específica
dispatch(client.util.invalidateTags([{ type: 'Credits', id: address }]));
```

### Resetar Estado do Client

```tsx
// Resetar todo o estado do client
dispatch(client.util.resetApiState());
```

### Prefetch de Dados

```tsx
// Prefetch de dados antes da navegação
dispatch(
  client.util.prefetch('getParcel', { id: '123' }, { force: false })
);
```

## Boas Práticas

### 1. Use nomes de endpoint descritivos

```tsx
// ✅ Bom
getParcelByCoords
getParcelsByOwner
updateParcelName

// ❌ Ruim
getParcel
fetch
update
```

### 2. Forneça Tags abrangentes

```tsx
// ✅ Bom: Fornece tanto tag de lista quanto tag de entidade
providesTags: (result) =>
  result
    ? [{ type: 'Parcels', id: result.id }, 'Parcels']
    : ['Parcels']

// ❌ Ruim: Fornece apenas a tag de lista
providesTags: ['Parcels']
```

### 3. Trate estados de carregamento e erro

```tsx
// ✅ Bom: Tratamento completo de estados
const { data, isLoading, isFetching, isError, error } = useGetParcelQuery({ id });

if (isLoading) return <Spinner />;
if (isError) return <Error error={error} />;
if (!data) return null;

// ❌ Ruim: Tratamento incompleto de estados
const { data } = useGetParcelQuery({ id });
return <div>{data.name}</div>; // Pode travar se data for undefined
```

### 4. Use Type Guards

```tsx
// ✅ Bom: Tratamento de erro com segurança de tipos
if (isFetchBaseQueryError(error)) {
  // Tratar erro de fetch
} else if (isErrorWithMessage(error)) {
  // Tratar erro com message
}

// ❌ Ruim: Cast inseguro de tipos
const message = (error as any).message;
```

## Próximos Passos

* Aprenda sobre [Gerenciamento de Estado](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/state-management) para estado de UI local
* Rever [Padrões de Componentes](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/component-patterns) para exemplos de uso
* Entender [Integração Web3](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/web3-integration) para dados de blockchain
