# Component Patterns

Esta página explica como usar efetivamente Redux e RTK Query em componentes React, incluindo hooks, padrões de otimização e melhores práticas.

## Uso Básico de Query

Use hooks gerados a partir dos endpoints do RTK Query:

```tsx
import { useGetParcelByCoordsQuery } from '@/features/land/land.client';

function ParcelCard({ x, y }: { x: number; y: number }) {
  const { data, isLoading, isError, error } = useGetParcelByCoordsQuery({ x, y });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.toString()}</div>;
  if (!data) return null;

  return (
    <div>
      <h2>{data.name || `Parcel ${x},${y}`}</h2>
      <p>Owner: {data.owner}</p>
    </div>
  );
}
```

## Otimização de Re-renders com `selectFromResult`

Reduza o resultado apenas aos campos que você precisa:

```tsx
function ParcelOwner({ x, y }: { x: number; y: number }) {
  // ✅ Bom: Inscreve-se apenas nas mudanças do campo owner
  const { owner, isFetching } = useGetParcelByCoordsQuery(
    { x, y },
    {
      selectFromResult: ({ data, isFetching }) => ({
        owner: data?.owner,
        isFetching,
      }),
    }
  );

  if (isFetching) return <span>Loading...</span>;
  return <span>Owner: {owner}</span>;
}

// ❌ Ruim: Re-renderiza em qualquer alteração de data
function ParcelOwnerBad({ x, y }: { x: number; y: number }) {
  const { data } = useGetParcelByCoordsQuery({ x, y });
  return <span>Owner: {data?.owner}</span>;
}
```

## Queries Condicionais com `skip`

Pule queries quando os argumentos não estiverem prontos:

```tsx
function UserProfile() {
  const { data: session } = useGetSessionQuery();
  const userId = session?.userId;

  // Pule a query de profile até termos userId
  const { data: profile } = useGetProfileQuery(
    { id: userId! },
    {
      skip: !userId, // Não buscar se userId for undefined
    }
  );

  if (!userId) return <div>Please log in</div>;
  if (!profile) return <div>Loading profile...</div>;

  return <div>{profile.name}</div>;
}
```

## Polling para Atualizações em Tempo Real

```tsx
function LiveBalance({ address }: { address: string }) {
  const { data } = useGetBalanceQuery(
    { address },
    {
      pollingInterval: 10000, // Poll a cada 10 segundos
      skipPollingIfUnfocused: true, // Pausar quando a aba não estiver focada
    }
  );

  return <div>Balance: {data?.amount ?? 0}</div>;
}
```

## Queries Lazy

Dispare queries manualmente em vez de no mount do componente:

```tsx
function SearchParcels() {
  const [trigger, result] = useLazyGetParcelsByOwnerQuery();
  const [owner, setOwner] = useState('');

  const handleSearch = () => {
    if (owner) {
      trigger({ owner });
    }
  };

  return (
    <div>
      <input
        value={owner}
        onChange={(e) => setOwner(e.target.value)}
        placeholder="Enter owner address"
      />
      <button onClick={handleSearch}>Search</button>
      
      {result.isLoading && <div>Searching...</div>}
      {result.data && (
        <ul>
          {result.data.map((parcel) => (
            <li key={parcel.id}>{parcel.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

## Uso de Mutation

### Mutation Básica

```tsx
function GrantCreditsButton({ address }: { address: string }) {
  const [grantCredits, { isLoading, isError, error }] = useGrantCreditsMutation();

  const handleGrant = async () => {
    try {
      await grantCredits({ address, amount: 100 }).unwrap();
      toast.success('Credits granted successfully!');
    } catch (err) {
      toast.error('Failed to grant credits');
      console.error('Grant failed:', err);
    }
  };

  return (
    <button onClick={handleGrant} disabled={isLoading}>
      {isLoading ? 'Granting...' : 'Grant 100 Credits'}
    </button>
  );
}
```

### Mutation com Feedback

```tsx
import { isFetchBaseQueryError } from '@/services/client';

function UpdateParcelName({ parcelId }: { parcelId: string }) {
  const [name, setName] = useState('');
  const [updateName, { isLoading, isSuccess, isError, error }] =
    useUpdateParcelNameMutation();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      const result = await updateName({ id: parcelId, name }).unwrap();
      toast.success(`Parcel renamed to "${result.name}"`);
      setName(''); // Limpar o formulário
    } catch (err) {
      if (isFetchBaseQueryError(err)) {
        toast.error(`Error: ${JSON.stringify(err.data)}`);
      } else {
        toast.error('An unexpected error occurred');
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="New name"
        disabled={isLoading}
      />
      <button type="submit" disabled={isLoading || !name}>
        {isLoading ? 'Updating...' : 'Update Name'}
      </button>
      
      {isSuccess && <p className="success">Name updated!</p>}
      {isError && <p className="error">Update failed</p>}
    </form>
  );
}
```

## Usando Estado de Slice

Acesse o estado do slice com `useAppSelector`:

```tsx
import { useAppSelector, useAppDispatch } from '@/app/hooks';
import { selectViewMode, viewModeChanged } from '@/features/ui/ui.slice';

function ViewModeToggle() {
  const dispatch = useAppDispatch();
  const viewMode = useAppSelector(selectViewMode);

  const toggleMode = () => {
    dispatch(viewModeChanged(viewMode === 'grid' ? 'list' : 'grid'));
  };

  return (
    <button onClick={toggleMode}>
      View: {viewMode === 'grid' ? '⊞ Grid' : '☰ List'}
    </button>
  );
}
```

## Combinando Múltiplas Queries

```tsx
function ParcelDetails({ id }: { id: string }) {
  const { data: parcel, isLoading: parcelLoading } = useGetParcelQuery({ id });
  const { data: owner, isLoading: ownerLoading } = useGetProfileQuery(
    { id: parcel?.owner! },
    { skip: !parcel?.owner }
  );

  const isLoading = parcelLoading || ownerLoading;

  if (isLoading) return <div>Loading...</div>;
  if (!parcel) return <div>Parcel not found</div>;

  return (
    <div>
      <h1>{parcel.name}</h1>
      <p>Coordinates: {parcel.x}, {parcel.y}</p>
      {owner && <p>Owner: {owner.name}</p>}
    </div>
  );
}
```

## Estado Derivado com Selectors

```tsx
import { useAppSelector } from '@/app/hooks';
import { selectFilteredParcels, selectActiveFiltersCount } from '@/features/land/land.selectors';

function ParcelList() {
  const parcels = useAppSelector(selectFilteredParcels);
  const filterCount = useAppSelector(selectActiveFiltersCount);

  return (
    <div>
      <h2>
        Parcels ({parcels.length})
        {filterCount > 0 && ` - ${filterCount} filter(s) active`}
      </h2>
      <ul>
        {parcels.map((parcel) => (
          <li key={parcel.id}>{parcel.name}</li>
        ))}
      </ul>
    </div>
  );
}
```

## Despacho de Ações

```tsx
import { useAppDispatch } from '@/app/hooks';
import { modalOpened, modalClosed } from '@/features/ui/ui.slice';

function TransferButton({ parcelId }: { parcelId: string }) {
  const dispatch = useAppDispatch();

  const handleClick = () => {
    // Despachar ação para abrir modal
    dispatch(modalOpened());
  };

  return <button onClick={handleClick}>Transfer Parcel</button>;
}
```

## Selectors do Entity Adapter

```tsx
import { useAppSelector } from '@/app/hooks';
import { creditsSelectors, selectTotalCredits } from '@/features/credits/credits.slice';

function CreditsSummary() {
  // Obter todas as transações
  const allTxs = useAppSelector(creditsSelectors.selectAll);
  
  // Obter transação específica
  const txId = 'tx-123';
  const tx = useAppSelector((state) => 
    creditsSelectors.selectById(state, txId)
  );
  
  // Obter contagem total
  const count = useAppSelector(creditsSelectors.selectTotal);
  
  // Obter valor derivado
  const total = useAppSelector(selectTotalCredits);

  return (
    <div>
      <p>Total Credits: {total}</p>
      <p>Transactions: {count}</p>
      <ul>
        {allTxs.map((tx) => (
          <li key={tx.id}>
            {tx.type === 'grant' ? '+' : '-'}
            {tx.amount} - {tx.description}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Prefetching de Dados

Pré-busque dados antes da navegação para uma UX mais rápida:

```tsx
import { useAppDispatch } from '@/app/hooks';
import { client } from '@/services/client';
import { Link } from 'react-router-dom';

function ParcelListItem({ parcel }: { parcel: Parcel }) {
  const dispatch = useAppDispatch();

  const handleMouseEnter = () => {
    // Pré-buscar detalhes do parcel ao passar o mouse
    dispatch(
      client.util.prefetch('getParcel', { id: parcel.id }, { force: false })
    );
  };

  return (
    <Link
      to={`/parcels/${parcel.id}`}
      onMouseEnter={handleMouseEnter}
    >
      {parcel.name}
    </Link>
  );
}
```

## Gerenciamento Manual de Cache

```tsx
import { useAppDispatch } from '@/app/hooks';
import { client } from '@/services/client';

function RefreshButton() {
  const dispatch = useAppDispatch();

  const handleRefresh = () => {
    // Invalidar todas as queries de Parcels
    dispatch(client.util.invalidateTags(['Parcels']));
    
    // Ou resetar todo o estado do client
    // dispatch(client.util.resetApiState());
  };

  return <button onClick={handleRefresh}>Refresh Data</button>;
}
```

## Tratando Estados de Query

```tsx
function ComprehensiveExample({ id }: { id: string }) {
  const {
    data,
    isLoading,      // Carregamento inicial
    isFetching,     // Qualquer fetch (incluindo refetch)
    isSuccess,      // Query bem-sucedida
    isError,        // Query falhou
    error,          // Objeto de erro
    refetch,        // Função de refetch manual
  } = useGetParcelQuery({ id });

  // Diferentes estados
  if (isLoading) {
    return <Spinner />;
  }

  if (isError) {
    return (
      <div>
        <p>Error: {error.toString()}</p>
        <button onClick={() => refetch()}>Retry</button>
      </div>
    );
  }

  if (!data) {
    return <div>No data</div>;
  }

  return (
    <div>
      {isFetching && <div className="refetch-indicator">Updating...</div>}
      <h1>{data.name}</h1>
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  );
}
```

## Integração com Formulários

```tsx
import { useState } from 'react';
import { useAppDispatch } from '@/app/hooks';
import { filtersUpdated } from '@/features/land/land.slice';

function FilterForm() {
  const dispatch = useAppDispatch();
  const [owner, setOwner] = useState('');
  const [minPrice, setMinPrice] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    dispatch(filtersUpdated({
      owner: owner || undefined,
      minPrice: minPrice ? parseInt(minPrice, 10) : undefined,
    }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={owner}
        onChange={(e) => setOwner(e.target.value)}
        placeholder="Owner address"
      />
      <input
        type="number"
        value={minPrice}
        onChange={(e) => setMinPrice(e.target.value)}
        placeholder="Min price"
      />
      <button type="submit">Apply Filters</button>
    </form>
  );
}
```

## Boas Práticas

### 1. Use Typed Hooks

```tsx
// ✅ Bom: Usa hooks tipados
import { useAppSelector, useAppDispatch } from '@/app/hooks';

const value = useAppSelector(selectValue);
const dispatch = useAppDispatch();

// ❌ Ruim: Usa hooks não tipados
import { useSelector, useDispatch } from 'react-redux';

const value = useSelector((state: RootState) => state.value);
const dispatch = useDispatch();
```

### 2. Trate Todos os Estados de Query

```tsx
// ✅ Bom: Tratamento abrangente de estados
const { data, isLoading, isError, error } = useQuery(args);
if (isLoading) return <Loading />;
if (isError) return <Error error={error} />;
if (!data) return null;
return <Content data={data} />;

// ❌ Ruim: Falta tratamento de erro
const { data } = useQuery(args);
return <Content data={data} />; // Pode quebrar
```

### 3. Use `.unwrap()` para Mutations

```tsx
// ✅ Bom: Tratamento explícito de erros
try {
  const result = await mutation(args).unwrap();
  toast.success('Success!');
} catch (error) {
  toast.error('Failed!');
}

// ❌ Ruim: Sem tratamento de erro
mutation(args);
```

### 4. Reduza Resultados com `selectFromResult`

```tsx
// ✅ Bom: Inscreve-se apenas nos campos necessários
const { name } = useQuery(args, {
  selectFromResult: ({ data }) => ({ name: data?.name }),
});

// ❌ Ruim: Inscreve-se no objeto inteiro
const { data } = useQuery(args);
const name = data?.name;
```

### 5. Evite Selecionar Slices Inteiros

```tsx
// ✅ Bom: Seleciona valores específicos
const viewMode = useAppSelector(selectViewMode);
const filters = useAppSelector(selectFilters);

// ❌ Ruim: Seleciona slice inteiro
const ui = useAppSelector((state) => state.ui);
const viewMode = ui.viewMode;
```

## Próximos Passos

* Aprenda sobre [Integração Web3](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/web3-integration) para padrões de blockchain
* Rever [Testes & Performance](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/testing-and-performance) para otimização
* Veja [RTK Query](https://docs.decentraland.org/contributor/contributor-pt/guias-para-contribuidores/ui-standards/rtk-query) para configuração avançada de endpoints
