# RTK Query

RTK Query es la potente capa de obtención y almacenamiento en caché de datos de Redux Toolkit. Elimina la necesidad de escribir creadores de acciones asíncronas y gestiona automáticamente los estados de carga, el caché y la sincronización de datos.

## Configuración base del cliente

Todos los endpoints de RTK Query se extienden desde una única instancia base del cliente.

### Configuración de la 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, // p. ej., https://api.decentraland.org
  
  prepareHeaders: (headers, { getState }) => {
    const state = getState() as RootState;
    
    // Añadir token de autenticación
    const token = state.user.session?.authToken;
    if (token) {
      headers.set('authorization', `Bearer ${token}`);
    }
    
    // Añadir chain ID para el contexto web3
    const chainId = state.user.chainId;
    if (chainId) {
      headers.set('x-chain-id', String(chainId));
    }
    
    // Encabezados estándar
    headers.set('accept', 'application/json');
    headers.set('content-type', 'application/json');
    
    return headers;
  },
  
  credentials: 'omit', // o 'include' si el backend requiere cookies
});
```

### Instancia del Cliente

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

export const client = createApi({
  reducerPath: 'client',
  baseQuery,
  
  // Definir todos los tipos de tag posibles para la invalidación de caché
  tagTypes: [
    'User',
    'Profile',
    'Parcels',
    'Estates',
    'Credits',
    'Orders',
    'Sales',
    'NFTs',
  ],
  
  // Configuración del caché
  keepUnusedDataFor: 60,         // Mantener datos sin usar durante 60 segundos
  refetchOnFocus: true,           // Volver a obtener cuando la ventana recupere el foco
  refetchOnReconnect: true,       // Volver a obtener al reconectar
  refetchOnMountOrArgChange: 30,  // Volver a obtener si los datos tienen más de 30s
  
  // Los endpoints serán inyectados en archivos de característica
  endpoints: () => ({}),
});
```

## Convenciones de Tags

Los tags se usan para la invalidación y sincronización del caché. Sigue estas convenciones:

### Nomenclatura de Tags

| Tipo de recurso        | Tags de Query                        | Mutación invalida              |
| ---------------------- | ------------------------------------ | ------------------------------ |
| **Colecciones**        | `'Parcels'` (plural)                 | `'Parcels'`                    |
| **Entidad individual** | `{type: 'Parcels', id: '123'}`       | `{type: 'Parcels', id: '123'}` |
| **Lista + Detalle**    | `['Parcels', {type: 'Parcels', id}]` | `['Parcels']` o id específico  |

### Ejemplos de Tags

```tsx
// Colección: provee tag de lista
providesTags: ['Parcels']

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

// Mutación: invalida tanto la lista como la entidad específica
invalidatesTags: (result, error, arg) => [
  { type: 'Parcels', id: arg.id },
  'Parcels'
]
```

## Creando Endpoints

Los endpoints DEBERÍAN ubicarse junto con su feature en `feature.client.ts` archivos.

### Endpoint de Query (Lectura)

```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) => ({
    // Obtener todos los tiles
    getTiles: build.query<Record<string, Tile>, void>({
      query: () => '/v1/tiles',
      providesTags: ['Parcels'],
    }),
    
    // Obtener parcela 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'],
    }),
    
    // Obtener parcelas por propietario
    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 Mutación (Escritura)

```tsx
// src/features/land/land.client.ts (continuación)
export const landClient = client.injectEndpoints({
  endpoints: (build) => ({
    // ... endpoints de query ...
    
    // Actualizar nombre de parcela
    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 parcela
    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 actualizar filtros por propietario
      ],
    }),
  }),
});

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

## Actualizaciones Optimistas

Usa `onQueryStarted` para actualizaciones optimistas de UI con reversión automática en caso de fallo.

```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,
      }),
      
      // Actualización optimista
      async onQueryStarted({ address, amount }, { dispatch, queryFulfilled }) {
        // Actualizar el caché de forma optimista
        const patchResult = dispatch(
          client.util.updateQueryData('getBalance', { address }, (draft) => {
            draft.amount += amount;
            draft.lastUpdated = new Date().toISOString();
          })
        );
        
        try {
          // Esperar a que la mutación se complete
          const { data } = await queryFulfilled;
          
          // Actualizar con la respuesta del servidor
          dispatch(
            client.util.updateQueryData('getBalance', { address }, (draft) => {
              draft.amount = data.newBalance;
            })
          );
        } catch {
          // Reversión en caso de fallo
          patchResult.undo();
        }
      },
      
      // También invalidar para asegurar consistencia
      invalidatesTags: (result, error, arg) => [
        { type: 'Credits', id: arg.address }
      ],
    }),
  }),
});

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

## Opciones avanzadas de Query

### Polling

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

### Omitir Query

```tsx
// Omitir query si la dirección no está disponible
const { data } = useGetBalanceQuery(
  { address: address! },
  { skip: !address }
);
```

### Query perezosa

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

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

### Transformar Respuesta

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

### Serialización Personalizada

Para paginación o búsqueda, personaliza la serialización de la clave de caché:

```tsx
searchParcels: build.query<Parcel[], { q: string; owner?: string; page?: number }>({
  query: (args) => ({
    url: '/v1/parcels/search',
    params: args,
  }),
  
  // Clave de caché personalizada para manejar params opcionales
  serializeQueryArgs: ({ endpointName, queryArgs }) => {
    const { q, owner = 'any', page = 1 } = queryArgs;
    return `${endpointName}-${q}-${owner}-${page}`;
  },
  
  // Fusionar resultados para paginación
  merge(currentCache, newItems, { arg }) {
    if (arg.page === 1) {
      return newItems;
    }
    return [...currentCache, ...newItems];
  },
  
  // Forzar refetch cuando los args cambian
  forceRefetch({ currentArg, previousArg }) {
    return JSON.stringify(currentArg) !== JSON.stringify(previousArg);
  },
  
  providesTags: ['Parcels'],
})
```

## Manejo de errores

### Manejo de Errores Personalizado

```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 en 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>;
  }
}
```

## Gestión del Caché

### Actualizaciones manuales del caché

```tsx
// Actualizar caché directamente
dispatch(
  client.util.updateQueryData('getBalance', { address }, (draft) => {
    draft.amount = 1000;
  })
);
```

### Invalidar Caché

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

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

### Restablecer el estado del cliente

```tsx
// Restablecer todo el estado del cliente
dispatch(client.util.resetApiState());
```

### Prefetch de Datos

```tsx
// Prefetch de datos antes de la navegación
dispatch(
  client.util.prefetch('getParcel', { id: '123' }, { force: false })
);
```

## Buenas prácticas

### 1. Usar nombres descriptivos para los endpoints

```tsx
// ✅ Bueno
getParcelByCoords
getParcelsByOwner
updateParcelName

// ❌ Malo
getParcel
fetch
update
```

### 2. Proveer tags comprehensivos

```tsx
// ✅ Bueno: Provee tanto tags de lista como de entidad
providesTags: (result) =>
  result
    ? [{ type: 'Parcels', id: result.id }, 'Parcels']
    : ['Parcels']

// ❌ Malo: Solo provee tag de lista
providesTags: ['Parcels']
```

### 3. Manejar estados de carga y error

```tsx
// ✅ Bueno: Manejo 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;

// ❌ Malo: Manejo incompleto de estados
const { data } = useGetParcelQuery({ id });
return <div>{data.name}</div>; // Puede fallar si data es undefined
```

### 4. Usar Type Guards

```tsx
// ✅ Bueno: Manejo de errores con seguridad de tipos
if (isFetchBaseQueryError(error)) {
  // Manejar error de fetch
} else if (isErrorWithMessage(error)) {
  // Manejar error con message
}

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

## Siguientes pasos

* Aprende sobre [Gestión de Estado](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/state-management.md) para estado UI local
* Revisar [Patrones de Componentes](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/component-patterns.md) para ejemplos de uso
* Entender [Integración Web3](/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/web3-integration.md) para datos de blockchain


---

# Agent Instructions: 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:

```
GET https://docs.decentraland.org/contributor/contributor-es/guias-para-colaboradores/estandares-de-ui/rtk-query.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
