react-clean-architecture

Clean Architecture for React Native (Expo) with TypeScript and Bun. Use this skill when creating features, refactoring code, or reviewing code in React Native projects. Enforces strict separation between Core (domain), Infrastructure (adapters), and UI layers. Implements ports/adapters pattern, Result pattern for errors, and atomic design for components.

$ Installer

git clone https://github.com/benaor/claude-config /tmp/claude-config && cp -r /tmp/claude-config/.claude/skills/react-clean-architecture ~/.claude/skills/claude-config

// tip: Run this command in your terminal to install the skill


name: react-clean-architecture description: Clean Architecture for React Native (Expo) with TypeScript and Bun. Use this skill when creating features, refactoring code, or reviewing code in React Native projects. Enforces strict separation between Core (domain), Infrastructure (adapters), and UI layers. Implements ports/adapters pattern, Result pattern for errors, and atomic design for components.

React Clean Architecture

Prescriptive architecture for React Native (Expo) applications with TypeScript and Bun.

Stack: React Native (Expo) โ€ข TypeScript โ€ข Bun โ€ข Zustand (client state) โ€ข React Query (server state)

Patterns: Ports/Adapters โ€ข Use Cases โ€ข Result Pattern โ€ข ViewModel โ€ข Atomic Design

Core Principle

Core depends on NOTHING. UI and Infrastructure can import from Core, never the other way around.

UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”œโ”€โ”€โ–ถ Core (entities, use cases, ports)
Infrastructure โ”€โ”€โ”˜

Project Structure

src/
โ”œโ”€โ”€ ui/                              # Global components and hooks
โ”‚   โ”œโ”€โ”€ components/                  # Atomic Design
โ”‚   โ”‚   โ”œโ”€โ”€ atoms/                   # e.g., Button, Text, Icon
โ”‚   โ”‚   โ”œโ”€โ”€ molecules/               # e.g., InputField, Card
โ”‚   โ”‚   โ”œโ”€โ”€ organisms/               # e.g., Header, Form
โ”‚   โ”‚   โ””โ”€โ”€ templates/               # e.g., PageLayout
โ”‚   โ”œโ”€โ”€ hooks/                       # Global hooks (useToggle, useDebounce)
โ”‚   โ””โ”€โ”€ theme/                       # Palette, fonts, spacing
โ”‚
โ”œโ”€โ”€ modules/
โ”‚   โ”œโ”€โ”€ [bounded-context]/           # e.g., authentication, events, profile
โ”‚   โ”‚   โ”œโ”€โ”€ core/                    # Pure domain (no external dependencies)
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ entities/            # Business types/interfaces
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ User.entity.ts
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ports/               # Interfaces (contracts)
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ AuthRepository.port.ts
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ usecases/            # Business logic
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ Login.usecase.ts
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ infrastructure/          # Port implementations
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ adapters/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ AuthApi.adapter.ts
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ””โ”€โ”€ ui/                      # React-specific for this context
โ”‚   โ”‚       โ”œโ”€โ”€ components/          # e.g., AuthenticationCard
โ”‚   โ”‚       โ”œโ”€โ”€ screens/             # e.g., LoginScreen.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ hooks/               # e.g., useAuthentication.tsx
โ”‚   โ”‚       โ”œโ”€โ”€ stores/              # Zustand stores
โ”‚   โ”‚       โ”‚   โ””โ”€โ”€ auth.store.ts
โ”‚   โ”‚       โ””โ”€โ”€ viewModels/          # UI orchestration
โ”‚   โ”‚           โ””โ”€โ”€ useLogin.viewModel.tsx
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ shared/                      # Shared code between bounded contexts
โ”‚   โ”‚   โ”œโ”€โ”€ analytics/
โ”‚   โ”‚   โ”œโ”€โ”€ storage/
โ”‚   โ”‚   โ”œโ”€โ”€ toaster/
โ”‚   โ”‚   โ””โ”€โ”€ utils/
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ app/                         # Application configuration
โ”‚       โ”œโ”€โ”€ dependencies/
โ”‚       โ”‚   โ”œโ”€โ”€ Dependencies.type.ts
โ”‚       โ”‚   โ”œโ”€โ”€ dependencies.dev.ts
โ”‚       โ”‚   โ”œโ”€โ”€ dependencies.prod.ts
โ”‚       โ”‚   โ””โ”€โ”€ dependencies.test-env.ts
โ”‚       โ”œโ”€โ”€ react/
โ”‚       โ”‚   โ”œโ”€โ”€ useDependencies.tsx
โ”‚       โ”‚   โ”œโ”€โ”€ render.tsx
โ”‚       โ”‚   โ””โ”€โ”€ renderHook.tsx
โ”‚       โ””โ”€โ”€ main.ts
โ”‚
โ”œโ”€โ”€ constants/                       # TestIDs, screen names
โ”œโ”€โ”€ types/                           # General types (ISO8601, DeepPartial)
โ””โ”€โ”€ utils/                           # Utility functions
    โ”œโ”€โ”€ strings/
    โ”‚   โ””โ”€โ”€ firstCharToUppercase.ts
    โ””โ”€โ”€ dates/
        โ””โ”€โ”€ isISO8601Before.ts

File Naming Conventions

TypeExtensionExample
Entity.entity.tsUser.entity.ts
Port.port.tsAuthRepository.port.ts
Use Case.usecase.tsLogin.usecase.ts
Adapter.adapter.tsAuthApi.adapter.ts
ViewModel.viewModel.tsxuseLogin.viewModel.tsx
Store.store.tsauth.store.ts
Model (API response).model.tsLoginResponse.model.ts

React components: PascalCase.tsx (e.g., LoginScreen.tsx, AuthenticationCard.tsx)

Layer Rules

Core (/modules/[context]/core/)

Core is pure and unaware of the outside world.

โœ… Defines entities (types/interfaces)
โœ… Defines ports (dependency interfaces)
โœ… Contains use cases (business logic)
โœ… Uses Result pattern for errors
โœ… Can import from: types/, utils/, other files in the same core

โŒ NEVER import from infrastructure/
โŒ NEVER import from ui/
โŒ NEVER depend on React
โŒ NEVER call APIs directly

Infrastructure (/modules/[context]/infrastructure/)

Implements ports defined in Core.

โœ… Adapters implement ports
โœ… Handles API calls, storage, external services
โœ… Transforms external data โ†’ Core entities
โœ… Returns Result<T, E>
โœ… Can import from: core/ (ports, entities)

โŒ NEVER contains business logic (just transformation/mapping)
โŒ NEVER import from ui/

UI (/modules/[context]/ui/)

Everything React-specific for the bounded context.

โœ… Screens, components, hooks specific to the context
โœ… ViewModels orchestrate: use cases โ†’ stores
โœ… Zustand stores for client state
โœ… Can import from: core/ (entities, use cases, ports)
โœ… Can call an adapter directly for simple CRUD (via React Query)

โŒ NEVER business logic in components
โŒ NEVER business logic in viewModels (delegate to use cases)

When to use a Use Case vs direct Adapter?

SituationApproach
Simple fetch, basic CRUDDirect adapter + React Query
Business logic, validation, orchestrationUse Case
// โœ… Simple CRUD โ†’ direct adapter
const { itemRepository } = useDependencies();
const query = useQuery({
  queryKey: ["item", id],
  queryFn: () => itemRepository.getById(id),
});

// โœ… Business logic โ†’ use case
const { authRepository } = useDependencies();
const result = await new LoginUseCase(authRepository).execute({
  email,
  password,
});

Result Pattern

Explicit handling of successes and errors without exceptions.

// types/Result.ts
type Success<T> = { success: true; data: T };
type Failure<E> = { success: false; error: E };
type Result<T, E = Error> = Success<T> | Failure<E>;

const ok = <T>(data: T): Success<T> => ({ success: true, data });
const fail = <E>(error: E): Failure<E> => ({ success: false, error });

export { Result, Success, Failure, ok, fail };

Usage in a use case:

// modules/authentication/core/usecases/Login.usecase.ts
import { Result, ok, fail } from "@/types/Result";
import { User } from "../entities/User.entity";
import { AuthRepository } from "../ports/AuthRepository.port";
import { AuthError } from "../entities/AuthError.entity";

interface LoginParams {
  email: string;
  password: string;
}

export class LoginUseCase {
  constructor(private authRepository: AuthRepository) {}

  async execute(params: LoginParams): Promise<Result<User, AuthError>> {
    const result = await this.authRepository.login(params);

    if (!result.success) {
      return fail(result.error);
    }

    // Business logic here if needed
    return ok(result.data);
  }
}

Usage in a viewModel:

// modules/authentication/ui/viewModels/useLogin.viewModel.tsx
import { LoginUseCase } from "../../core/usecases/Login.usecase";

export const useLoginViewModel = () => {
  const { authRepository } = useDependencies();
  const [state, setState] = useState<LoginState>({ status: "idle" });

  const handlers = {
    login: async (email: string, password: string) => {
      setState({ status: "loading" });

      const result = await new LoginUseCase(authRepository).execute({
        email,
        password,
      });

      if (result.success) {
        setState({ status: "success", user: result.data });
      } else {
        setState({ status: "error", error: result.error });
      }
    },
  };

  return { state, handlers };
};

React Query

React Query handles server state (remote data, cache, synchronization).

Where to place React Query hooks?

useQuery / useMutation hooks live in the viewModel or in dedicated hooks within the bounded context.

modules/[context]/ui/
โ”œโ”€โ”€ hooks/
โ”‚   โ”œโ”€โ”€ useItems.query.ts       # Reusable query
โ”‚   โ””โ”€โ”€ useCreateItem.mutation.ts
โ””โ”€โ”€ viewModels/
    โ””โ”€โ”€ useItemList.viewModel.tsx  # Can contain inline queries

Query Keys

Use a factory object for consistency and autocompletion:

// modules/items/ui/hooks/items.queryKeys.ts
export const itemsKeys = {
  all: ["items"] as const,
  lists: () => [...itemsKeys.all, "list"] as const,
  list: (filters: ItemFilters) => [...itemsKeys.lists(), filters] as const,
  details: () => [...itemsKeys.all, "detail"] as const,
  detail: (id: string) => [...itemsKeys.details(), id] as const,
};

Query Hook

// modules/items/ui/hooks/useItem.query.ts
import { useQuery } from "@tanstack/react-query";
import { useDependencies } from "@app/react/useDependencies";
import { itemsKeys } from "./items.queryKeys";

export const useItemQuery = (id: string) => {
  const { itemRepository } = useDependencies();

  return useQuery({
    queryKey: itemsKeys.detail(id),
    queryFn: () => itemRepository.getById(id),
    enabled: !!id,
  });
};

Mutation Hook

// modules/items/ui/hooks/useCreateItem.mutation.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDependencies } from "@app/react/useDependencies";
import { CreateItemUseCase } from "../../core/usecases/CreateItem.usecase";
import { itemsKeys } from "./items.queryKeys";

export const useCreateItemMutation = () => {
  const { itemRepository } = useDependencies();
  const queryClient = useQueryClient();

  const createItemUseCase = new CreateItemUseCase(itemRepository);

  return useMutation({
    mutationFn: (params: CreateItemParams) => createItemUseCase.execute(params),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: itemsKeys.lists() });
    },
  });
};

Usage in a ViewModel

// modules/items/ui/viewModels/useItemList.viewModel.tsx
import { useItemsQuery } from "../hooks/useItems.query";
import { useCreateItemMutation } from "../hooks/useCreateItem.mutation";

export const useItemListViewModel = () => {
  const itemsQuery = useItemsQuery();
  const createItemMutation = useCreateItemMutation();

  const state = {
    items: itemsQuery.data ?? [],
    isLoading: itemsQuery.isLoading,
    error: itemsQuery.error,
  };

  const handlers = {
    createItem: (params: CreateItemParams) => createItemMutation.mutate(params),
    refresh: () => itemsQuery.refetch(),
  };

  return { state, handlers };
};

React Query Conventions

RuleExample
Query hook naminguse[Entity].query.ts
Mutation hook naminguse[Action][Entity].mutation.ts
Query keys naming[entity].queryKeys.ts
Always invalidate after mutationqueryClient.invalidateQueries()
Use case in mutation if business logicnew CreateItemUseCase(...).execute()
Direct adapter in query if simple fetchrepository.getById(id)

Dependency Injection

Dependency injection system based on React Context, with environment-based configuration.

Structure

modules/app/
โ”œโ”€โ”€ dependencies/
โ”‚   โ”œโ”€โ”€ Dependencies.type.ts      # Dependencies interface
โ”‚   โ”œโ”€โ”€ dependencies.dev.ts       # Development implementation
โ”‚   โ”œโ”€โ”€ dependencies.prod.ts      # Production implementation
โ”‚   โ””โ”€โ”€ dependencies.test-env.ts  # Test implementation
โ”œโ”€โ”€ react/
โ”‚   โ”œโ”€โ”€ useDependencies.tsx       # Dependencies access hook
โ”‚   โ””โ”€โ”€ DependenciesProvider.tsx  # React provider
โ””โ”€โ”€ main.ts                       # Application bootstrap

Dependencies.type.ts

Defines the contract of available dependencies in the app:

// modules/app/dependencies/Dependencies.type.ts
import { AuthRepository } from "@modules/authentication/core/ports/AuthRepository.port";
import { ItemRepository } from "@modules/items/core/ports/ItemRepository.port";
import { StorageService } from "@modules/shared/storage/Storage.port";

export interface Dependencies {
  // Repositories
  authRepository: AuthRepository;
  itemRepository: ItemRepository;

  // Services
  storageService: StorageService;
}

Environment-based implementations

// modules/app/dependencies/dependencies.prod.ts
import { Dependencies } from "./Dependencies.type";
import { AuthApiAdapter } from "@modules/authentication/infrastructure/adapters/AuthApi.adapter";
import { ItemApiAdapter } from "@modules/items/infrastructure/adapters/ItemApi.adapter";
import { AsyncStorageAdapter } from "@modules/shared/storage/AsyncStorage.adapter";

export const prodDependencies: Dependencies = {
  authRepository: new AuthApiAdapter(),
  itemRepository: new ItemApiAdapter(),
  storageService: new AsyncStorageAdapter(),
};
// modules/app/dependencies/dependencies.test-env.ts
import { Dependencies } from "./Dependencies.type";
import { AuthInMemoryAdapter } from "@modules/authentication/infrastructure/adapters/AuthInMemory.adapter";
import { ItemInMemoryAdapter } from "@modules/items/infrastructure/adapters/ItemInMemory.adapter";
import { InMemoryStorageAdapter } from "@modules/shared/storage/InMemoryStorage.adapter";

export const testDependencies: Dependencies = {
  authRepository: new AuthInMemoryAdapter(),
  itemRepository: new ItemInMemoryAdapter(),
  storageService: new InMemoryStorageAdapter(),
};

main.ts

Application bootstrap with environment-based dependency selection:

// modules/app/main.ts
import { Dependencies } from "@app/dependencies/Dependencies.type";

export class Main {
  public dependencies: Dependencies;

  constructor() {
    this.dependencies = this.setupDependencies();
  }

  setupDependencies(): Dependencies {
    let importPath;
    let dependencies: Dependencies;

    switch (process.env.NODE_ENV) {
      case "production":
        importPath = require("@app/dependencies/dependencies.prod");
        dependencies = importPath.prodDependencies;
        break;
      case "test":
        importPath = require("@app/dependencies/dependencies.test-env");
        dependencies = importPath.testDependencies;
        break;
      default:
      case "development":
        importPath = require("@app/dependencies/dependencies.dev");
        dependencies = importPath.devDependencies;
        break;
    }

    return dependencies;
  }
}

export const app = new Main();

React Provider

// modules/app/react/DependenciesProvider.tsx
import { createContext, ReactNode } from "react";
import { Dependencies } from "@app/dependencies/Dependencies.type";
import { app } from "@app/main";

export const DependenciesContext = createContext<Dependencies | null>(null);

export const DependenciesProvider = ({
  children,
  dependencies,
}: {
  children: ReactNode;
  dependencies?: Partial<Dependencies>;
}) => (
  <DependenciesContext.Provider
    value={{ ...app.dependencies, ...dependencies }}
  >
    {children}
  </DependenciesContext.Provider>
);

useDependencies Hook

// modules/app/react/useDependencies.tsx
import { useContext } from "react";
import { DependenciesContext } from "./DependenciesProvider";
import { Dependencies } from "@app/dependencies/Dependencies.type";

export const useDependencies = (): Dependencies => {
  const dependencies = useContext(DependenciesContext);

  if (!dependencies) {
    throw new Error("useDependencies must be used within DependenciesProvider");
  }

  return dependencies;
};

Usage

// In a viewModel or hook
const { authRepository, storageService } = useDependencies();

// In a test โ€” partial override, other dependencies remain real
render(
  <DependenciesProvider dependencies={{ authRepository: mockAuthRepo }}>
    <ComponentUnderTest />
  </DependenciesProvider>
);

Workflows

Creating a new feature

Example: "Create an event" feature in the events bounded context

Step 1: Core โ€” Entities

Define business types.

// modules/events/core/entities/Event.entity.ts
export interface Event {
  id: string;
  title: string;
  date: ISO8601;
  organizerId: string;
}

// modules/events/core/entities/EventError.entity.ts
export type EventError =
  | { type: "VALIDATION_ERROR"; message: string }
  | { type: "NETWORK_ERROR" }
  | { type: "UNAUTHORIZED" };

Step 2: Core โ€” Port

Define the repository contract.

// modules/events/core/ports/EventRepository.port.ts
import { Result } from "@/types/Result";
import { Event, EventError } from "../entities/Event.entity";

export interface CreateEventParams {
  title: string;
  date: ISO8601;
}

export interface EventRepository {
  create(params: CreateEventParams): Promise<Result<Event, EventError>>;
  getById(id: string): Promise<Result<Event, EventError>>;
  list(): Promise<Result<Event[], EventError>>;
}

Step 3: Core โ€” Use Case (if business logic needed)

// modules/events/core/usecases/CreateEvent.usecase.ts
import { Result, ok, fail } from "@/types/Result";
import { Event, EventError } from "../entities/Event.entity";
import {
  EventRepository,
  CreateEventParams,
} from "../ports/EventRepository.port";

export class CreateEventUseCase {
  constructor(private eventRepository: EventRepository) {}

  async execute(params: CreateEventParams): Promise<Result<Event, EventError>> {
    // Business validation
    if (params.title.length < 3) {
      return fail({ type: "VALIDATION_ERROR", message: "Title too short" });
    }

    if (new Date(params.date) < new Date()) {
      return fail({
        type: "VALIDATION_ERROR",
        message: "Date must be in future",
      });
    }

    return this.eventRepository.create(params);
  }
}

Step 4: Infrastructure โ€” Adapter

Implement the port.

// modules/events/infrastructure/adapters/EventApi.adapter.ts
import { Result, ok, fail } from "@/types/Result";
import { Event, EventError } from "../../core/entities/Event.entity";
import {
  EventRepository,
  CreateEventParams,
} from "../../core/ports/EventRepository.port";
import { EventApiResponse } from "./EventApiResponse.model";

export class EventApiAdapter implements EventRepository {
  private baseUrl = "https://api.example.com";

  async create(params: CreateEventParams): Promise<Result<Event, EventError>> {
    try {
      const response = await fetch(`${this.baseUrl}/events`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(params),
      });

      if (!response.ok) {
        return fail({ type: "NETWORK_ERROR" });
      }

      const data: EventApiResponse = await response.json();
      return ok(this.mapToEntity(data));
    } catch {
      return fail({ type: "NETWORK_ERROR" });
    }
  }

  private mapToEntity(response: EventApiResponse): Event {
    return {
      id: response.id,
      title: response.title,
      date: response.date,
      organizerId: response.organizer_id, // snake_case โ†’ camelCase
    };
  }

  // ... other methods
}

Step 5: Register the dependency

// modules/app/dependencies/Dependencies.type.ts
import { EventRepository } from "@modules/events/core/ports/EventRepository.port";

export interface Dependencies {
  // ... others
  eventRepository: EventRepository;
}

// modules/app/dependencies/dependencies.prod.ts
import { EventApiAdapter } from "@modules/events/infrastructure/adapters/EventApi.adapter";

export const prodDependencies: Dependencies = {
  // ... others
  eventRepository: new EventApiAdapter(),
};

Step 6: UI โ€” Query Keys

// modules/events/ui/hooks/events.queryKeys.ts
export const eventsKeys = {
  all: ["events"] as const,
  lists: () => [...eventsKeys.all, "list"] as const,
  details: () => [...eventsKeys.all, "detail"] as const,
  detail: (id: string) => [...eventsKeys.details(), id] as const,
};

Step 7: UI โ€” Mutation Hook

// modules/events/ui/hooks/useCreateEvent.mutation.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useDependencies } from "@app/react/useDependencies";
import { CreateEventUseCase } from "../../core/usecases/CreateEvent.usecase";
import { eventsKeys } from "./events.queryKeys";

export const useCreateEventMutation = () => {
  const { eventRepository } = useDependencies();
  const queryClient = useQueryClient();

  const createEventUseCase = new CreateEventUseCase(eventRepository);

  return useMutation({
    mutationFn: createEventUseCase.execute.bind(createEventUseCase),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: eventsKeys.lists() });
    },
  });
};

Step 8: UI โ€” ViewModel

// modules/events/ui/viewModels/useCreateEvent.viewModel.tsx
import { useState } from "react";
import { useCreateEventMutation } from "../hooks/useCreateEvent.mutation";

interface FormState {
  title: string;
  date: string;
}

export const useCreateEventViewModel = () => {
  const [form, setForm] = useState<FormState>({ title: "", date: "" });
  const mutation = useCreateEventMutation();

  const state = {
    form,
    isLoading: mutation.isPending,
    error: mutation.data?.success === false ? mutation.data.error : null,
  };

  const handlers = {
    setTitle: (title: string) => setForm((f) => ({ ...f, title })),
    setDate: (date: string) => setForm((f) => ({ ...f, date })),
    submit: () => mutation.mutate({ title: form.title, date: form.date }),
  };

  return { state, handlers };
};

Step 9: UI โ€” Screen

// modules/events/ui/screens/CreateEventScreen.tsx
import { useCreateEventViewModel } from "../viewModels/useCreateEvent.viewModel";

export const CreateEventScreen = () => {
  const { state, handlers } = useCreateEventViewModel();

  return (
    <View>
      <TextInput
        value={state.form.title}
        onChangeText={handlers.setTitle}
        placeholder="Event title"
      />
      <TextInput
        value={state.form.date}
        onChangeText={handlers.setDate}
        placeholder="YYYY-MM-DD"
      />
      {state.error && <Text>{state.error.message}</Text>}
      <Button
        title="Create"
        onPress={handlers.submit}
        disabled={state.isLoading}
      />
    </View>
  );
};

Refactoring existing code

Identify violations

  1. Business logic in a component or viewModel โ†’ extract to Use Case
  2. Direct API call in a component โ†’ extract to Adapter
  3. Inline type or any โ†’ create an Entity
  4. Hardcoded dependency โ†’ extract to Port + Adapter

Refactoring process

1. Identify the violation
2. Create the target file (use case, adapter, entity)
3. Extract the code
4. Update imports
5. Verify Core doesn't import from UI/Infrastructure
6. Register new dependencies if needed

Example: extracting an API call from a component

Before (violation):

// โŒ Direct API call in component
const EventList = () => {
  const [events, setEvents] = useState([]);

  useEffect(() => {
    fetch("https://api.example.com/events")
      .then((r) => r.json())
      .then(setEvents);
  }, []);
};

After (clean):

// โœ… Adapter + Query + ViewModel
const EventList = () => {
  const { state } = useEventListViewModel();

  return <FlatList data={state.events} />;
};

Code Review Checklist

See references/code-review-checklist.md for the complete checklist.

References