@effectify/solid-query
@effectify/solid-query
Section titled “@effectify/solid-query”The @effectify/solid-query package provides seamless integration between Effect and TanStack Query for SolidJS applications. It enables you to use Effect’s powerful error handling and composability features while leveraging TanStack Query’s caching and synchronization capabilities, all within SolidJS’s reactive system.
Installation
Section titled “Installation”npm install @effectify/solid-query @tanstack/solid-query effect solid-jsBasic Usage
Section titled “Basic Usage”Simple Data Fetching with createQuery
Section titled “Simple Data Fetching with createQuery”import { createQuery } from "@tanstack/solid-query"
import { Effect } from "effect"
import { Show } from "solid-js"
// Define your Effect
const fetchUser = (id: number) =>
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then((res) => res.json()),
catch: (error) => new Error(`Failed to fetch user: ${error}`),
})
// Use in component
function UserProfile(props: { userId: number }) {
const userQuery = createQuery(() => ({
queryKey: ["user", props.userId],
queryFn: () => Effect.runPromise(fetchUser(props.userId)),
}))
return (
<Show when={userQuery.data} fallback={<div>Loading...</div>}>
{(user) => (
<div>
<h1>{user().name}</h1>
<Show when={userQuery.isError}>
<p>Error: {userQuery.error?.message}</p>
</Show>
</div>
)}
</Show>
)
}Using SolidJS Resources
Section titled “Using SolidJS Resources”For simpler use cases, you can use SolidJS’s built-in createResource:
import { createResource, Show } from "solid-js"
import { Effect } from "effect"
function UserProfile(props: { userId: number }) {
const [user] = createResource(
() => props.userId,
(id) => Effect.runPromise(fetchUser(id)),
)
return (
<Show when={user()} fallback={<div>Loading...</div>}>
{(userData) => (
<div>
<h1>{userData().name}</h1>
<p>{userData().email}</p>
</div>
)}
</Show>
)
}With Error Handling and Retry
Section titled “With Error Handling and Retry”import { Effect, pipe } from "effect"
const fetchUserWithRetry = (id: number) =>
pipe(
fetchUser(id),
Effect.retry({ times: 3, delay: "1 second" }),
Effect.catchAll((error) =>
Effect.succeed({
id,
name: "Unknown User",
email: "unknown@example.com",
})
),
)
function UserProfile(props: { userId: number }) {
const userQuery = createQuery(() => ({
queryKey: ["user", props.userId],
queryFn: () => Effect.runPromise(fetchUserWithRetry(props.userId)),
}))
return (
<Show when={userQuery.data}>
{(user) => <div>Hello, {user().name}!</div>}
</Show>
)
}Advanced Patterns
Section titled “Advanced Patterns”Reactive Queries with Signals
Section titled “Reactive Queries with Signals”Combine SolidJS signals with TanStack Query:
import { createMemo, createSignal } from "solid-js"
import { createQuery } from "@tanstack/solid-query"
function UserSearch() {
const [searchTerm, setSearchTerm] = createSignal("")
const searchQuery = createQuery(() => ({
queryKey: ["users", "search", searchTerm()],
queryFn: () => Effect.runPromise(searchUsers(searchTerm())),
enabled: searchTerm().length > 2,
}))
return (
<div>
<input
type="text"
value={searchTerm()}
onInput={(e) => setSearchTerm(e.currentTarget.value)}
placeholder="Search users..."
/>
<Show when={searchQuery.data}>
<For each={searchQuery.data}>
{(user) => <div>{user.name}</div>}
</For>
</Show>
</div>
)
}Combining Multiple Effects
Section titled “Combining Multiple Effects”const fetchUserWithPosts = (userId: number) =>
Effect.all([
fetchUser(userId),
fetchUserPosts(userId),
]).pipe(
Effect.map(([user, posts]) => ({ user, posts })),
)
function UserDashboard(props: { userId: number }) {
const dashboardQuery = createQuery(() => ({
queryKey: ["userDashboard", props.userId],
queryFn: () => Effect.runPromise(fetchUserWithPosts(props.userId)),
}))
return (
<Show when={dashboardQuery.data} fallback={<div>Loading dashboard...</div>}>
{(data) => (
<div>
<h1>{data().user.name}</h1>
<div>Posts: {data().posts.length}</div>
</div>
)}
</Show>
)
}Custom Error Types
Section titled “Custom Error Types”class UserNotFoundError {
readonly _tag = "UserNotFoundError"
constructor(readonly userId: number) {}
}
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly message: string) {}
}
const fetchUserTyped = (id: number) =>
Effect.tryPromise({
try: async () => {
const response = await fetch(`/api/users/${id}`)
if (response.status === 404) {
throw new UserNotFoundError(id)
}
if (!response.ok) {
throw new NetworkError(`HTTP ${response.status}`)
}
return response.json()
},
catch: (error) => {
if (error instanceof UserNotFoundError) return error
if (error instanceof NetworkError) return error
return new NetworkError(String(error))
},
})
function UserProfile(props: { userId: number }) {
const userQuery = createQuery(() => ({
queryKey: ["user", props.userId],
queryFn: () => Effect.runPromise(fetchUserTyped(props.userId)),
}))
return (
<Switch>
<Match when={userQuery.isLoading}>
<div>Loading...</div>
</Match>
<Match when={userQuery.error instanceof UserNotFoundError}>
<div>User {props.userId} not found</div>
</Match>
<Match when={userQuery.error instanceof NetworkError}>
<div>Network error: {userQuery.error.message}</div>
</Match>
<Match when={userQuery.data}>
{(user) => (
<div>
<h1>{user().name}</h1>
<p>{user().email}</p>
</div>
)}
</Match>
</Switch>
)
}Mutations with Effect
Section titled “Mutations with Effect”import { createMutation, useQueryClient } from "@tanstack/solid-query"
const updateUser = (id: number, data: Partial<User>) =>
Effect.tryPromise({
try: () =>
fetch(`/api/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res.json()),
catch: (error) => new Error(`Failed to update user: ${error}`),
})
function EditUserForm(props: { userId: number }) {
const queryClient = useQueryClient()
const updateMutation = createMutation(() => ({
mutationFn: (data: Partial<User>) => Effect.runPromise(updateUser(props.userId, data)),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user", props.userId] })
},
}))
const [formData, setFormData] = createSignal({ name: "", email: "" })
const handleSubmit = (e: Event) => {
e.preventDefault()
updateMutation.mutate(formData())
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData().name}
onInput={(e) =>
setFormData((prev) => ({
...prev,
name: e.currentTarget.value,
}))}
placeholder="Name"
/>
<input
type="email"
value={formData().email}
onInput={(e) =>
setFormData((prev) => ({
...prev,
email: e.currentTarget.value,
}))}
placeholder="Email"
/>
<button
type="submit"
disabled={updateMutation.isPending}
>
{updateMutation.isPending ? "Saving..." : "Save"}
</button>
</form>
)
}Reactive Patterns
Section titled “Reactive Patterns”Dependent Queries
Section titled “Dependent Queries”function UserWithPosts(props: { userId: number }) {
const userQuery = createQuery(() => ({
queryKey: ["user", props.userId],
queryFn: () => Effect.runPromise(fetchUser(props.userId)),
}))
const postsQuery = createQuery(() => ({
queryKey: ["posts", props.userId],
queryFn: () => Effect.runPromise(fetchUserPosts(props.userId)),
enabled: !!userQuery.data,
}))
return (
<div>
<Show when={userQuery.data}>
{(user) => <h1>{user().name}</h1>}
</Show>
<Show when={postsQuery.data}>
<For each={postsQuery.data}>
{(post) => <div>{post.title}</div>}
</For>
</Show>
</div>
)
}Infinite Queries
Section titled “Infinite Queries”import { createInfiniteQuery } from "@tanstack/solid-query"
const fetchPosts = (page: number) =>
Effect.tryPromise({
try: () => fetch(`/api/posts?page=${page}`).then((res) => res.json()),
catch: (error) => new Error(`Failed to fetch posts: ${error}`),
})
function PostList() {
const postsQuery = createInfiniteQuery(() => ({
queryKey: ["posts"],
queryFn: ({ pageParam = 1 }) => Effect.runPromise(fetchPosts(pageParam)),
getNextPageParam: (lastPage, pages) => lastPage.hasMore ? pages.length + 1 : undefined,
initialPageParam: 1,
}))
return (
<div>
<For each={postsQuery.data?.pages}>
{(page) => (
<For each={page.posts}>
{(post) => <div>{post.title}</div>}
</For>
)}
</For>
<Show when={postsQuery.hasNextPage}>
<button
onClick={() => postsQuery.fetchNextPage()}
disabled={postsQuery.isFetchingNextPage}
>
{postsQuery.isFetchingNextPage ? "Loading..." : "Load More"}
</button>
</Show>
</div>
)
}Best Practices
Section titled “Best Practices”1. Use Proper Query Keys
Section titled “1. Use Proper Query Keys”Create query key factories for consistency:
const userKeys = {
all: ["users"] as const,
lists: () => [...userKeys.all, "list"] as const,
list: (filters: string) => [...userKeys.lists(), { filters }] as const,
details: () => [...userKeys.all, "detail"] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
}
// Usage
const userQuery = createQuery(() => ({
queryKey: userKeys.detail(props.userId),
queryFn: () => Effect.runPromise(fetchUser(props.userId)),
}))2. Handle Loading and Error States
Section titled “2. Handle Loading and Error States”function UserProfile(props: { userId: number }) {
const userQuery = createQuery(() => ({
queryKey: ["user", props.userId],
queryFn: () => Effect.runPromise(fetchUser(props.userId)),
}))
return (
<Switch>
<Match when={userQuery.isLoading}>
<div>Loading user...</div>
</Match>
<Match when={userQuery.isError}>
<div>Error: {userQuery.error?.message}</div>
</Match>
<Match when={userQuery.data}>
{(user) => (
<div>
<h1>{user().name}</h1>
<p>{user().email}</p>
</div>
)}
</Match>
</Switch>
)
}3. Leverage Effect’s Composability
Section titled “3. Leverage Effect’s Composability”const fetchUserProfile = (id: number) =>
pipe(
fetchUser(id),
Effect.flatMap((user) =>
pipe(
fetchUserPreferences(user.id),
Effect.map((preferences) => ({ user, preferences })),
)
),
Effect.retry({ times: 2 }),
Effect.timeout("10 seconds"),
)Integration with SolidJS Ecosystem
Section titled “Integration with SolidJS Ecosystem”With SolidJS Router
Section titled “With SolidJS Router”import { useParams } from "@solidjs/router"
function UserPage() {
const params = useParams()
const userQuery = createQuery(() => ({
queryKey: ["user", params.id],
queryFn: () => Effect.runPromise(fetchUser(Number(params.id))),
}))
return (
<Show when={userQuery.data}>
{(user) => <UserProfile user={user()} />}
</Show>
)
}With SolidJS Store
Section titled “With SolidJS Store”import { createStore } from "solid-js/store"
function UserManager() {
const [store, setStore] = createStore({ selectedUserId: null })
const userQuery = createQuery(() => ({
queryKey: ["user", store.selectedUserId],
queryFn: () => Effect.runPromise(fetchUser(store.selectedUserId)),
enabled: !!store.selectedUserId,
}))
return (
<div>
<button onClick={() => setStore("selectedUserId", 1)}>
Load User 1
</button>
<Show when={userQuery.data}>
{(user) => <div>{user().name}</div>}
</Show>
</div>
)
}Examples
Section titled “Examples”Check out the complete implementation in:
API Reference
Section titled “API Reference”Effect Utilities
Section titled “Effect Utilities”Effect.runPromise()- Convert Effect to Promise for use with TanStack QueryEffect.retry()- Add retry logic to your EffectsEffect.timeout()- Add timeout handlingEffect.catchAll()- Handle all possible errorsEffect.all()- Combine multiple Effects
SolidJS Integration
Section titled “SolidJS Integration”- Works seamlessly with
createQuery,createMutation,createInfiniteQuery - Compatible with
createResourcefor simpler use cases - Integrates with SolidJS’s reactive system and signals
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”- Effect not executing: Ensure you’re calling
Effect.runPromise() - Reactivity not working: Make sure query keys are reactive (use functions)
- Type errors: Verify all peer dependencies are installed
- Stale data: Use proper query key invalidation strategies
Performance Tips
Section titled “Performance Tips”- Use
Effect.cached()for expensive computations - Implement proper query key strategies
- Leverage TanStack Query’s built-in optimizations
- Use SolidJS’s fine-grained reactivity effectively