Docs
Advanced Usage
Structured Activity

Structured Activity

A Structured Activity separates an activity into four distinct concerns: content, layout, loading state, and error handling. This makes it easy to apply code splitting, Suspense-based loading, and error boundaries — without wiring them up manually.

Basic Usage

Use structuredActivityComponent() instead of a plain React component when registering an activity.

Article.tsx
import { structuredActivityComponent } from "@stackflow/react";
 
declare module "@stackflow/config" {
  interface Register {
    Article: {
      articleId: number;
      title?: string;
    };
  }
}
 
export const Article = structuredActivityComponent<"Article">({
  content: ArticleContent,
});

Then register it in stackflow() the same way as a regular component:

stackflow.ts
import { stackflow } from "@stackflow/react";
import { config } from "./stackflow.config";
import { Article } from "./Article";
 
export const { Stack } = stackflow({
  config,
  components: {
    Article,
  },
  plugins: [...],
});

Code Splitting

Pass an async import as content to code-split the activity. Stackflow pauses stack state updates while the bundle loads, then resumes once it's ready — so transitions always feel correct.

Article.tsx
export const Article = structuredActivityComponent<"Article">({
  content: () => import("./Article.content"),
});

Article.content.tsx exports a content() helper:

Article.content.tsx
import { content, useActivityParams } from "@stackflow/react";
 
const ArticleContent = content<"Article">(() => {
  const { title } = useActivityParams<"Article">();
 
  return (
    <div>
      <h1>{title}</h1>
    </div>
  );
});
 
export default ArticleContent;
💡

useActivityParams() reads the current activity's params. It's a convenient alternative to receiving params as a prop inside content().

Loading State

Provide a loading component to show while the content bundle or loader data is being fetched. It renders as the Suspense fallback.

Article.loading.tsx
import { loading } from "@stackflow/react";
 
const ArticleLoading = loading<"Article">(() => {
  return <div>Loading...</div>;
});
 
export default ArticleLoading;
Article.tsx
import { structuredActivityComponent } from "@stackflow/react";
import ArticleLoading from "./Article.loading";
 
export const Article = structuredActivityComponent<"Article">({
  content: () => import("./Article.content"),
  loading: ArticleLoading,
});

Layout

Provide a layout component to wrap the content. It receives params and children, making it easy to build consistent app bars or shell UIs that are available immediately — even while content is still loading.

Article.layout.tsx
import { layout } from "@stackflow/react";
import { AppScreen } from "@stackflow/plugin-basic-ui";
 
const ArticleLayout = layout<"Article">(({ params: { title }, children }) => {
  return (
    <AppScreen appBar={{ title }}>
      {children}
    </AppScreen>
  );
});
 
export default ArticleLayout;
Article.tsx
import { structuredActivityComponent } from "@stackflow/react";
import ArticleLayout from "./Article.layout";
import ArticleLoading from "./Article.loading";
 
export const Article = structuredActivityComponent<"Article">({
  content: () => import("./Article.content"),
  layout: ArticleLayout,
  loading: ArticleLoading,
});

The render order is: Layout wraps ErrorHandler wraps Suspense(Loading) wraps Content.

Error Handling

Provide an errorHandler component to show when content throws. It receives the error and a reset() function to retry.

Article.tsx
import { structuredActivityComponent, errorHandler } from "@stackflow/react";
import ArticleLayout from "./Article.layout";
import ArticleLoading from "./Article.loading";
 
const ArticleError = errorHandler<"Article">(({ error, reset }) => {
  return (
    <div>
      <p>Something went wrong.</p>
      <button onClick={reset}>Retry</button>
    </div>
  );
});
 
export const Article = structuredActivityComponent<"Article">({
  content: () => import("./Article.content"),
  layout: ArticleLayout,
  loading: ArticleLoading,
  errorHandler: ArticleError,
});

If you need a custom error boundary implementation (e.g. to integrate with an error reporting service), pass it via the boundary option:

import { errorHandler } from "@stackflow/react";
import type { CustomErrorBoundary } from "@stackflow/react";
 
const MyErrorBoundary: CustomErrorBoundary = ({ children, renderFallback }) => {
  // your custom boundary logic
};
 
const ArticleError = errorHandler<"Article">(
  ({ error, reset }) => <div>...</div>,
  { boundary: MyErrorBoundary },
);

With Loader API

Structured activities work seamlessly with the Loader API. Define the loader in stackflow.config.ts and use useLoaderData() inside content().

Article.loader.ts
import type { ActivityLoaderArgs } from "@stackflow/config";
 
export async function articleLoader({ params }: ActivityLoaderArgs<"Article">) {
  const data = await fetchArticle(params.articleId);
  return { data };
}
stackflow.config.ts
import { defineConfig } from "@stackflow/config";
import { articleLoader } from "./Article.loader";
 
export const config = defineConfig({
  activities: [
    {
      name: "Article",
      route: "/articles/:articleId",
      loader: articleLoader,
    },
  ],
  transitionDuration: 350,
});
Article.content.tsx
import { content, useActivityParams, useLoaderData } from "@stackflow/react";
import type { articleLoader } from "./Article.loader";
 
const ArticleContent = content<"Article">(() => {
  const { title } = useActivityParams<"Article">();
  const { data } = useLoaderData<typeof articleLoader>();
 
  return (
    <div>
      <h1>{title}</h1>
      {/* use data */}
    </div>
  );
});
 
export default ArticleContent;

Recommended File Structure

Co-locating the pieces by activity keeps things easy to navigate:

activities/
└── Article/
    ├── Article.tsx          # structuredActivityComponent definition
    ├── Article.content.tsx  # content()
    ├── Article.layout.tsx   # layout()
    ├── Article.loading.tsx  # loading()
    └── Article.loader.ts    # loader

API Reference

structuredActivityComponent<ActivityName>(options)

OptionTypeDescription
contentContent | (() => Promise<{ default: Content }>)Main content component, or an async import for code splitting
layoutLayout (optional)Wraps the content and loading/error states
loadingLoading (optional)Rendered as the Suspense fallback
errorHandlerErrorHandler (optional)Rendered when content throws

Helper Functions

FunctionDescription
content<ActivityName>(component)Creates a content descriptor
layout<ActivityName>(component)Creates a layout descriptor; receives params and children
loading<ActivityName>(component)Creates a loading descriptor; receives params
errorHandler<ActivityName>(component, options?)Creates an error handler descriptor; receives params, error, reset