docs/docs/en/plugin-development/client/appendix/faq.md
This page collects common pitfalls when developing client plugins. If you run into a situation where "the code looks right but it just doesn't work," check here first.
Make sure you ran yarn pm create instead of manually creating the directory. yarn pm create not only generates files but also registers the plugin in the applicationPlugins database table. If you created the directory manually, run yarn nocobase upgrade to trigger a rescan.
Troubleshoot in this order:
yarn pm enable <pluginName>Ctrl+Shift+R is needed)Different file types have different hot reload behaviors:
| File Type | After Modification |
|---|---|
tsx/ts under src/client-v2/ | Auto hot reload, no action needed |
Translation files under src/locale/ | Restart the application |
New or modified collections under src/server/collections/ | Run yarn nocobase upgrade |
If client code changes don't hot reload, try refreshing the browser first.
NocoBase v2 routes automatically add a /v prefix. For example, if you registered path: '/hello', the actual URL is /v/hello:
this.router.add('hello', {
path: '/hello', // actual URL -> /v/hello
componentLoader: () => import('./pages/HelloPage'),
});
See Router for details.
If the settings menu item appears but the content is blank, it's usually one of two reasons:
Reason 1: Used componentLoader in v1 client
componentLoader is a client-v2 syntax. In v1 client, use Component to pass the component directly:
// ❌ v1 client does not support componentLoader
this.pluginSettingsManager.addPageTabItem({
menuKey: 'my-settings',
key: 'index',
componentLoader: () => import('./pages/MyPage'),
});
// ✅ v1 client uses Component
import MyPage from './pages/MyPage';
this.pluginSettingsManager.addPageTabItem({
menuKey: 'my-settings',
key: 'index',
Component: MyPage,
});
Reason 2: The page component is not exported with export default
componentLoader requires the module to have a default export. Missing default means the component can't be loaded.
Confirm you registered the model in load():
this.flowEngine.registerModelLoaders({
MyBlockModel: {
loader: () => import('./models/MyBlockModel'),
},
});
If you're using registerModels (non-lazy-loading approach), confirm the model is properly exported from models/index.ts.
Tables defined via defineCollection are server-internal tables and do not appear in the UI data source list by default.
Recommended approach: Add the corresponding data table in the NocoBase interface under "Data Source Management," configure the fields and interface types, and the table will automatically appear in the block's data source selection list.
If you do need to register it in plugin code (e.g., for demo scenarios in example plugins), you can manually register it via addCollection. See Building a Full-Stack Data Management Plugin for details. Note that you must register via the eventBus pattern — you cannot call it directly in load(), because ensureLoaded() will clear and re-set all collections after load().
Override static filterCollection on the model — only collections that return true will appear in the selection list:
export class MyBlockModel extends TableBlockModel {
static filterCollection(collection: Collection) {
return collection.name === 'myTable';
}
}
Troubleshoot in this order:
DisplayItemModel.bindModelToInterface('ModelName', ['input']), and the interface type matches — for example, input corresponds to single-line text fields, checkbox corresponds to checkboxesload() (registerModels or registerModelLoaders)define({ label })You forgot to call define({ label }) on the field model. Just add it:
MyFieldModel.define({
label: tExpr('My field'),
});
Also make sure the translation files under src/locale/ contain the corresponding key, otherwise the English text will be shown in Chinese environments.
Confirm the model has the correct static scene set:
| Value | Where it appears |
|---|---|
ActionSceneEnum.collection | Block top action bar (next to the "Create" button) |
ActionSceneEnum.record | Table row action column (next to "Edit" / "Delete") |
ActionSceneEnum.both | Both scenarios |
Confirm registerFlow's on is set to 'click':
MyActionModel.registerFlow({
key: 'myFlow',
on: 'click', // listen for button click
steps: {
doSomething: {
async handler(ctx) {
// your logic
},
},
},
});
:::warning Note
uiSchema in registerFlow is for the configuration panel (design-time UI), not the runtime dialog. If you want to open a form dialog when the button is clicked, use ctx.viewer.dialog() in the handler.
:::
Most common causes:
src/locale/ directory or files — requires an application restart to take effectctx.t() directly in a component — ctx.t() does not automatically inject the plugin's namespace. In components, use the useT() hook (imported from locale.ts)tExpr(), useT(), and this.t() in the wrong contextThese three translation methods are for different contexts. Using the wrong one will either cause errors or translations won't work:
| Method | Where to use | Description |
|---|---|---|
tExpr() | Static definitions like define(), registerFlow() | i18n isn't initialized at module load time, use deferred translation |
useT() | Inside React components | Returns a translation function bound to the plugin's namespace |
this.t() | In Plugin's load() | Automatically injects the plugin package name as namespace |
See i18n Internationalization for details.
Usually the server-side ACL is not configured. For example, if your collection is called todoItems, you need to allow the corresponding actions in the server plugin's load():
// Allow read only
this.app.acl.allow('todoItems', ['list', 'get'], 'loggedIn');
// Allow full CRUD
this.app.acl.allow('todoItems', ['list', 'get', 'create', 'update', 'destroy'], 'loggedIn');
'loggedIn' means any logged-in user can access it. Without acl.allow, only administrators can operate by default.
Troubleshoot in this order:
defineCollection, confirm the collection name is spelled correctlyresourceManager.define, confirm both the resource name and action name are correctresourceName:actionName, e.g., todoItems:list, externalApi:getyarn build --tar error "no paths specified to add to archive"When running yarn build <pluginName> --tar, you get:
TypeError: no paths specified to add to archive
However, running yarn build <pluginName> alone (without --tar) works fine.
This is usually because the plugin's .npmignore uses negation syntax (npm's ! prefix). When --tar packages the plugin, NocoBase reads each line of .npmignore and prepends ! to convert them into fast-glob exclusion patterns. If your .npmignore already uses negation syntax, like:
*
!dist
!package.json
After processing, it becomes ['!*', '!!dist', '!!package.json', '**/*']. The !* excludes all root-level files (including package.json), while !!dist is not recognized by fast-glob as "re-include dist" — the negation fails. If the dist/ directory happens to be empty or the build produced no output files, the collected file list ends up empty, and tar throws this error.
Solution: Don't use negation syntax in .npmignore. Instead, only list the directories to exclude:
/node_modules
/src
The packaging logic will convert these into exclusion patterns (!./node_modules, !./src) and add **/* to match all other files. This approach is simpler and avoids the negation processing issue.
The plugin works fine during local development, but fails to activate after uploading via the "Plugin Manager" to production. The log shows an error like:
TypeError: Cannot assign to read only property 'constructor' of object '[object Object]'
This is usually because the plugin bundled NocoBase's built-in dependencies into its own node_modules/. NocoBase's build system maintains an external list of packages (such as react, antd, axios, lodash, etc.) that are provided by the NocoBase host and should not be bundled into plugins. If a plugin carries its own private copy, it may conflict with the version already loaded by the host at runtime, causing various unexpected errors.
Why it works locally: During local development, the plugin is in the packages/plugins/ directory without a private node_modules/. Dependencies resolve to the already-loaded versions in the project root, so no conflicts occur.
Solution: Move all dependencies in the plugin's package.json to devDependencies — NocoBase's build system will automatically handle plugin dependencies:
{
- "dependencies": {
- "axios": "1.7.7"
- },
+ "devDependencies": {
+ "axios": "1.7.7"
+ },
}
Then rebuild and repackage. This way the plugin's dist/node_modules/ won't contain these packages, and the NocoBase host-provided versions will be used at runtime.
:::tip General Principle
NocoBase's build system maintains an external list of packages (such as react, antd, axios, lodash, etc.) that are provided by the NocoBase host — plugins should not bundle them. All plugin dependencies should be placed in devDependencies, and the build system will automatically determine which need to be bundled into dist/node_modules/ and which are provided by the host.
:::
/v prefix