Package Exports
- @well-prado/blok-react-sdk
Readme
@well-prado/blok-react-sdk
Type-safe React hooks and components for Blok Framework workflows with automatic code generation.
Overview
The @well-prado/blok-react-sdk
provides a complete React integration for Blok Framework applications. It includes type-safe hooks, context providers, and utilities that make it easy to interact with Blok workflows from React components.
Features
- 🎣 React Hooks - Custom hooks for workflow execution with caching and error handling
- 🔄 TanStack Query Integration - Built on React Query for optimal data fetching
- 🛡️ Type Safety - Full TypeScript support with auto-generated types
- 🎯 Context Providers - Easy state management across your app
- ⚡ Performance - Automatic caching, deduplication, and background updates
- 🔧 Extensible - Easy to customize and extend
Installation
npm install @well-prado/blok-react-sdk @tanstack/react-query
Peer Dependencies
This package requires the following peer dependencies:
react
: ^18.0.0 || ^19.0.0@tanstack/react-query
: ^5.0.0
Quick Start
1. Setup the Provider
Wrap your app with the BlokProvider
:
import { BlokProvider } from "@well-prado/blok-react-sdk";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<BlokProvider
config={{
baseURL: "http://localhost:4000",
timeout: 10000,
}}
>
<YourApp />
</BlokProvider>
</QueryClientProvider>
);
}
2. Use Workflows in Components
import {
useWorkflowQuery,
useWorkflowMutation,
} from "@well-prado/blok-react-sdk";
function UserProfile() {
// Query data (GET-like operations)
const { data: user, isLoading, error } = useWorkflowQuery("get-user-profile");
// Mutations (POST/PUT/DELETE-like operations)
const updateProfile = useWorkflowMutation("update-profile", {
onSuccess: () => {
console.log("Profile updated successfully!");
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Hello, {user?.name}</h1>
<button
onClick={() => updateProfile.mutate({ name: "New Name" })}
disabled={updateProfile.isPending}
>
Update Profile
</button>
</div>
);
}
API Reference
BlokProvider
The main provider component that sets up the Blok client and context.
interface BlokProviderProps {
config: BlokClientConfig;
children: React.ReactNode;
}
<BlokProvider
config={{
baseURL: "http://localhost:4000",
timeout: 15000,
withCredentials: true,
}}
>
{children}
</BlokProvider>;
Hooks
useWorkflowQuery
Execute workflows that fetch data (similar to GET requests).
const result = useWorkflowQuery(
workflowName: string,
input?: any,
options?: UseWorkflowQueryOptions
);
Parameters:
workflowName
: The name of the workflow to executeinput
: Optional input data for the workflowoptions
: React Query options + Blok-specific options
Returns:
data
: The workflow response dataisLoading
: Loading stateerror
: Error object if the request failedrefetch
: Function to manually refetch dataisSuccess
: Boolean indicating successful completion
Example:
// Simple query
const { data, isLoading } = useWorkflowQuery("get-users");
// With input parameters
const { data: user } = useWorkflowQuery("get-user-by-id", { id: "123" });
// With options
const { data, refetch } = useWorkflowQuery("get-dashboard-data", null, {
refetchInterval: 30000, // Refetch every 30 seconds
staleTime: 60000, // Data is fresh for 1 minute
enabled: isAuthenticated, // Only run when authenticated
});
useWorkflowMutation
Execute workflows that modify data (similar to POST/PUT/DELETE requests).
const mutation = useWorkflowMutation(
workflowName: string,
options?: UseWorkflowMutationOptions
);
Parameters:
workflowName
: The name of the workflow to executeoptions
: React Query mutation options + Blok-specific options
Returns:
mutate
: Function to trigger the mutationmutateAsync
: Async version of mutatedata
: The mutation response dataisPending
: Loading stateerror
: Error object if the mutation failedisSuccess
: Boolean indicating successful completionreset
: Function to reset mutation state
Example:
// Simple mutation
const createUser = useWorkflowMutation("create-user");
// With success/error handlers
const updateUser = useWorkflowMutation("update-user", {
onSuccess: (data) => {
console.log("User updated:", data);
// Invalidate and refetch user list
queryClient.invalidateQueries(["get-users"]);
},
onError: (error) => {
console.error("Update failed:", error);
},
});
// Usage in component
const handleSubmit = (userData) => {
updateUser.mutate(userData);
};
// Async usage
const handleAsyncSubmit = async (userData) => {
try {
const result = await updateUser.mutateAsync(userData);
console.log("Success:", result);
} catch (error) {
console.error("Error:", error);
}
};
useBlokClient
Access the underlying Blok client instance.
const client = useBlokClient();
// Use client directly
const handleCustomRequest = async () => {
const response = await client.executeWorkflow("custom-workflow");
console.log(response);
};
useWorkflows
Get information about available workflows (requires workflow discovery).
const { workflows, isLoading } = useWorkflows();
// Display available workflows
workflows?.forEach((workflow) => {
console.log(workflow.name, workflow.description);
});
Advanced Usage
Custom Query Keys
Customize how queries are cached:
const { data } = useWorkflowQuery(
"get-user-posts",
{ userId: "123" },
{
queryKey: ["user-posts", userId], // Custom cache key
staleTime: 5 * 60 * 1000, // 5 minutes
}
);
Optimistic Updates
Update UI immediately before server response:
const updatePost = useWorkflowMutation("update-post", {
onMutate: async (newPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries(["get-posts"]);
// Snapshot previous value
const previousPosts = queryClient.getQueryData(["get-posts"]);
// Optimistically update
queryClient.setQueryData(["get-posts"], (old) =>
old.map((post) => (post.id === newPost.id ? newPost : post))
);
return { previousPosts };
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(["get-posts"], context.previousPosts);
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries(["get-posts"]);
},
});
Dependent Queries
Execute queries that depend on other queries:
function UserProfile({ userId }) {
// First, get the user
const { data: user } = useWorkflowQuery("get-user", { id: userId });
// Then get user's posts (only when we have the user)
const { data: posts } = useWorkflowQuery(
"get-user-posts",
{ userId: user?.id },
{
enabled: !!user?.id, // Only run when user is loaded
}
);
return (
<div>
<h1>{user?.name}</h1>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
Infinite Queries
Handle paginated data:
import { useInfiniteWorkflowQuery } from "@well-prado/blok-react-sdk";
function PostList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteWorkflowQuery(
"get-posts-paginated",
({ pageParam = 1 }) => ({ page: pageParam, limit: 10 }),
{
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
},
}
);
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button
onClick={fetchNextPage}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
</div>
);
}
Error Handling
Global Error Handling
import { BlokProvider, BlokErrorBoundary } from "@well-prado/blok-react-sdk";
function App() {
return (
<BlokProvider config={config}>
<BlokErrorBoundary
fallback={({ error, retry }) => (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={retry}>Try Again</button>
</div>
)}
>
<YourApp />
</BlokErrorBoundary>
</BlokProvider>
);
}
Component-Level Error Handling
function UserComponent() {
const { data, error, isError } = useWorkflowQuery("get-user");
if (isError) {
return (
<div className="error">
<h3>Failed to load user</h3>
<p>{error.message}</p>
{error.code === "UNAUTHORIZED" && (
<button onClick={() => (window.location.href = "/login")}>
Please log in
</button>
)}
</div>
);
}
return <div>{data?.name}</div>;
}
TypeScript Integration
Auto-Generated Types
When using with @well-prado/blok-codegen
, you get fully typed workflows:
// Types are automatically generated from your workflows
import { useWorkflowQuery } from "./blok-types/hooks";
function UserProfile() {
// TypeScript knows the exact shape of the response
const { data } = useWorkflowQuery("getUserProfile"); // ✅ Typed
return <h1>{data.user.name}</h1>; // ✅ Type safe
}
Manual Type Definitions
For custom typing without codegen:
interface User {
id: string;
name: string;
email: string;
}
interface GetUserResponse {
user: User;
}
const { data } = useWorkflowQuery<GetUserResponse>("get-user");
// data is now typed as GetUserResponse
Performance Optimization
Query Invalidation
Efficiently update cache when data changes:
import { useQueryClient } from "@tanstack/react-query";
function useUserActions() {
const queryClient = useQueryClient();
const deleteUser = useWorkflowMutation("delete-user", {
onSuccess: (data, variables) => {
// Remove from user list
queryClient.invalidateQueries(["get-users"]);
// Remove individual user query
queryClient.removeQueries(["get-user", variables.id]);
},
});
return { deleteUser };
}
Background Updates
Keep data fresh automatically:
const { data } = useWorkflowQuery("get-notifications", null, {
refetchInterval: 30000, // Check every 30 seconds
refetchIntervalInBackground: true, // Even when tab is not active
staleTime: 60000, // Consider fresh for 1 minute
});
Testing
Mock Workflows for Testing
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BlokProvider } from "@well-prado/blok-react-sdk";
import { render, screen } from "@testing-library/react";
// Create test wrapper
function TestWrapper({ children }) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return (
<QueryClientProvider client={queryClient}>
<BlokProvider config={{ baseURL: "http://test-api" }}>
{children}
</BlokProvider>
</QueryClientProvider>
);
}
// Test component
test("displays user name", async () => {
// Mock the workflow response
const mockUser = { id: "1", name: "John Doe" };
// Setup your test...
render(<UserProfile />, { wrapper: TestWrapper });
expect(await screen.findByText("John Doe")).toBeInTheDocument();
});
Best Practices
1. Query Key Consistency
Use consistent query keys across your app:
// ✅ Good - Consistent structure
const QUERY_KEYS = {
users: {
all: ["users"],
detail: (id) => ["users", id],
posts: (id) => ["users", id, "posts"],
},
};
const { data } = useWorkflowQuery(
"get-user",
{ id },
{
queryKey: QUERY_KEYS.users.detail(id),
}
);
2. Separate Data Fetching Logic
Create custom hooks for complex data operations:
// hooks/useUserData.ts
export function useUserData(userId) {
const user = useWorkflowQuery("get-user", { id: userId });
const posts = useWorkflowQuery(
"get-user-posts",
{ userId },
{
enabled: !!userId,
}
);
return {
user: user.data,
posts: posts.data,
isLoading: user.isLoading || posts.isLoading,
error: user.error || posts.error,
};
}
// Component
function UserProfile({ userId }) {
const { user, posts, isLoading } = useUserData(userId);
if (isLoading) return <Loading />;
return (
<div>
<h1>{user.name}</h1>
{posts.map((post) => (
<Post key={post.id} {...post} />
))}
</div>
);
}
3. Error Boundaries
Use error boundaries to catch and handle errors gracefully:
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<BlokProvider config={config}>
<YourApp />
</BlokProvider>
</ErrorBoundary>
);
}
Migration Guide
From REST API to Blok Workflows
// Before (REST API)
const [user, setUser] = useState(null);
useEffect(() => {
fetch("/api/users/123")
.then((res) => res.json())
.then(setUser);
}, []);
// After (Blok SDK)
const { data: user } = useWorkflowQuery("get-user", { id: "123" });
From Manual State Management
// Before (Manual state)
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const addUser = async (userData) => {
setLoading(true);
try {
const newUser = await api.createUser(userData);
setUsers((prev) => [...prev, newUser]);
} finally {
setLoading(false);
}
};
// After (Blok SDK)
const { data: users } = useWorkflowQuery("get-users");
const addUser = useWorkflowMutation("create-user", {
onSuccess: () => {
queryClient.invalidateQueries(["get-users"]);
},
});
Troubleshooting
Common Issues
Provider Not Found Error
Error: useBlokClient must be used within a BlokProvider
Solution: Wrap your app with
BlokProvider
.Query Not Updating
- Check if you're invalidating queries after mutations
- Verify query keys are consistent
- Ensure
enabled
option isn't preventing execution
Type Errors
- Run
npm run blok:codegen
to regenerate types - Check that workflow names match exactly
- Verify input/output types match workflow definitions
- Run
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
Related Packages
@well-prado/blok-front-sdk-core
- Core utilities and client@well-prado/blok-codegen
- TypeScript code generation@well-prado/blok-admin-dashboard
- Complete backend solution@well-prado/blok-cli
- CLI for project management