Component Development
In NocoBase, page components mounted on routes are ordinary React components. You can write them directly with React + Antd — no different from regular frontend development.
NocoBase additionally provides:
observable + observer — The recommended state management approach, better suited for the NocoBase ecosystem than useState
useFlowContext() — Access NocoBase context capabilities (making requests, internationalization, route navigation, etc.)
Basic Usage
A minimal page component:
// pages/HelloPage.tsx
export default function HelloPage() {
return <h1>Hello, NocoBase!</h1>;
}
After writing the component, register it in the plugin's load() with this.router.add(). See Router for details.
State Management: observable
NocoBase recommends using observable + observer to manage component state instead of React's useState. The benefits are:
- Directly modifying object properties triggers updates — no need for
setState
- Automatic dependency tracking — components only re-render when the properties they use change
- Consistent with NocoBase's underlying reactive mechanism (FlowModel, FlowContext, etc.)
Basic usage: create a reactive object with observable.deep() and wrap the component with observer(). Both observable and observer are imported from @nocobase/flow-engine:
import React from 'react';
import { Input } from 'antd';
import { observable, observer } from '@nocobase/flow-engine';
// Create a reactive state object
const state = observable.deep({
text: '',
});
// Wrap the component with observer so it auto-updates when state changes
const DemoPage = observer(() => {
return (
<div>
<Input
placeholder="Type something..."
value={state.text}
onChange={(e) => {
state.text = e.target.value;
}}
/>
{state.text && <div style={{ marginTop: 8 }}>You typed: {state.text}</div>}
</div>
);
});
export default DemoPage;
Live preview:
import { Application, Plugin } from '@nocobase/client-v2';
class MyPlugin extends Plugin {
async load() {
this.router.add('root', {
path: '/',
componentLoader: () => import('@docs/en/plugin-development/client/component/_demos/observable-basic-page'),
});
}
}
// The code below is only to make this demo run independently in the documentation. You don't need to worry about app instantiation when developing plugins.
const app = new Application({
// The code example uses a memory router. In actual projects, you don't need to worry about this — app instantiation is handled internally by NocoBase.
router: { type: 'memory', initialEntries: ['/'] },
plugins: [MyPlugin],
});
export default app.getRootComponent();
For more usage, see Observable Reactive Mechanism.
Using useFlowContext
useFlowContext() is the entry point for accessing NocoBase capabilities. Import it from @nocobase/flow-engine — it returns a ctx object:
import { useFlowContext } from '@nocobase/flow-engine';
export default function MyPage() {
const ctx = useFlowContext();
// ctx.api — make requests
// ctx.t — internationalization
// ctx.router — route navigation
// ctx.logger — logging
// ...
}
Below are examples of commonly used capabilities.
Making Requests
Use ctx.api.request() to call backend APIs, with the same usage as Axios:
const response = await ctx.api.request({
url: 'users:list',
method: 'get',
});
console.log(response.data);
Internationalization
Use ctx.t() to get translated text:
const label = ctx.t('Hello');
// Specify namespace
const msg = ctx.t('Save success', { ns: '@my-project/plugin-hello' });
Route Navigation
Use ctx.router.navigate() for page navigation:
ctx.router.navigate('/some-page'); // -> /v2/some-page
Get current route parameters:
// e.g., route defined as /users/:id
const { id } = ctx.route.params; // get dynamic parameter
Get current route name:
const { name } = ctx.route; // get route name
For more log levels and usage, see Context - Common Capabilities.
Complete Example
Combining observable, useFlowContext, and Antd — a page component that fetches data from the backend and displays it:
// pages/PostListPage.tsx
import React, { useEffect } from 'react';
import { Button, Card, List, Spin } from 'antd';
import { observable, observer, FlowContext, useFlowContext } from '@nocobase/flow-engine';
interface Post {
id: number;
title: string;
}
// Use observable to manage page state
const state = observable.deep({
posts: [] as Post[],
loading: true,
});
const PostListPage = observer(() => {
const ctx = useFlowContext();
useEffect(() => {
loadPosts(ctx);
}, []);
return (
<Card title={ctx.t('Post list')}>
<Spin spinning={state.loading}>
<List
dataSource={state.posts}
renderItem={(post: Post) => (
<List.Item
actions={[
<Button danger onClick={() => handleDelete(ctx, post.id)}>
{ctx.t('Delete')}
</Button>,
]}
>
{post.title}
</List.Item>
)}
/>
</Spin>
</Card>
);
});
async function loadPosts(ctx: FlowContext) {
state.loading = true;
try {
const response = await ctx.api.request({
url: 'posts:list',
method: 'get',
});
state.posts = response.data?.data || [];
} catch (error) {
ctx.logger.error('Failed to load post list', { error });
} finally {
state.loading = false;
}
}
async function handleDelete(ctx: FlowContext, id: number) {
await ctx.api.request({
url: `posts:destroy/${id}`,
method: 'post',
});
loadPosts(ctx); // Refresh the list
}
export default PostListPage;
What's Next
- Full capabilities provided by
useFlowContext — see Context
- Component styles and theme customization — see Styles & Themes
- If your component needs to appear in NocoBase's "Add Block / Field / Action" menus and support visual configuration by users, you need to wrap it with FlowModel — see FlowEngine
- Not sure whether to use Component or FlowModel? — See Component vs FlowModel