Use a provider as composition root; consume via hooks.
useX()
hooks; swap in tests.// services.ts
export type Services = { todos: { list: () => Promise<string[]> } };
export const createServices = (baseUrl: string): Services => ({
todos: {
async list() {
const r = await fetch(`${baseUrl}/todos`);
return await r.json();
},
},
});
// ServicesProvider.tsx
import React, { createContext, useContext } from "react";
import type { Services } from "./services";
const Ctx = createContext<Services | null>(null);
export function ServicesProvider({
services,
children,
}: {
services: Services;
children: React.ReactNode;
}) {
return <Ctx.Provider value={services}>{children}</Ctx.Provider>;
}
export function useServices(): Services {
const s = useContext(Ctx);
if (!s) throw new Error("ServicesProvider missing");
return s;
}
// usage in a screen
import { useServices } from "./ServicesProvider";
const { todos } = useServices();
Create services, provide them at the app root, and consume from a screen.
// app/services.ts
export type Services = {
todos: { list: () => Promise<string[]> };
};
export const createServices = (baseUrl: string): Services => ({
todos: {
async list() {
const r = await fetch(`${baseUrl}/todos`);
return await r.json();
},
},
});
// app/ServicesProvider.tsx
import React, { createContext, useContext } from "react";
import type { Services } from "./services";
const Ctx = createContext<Services | null>(null);
export function ServicesProvider({
services,
children,
}: {
services: Services;
children: React.ReactNode;
}) {
return <Ctx.Provider value={services}>{children}</Ctx.Provider>;
}
export function useServices(): Services {
const s = useContext(Ctx);
if (!s) throw new Error("ServicesProvider missing");
return s;
}
// App.tsx
import React from "react";
import { ServicesProvider } from "./app/ServicesProvider";
import { createServices } from "./app/services";
import { TodosScreen } from "./screens/TodosScreen";
export default function App() {
const services = createServices("https://jsonplaceholder.typicode.com");
return (
<ServicesProvider services={services}>
<TodosScreen />
</ServicesProvider>
);
}
// screens/TodosScreen.tsx
import React, { useEffect, useState } from "react";
import { View, Text, ActivityIndicator, FlatList } from "react-native";
import { useServices } from "../app/ServicesProvider";
export function TodosScreen() {
const { todos } = useServices();
const [state, set] = useState<
| { kind: "loading" }
| { kind: "data"; items: string[] }
| { kind: "error"; msg: string }
>({ kind: "loading" });
useEffect(() => {
todos
.list()
.then((items) => set({ kind: "data", items }))
.catch((e) => set({ kind: "error", msg: e.message }));
}, [todos]);
if (state.kind === "loading") return <ActivityIndicator />;
if (state.kind === "error") return <Text>{state.msg}</Text>;
return (
<FlatList
data={state.items}
keyExtractor={(x, i) => String(i)}
renderItem={({ item }) => <Text>{item}</Text>}
/>
);
}
Notes
services
instance in tests (fake or mock).Paste into an Expo app (see sandboxes/react-native-expo):