Integration of Effect-ts with Tanstack Query. Run your Effects from Tanstack Query. Fully type-safe and compatible with Effect RPC and Effect HttpApi.
# Install the package
npm install effect-query
# Install peer dependencies (if not already installed)
npm install @tanstack/react-query effect
// src/utils/effect-query.ts
import { createEffectQuery } from "effect-query";
import { Layer } from "effect";
export const eq = createEffectQuery(Layer.empty);
// Alternative: Create from effect-query from ManagedRuntime instead of Layer
import { createEffectQueryFromManagedRuntime } from "effect-query";
import { Layer, ManagedRuntime } from "effect";
const managedRuntime = ManagedRuntime.make(Layer.empty);
export const eq = createEffectQueryFromManagedRuntime(managedRuntime);// src/pages/example.tsx
import { useQuery } from "@tanstack/react-query";
import { Effect } from "effect";
import { eq } from "./effect-query";
export const eq = createEffectQuery(Layer.empty);
export default function HomeRoute() {
const { data, status } = useQuery(
eq.queryOptions({
queryKey: ["namespace", "action"],
queryFn: () => Effect.succeed("Hello, world!"),
})
);
return (
<div>
{status === "pending" && <div>Loading...</div>}
{status === "success" && <div>{data}</div>}
</div>
);
}// src/pages/users.tsx
import { eq } from "./effectQuery";
import { useMutation } from "@tanstack/react-query";
// You can move this outside of the component and even share it with other components
const updateUserOptions = eq.mutationOptions({
mutationKey: "updateUserOptions"
mutationFn: () => Effect.gen(function* () {
const user = yield* Effect.sleep(1000);
yield* Console.log("Updating user...");
return Effect.succeed("User updated");
}),
});
function UpdateUserPage({ id }: { id: string }) {
const { mutate } = useMutation(updateUserOptions);
return <button onClick={() => mutate({ id })}>Update User</button>;
}When your Effect fails, the error object includes a match function that lets you handle different error types in a type-safe manner. When using match, the OrElse case is used as a catch-all for all the remaining unhandled failures and for defects. We are not able to fully check if defects can occur and therefore OrElse is required.
function Examle() {
const { data, status, error } = useQuery({
/** ... */
});
if (status === "error" && error) {
return error.match({
QueryError: (queryError) => <div>Query error: {queryError.hello}</div>,
TestError: (testError) => <div>Test error: {testError.message}</div>,
OrElse: (cause) => <div>Error: {Cause.pretty(cause)}</div>,
});
}
}Match can be also used to handle errors during mutations.
The same pattern works for mutations, allowing you to handle errors in callbacks:
export default function UpdateUserPage({ id }: { id: string }) {
const { mutate } = useMutation({
/*...*/
onError: (error) =>
error.match({
UserUpdateError: (userUpdateError) => {
alert(`${userUpdateError.message}`);
},
OrElse: (cause) => {
alert(`Error updating user: ${Cause.pretty(cause)}`);
},
}),
});
}// src/utils/effect-query.ts
import { createEffectQuery } from "effect-query";
import { Layer } from "effect";
import { HttpApiClient } from "@effect/http-api";
import { HttpApiSpec } from "./http-api-spec";
// Create your ApiClient service
export class ApiClient extends Effect.Service<ApiClient>()(
'example/ApiClient',
{
dependencies: [],
effect: HttpApiClient.make(HttpApiSpec, {
baseUrl: "https://api.example.com"
});
}
) {}
// Create a final layer for your Effect Query
export const LiveLayer = Layer.mergeAll(ApiClient.Default);
export const eq = createEffectQuery(LiveLayer);
// Use it in your components
export default function HomeRoute() {
const { data, status, error } = useQuery(
eq.queryOptions({
queryKey: ["example", "hello-world"],
queryFn: () => Effect.gen(function* () {
const apiClient = yield* ApiClient;
return yield* apiClient.hello.helloWorld({ /* ... */ });
}),
})
);
}
// src/utils/effect-query.ts
import { createEffectQuery } from "effect-query";
import { Layer } from "effect";
import { FetchHttpClient } from "@effect/platform";
import { RpcClient, RpcSerialization } from "@effect/rpc";
const API_DOMAIN = "https://api.example.com";
// Create RpcProtocol layer for your RPC client
export const RpcProtocolLive = RpcClient.layerProtocolHttp({
url: `${API_DOMAIN}/rpc`,
}).pipe(
Layer.provide([
// use fetch for http requests
FetchHttpClient.layer
// use ndjson for serialization
RpcSerialization.layerNdjson,
])
);
// Create your ApiClient service
export class MyRpcClient extends Effect.Service<MyRpcClient>()(
'example/MyRpcClient',
{
dependencies: [],
scoped: RpcClient.make(RpcGroups)
}
) {}
// Create a final layer for your Effect Query
export const LiveLayer = MyRpcClient.Default.pipe(
Layer.provideMerge(RpcProtocolLive)
);
export const eq = createEffectQuery(LiveLayer);
// Use it in your components
export default function HomeRoute() {
const { data, status, error } = useQuery(
eq.queryOptions({
queryKey: ["example", "hello-world"],
queryFn: () => Effect.gen(function* () {
const rpcClient = yield* MyRpcClient;
return yield* rpcClient.HelloWorld()
}),
})
);
}Made with ❤️ by Voidhash
