JSPM

@well-prado/blok-react-sdk

1.0.5
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 7
  • Score
    100M100P100Q80179F
  • License MIT

Type-safe React hooks and components for Blok Framework workflows with automatic code generation

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.

npm version License: MIT

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 execute
  • input: Optional input data for the workflow
  • options: React Query options + Blok-specific options

Returns:

  • data: The workflow response data
  • isLoading: Loading state
  • error: Error object if the request failed
  • refetch: Function to manually refetch data
  • isSuccess: 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 execute
  • options: React Query mutation options + Blok-specific options

Returns:

  • mutate: Function to trigger the mutation
  • mutateAsync: Async version of mutate
  • data: The mutation response data
  • isPending: Loading state
  • error: Error object if the mutation failed
  • isSuccess: Boolean indicating successful completion
  • reset: 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

  1. Provider Not Found Error

    Error: useBlokClient must be used within a BlokProvider

    Solution: Wrap your app with BlokProvider.

  2. Query Not Updating

    • Check if you're invalidating queries after mutations
    • Verify query keys are consistent
    • Ensure enabled option isn't preventing execution
  3. Type Errors

    • Run npm run blok:codegen to regenerate types
    • Check that workflow names match exactly
    • Verify input/output types match workflow definitions

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support