Cell - React Component
The React frontend of a Cell application: pages declared with decorators, React Router routing, layouts and the application shell, all managed by the frontend DI container. Installing it adds the frontend build target (through @celljs/widget). Guide: Frontend and RPC.
npm install @celljs/react react
npm install -D @types/reactThe fullstack template of cell init is set up with it. cell dev serves the frontend with HMR on the backend's port; cell build writes it to .cell/output/public/.
Pages
Pages are the files of src/browser/pages/, each with the page component as default export: index.tsx is /, about.tsx is /about, users/[id].tsx is /users/:id, [...rest].tsx matches the rest of the path, _layout.tsx wraps the pages of its directory and below, and _error.tsx shows when one of them fails. Names starting with _ and tests are not pages. The toolchain (cell dev, cell build) generates the routes (.cell/generated/frontend/routes.js, listed by cell info --json); each page is a chunk loaded when it is visited. Guide: Frontend and RPC.
// src/browser/pages/users/[id].tsx
import { useParams } from '@celljs/react';
export default function User() {
return <h1>User {useParams().id}</h1>;
}Routing is React Router 7; Link, NavLink, Navigate, Outlet, useParams, useNavigate, useLocation, useSearchParams and useMatch are re-exported from here.
Packages, which have no pages directory, add pages and replace parts of the frontend with decorators on a class that their DI module imports:
| Decorator | |
|---|---|
@View(pathOrMetadata) / @Route(...) | A route. A string is the path; the metadata has path, index, component (default: the decorated class), layout, caseSensitive, priority |
@DefaultLayout(component?) | The layout around every route without its own layout (default: none) |
@App(component?) | Replaces the root component |
@Router(component?) | Replaces the router |
@Context(component?) | Adds a React context provider around the application |
Paths are resolved against react.path (default server.path, /).
Errors
A page that throws while rendering, or whose code does not load, does not blank the application: an ErrorBoundary inside its layout shows the fallback instead, and the page is tried again when the user goes to another path. The fallback is the default export of the nearest _error.tsx (in the directory of the page or above), which gets { error, reset } (ErrorFallbackProps); without one, DefaultErrorFallback shows the message and a button to try again. The shell puts another boundary around the whole application, for errors of layouts and of the fallbacks themselves. React logs the errors to the console.
// src/browser/pages/_error.tsx
import type { ErrorFallbackProps } from '@celljs/react';
export default function PageError({ error, reset }: ErrorFallbackProps) {
return <p>Could not show this page ({String(error)}). <button onClick={reset}>Retry</button></p>;
}ErrorBoundary (fallback, resetKey) is exported for parts of a page.
Services in components
| Hook | |
|---|---|
useInject<T>(id) | A component of the container, resolved once per component instance |
useValue<T>(key, default?) | A configuration value, like @Value(key) (the frontend only has public keys) |
useContainer() | The container itself |
useRpc<T>(id) | The proxy of a backend service (needs @celljs/rpc), like @Autorpc(id) |
useLocale() | [locale, setLocale]: the locale of widget.locales the user selected, and how to change it |
They use the container of ContainerContext, which the shell provides; there is no global container, so a component rendered outside the application fails with a message saying so. In a test, render the component inside <ContainerContext.Provider value={container}>; reactTestModule (@celljs/react/lib/browser/test/test-module) has the components of this package without the shell, which needs a DOM.
function Header() {
const userService = useInject<UserService>(UserService);
const title = useValue<string>('app.public.title', 'My app');
return <header>{title}: {userService.name}</header>;
}Configuration (public: the frontend receives it)
| Key | Default | |
|---|---|---|
react.router.type | browser | hash for hash-based URLs |
react.path | ${server.path ?? '/'} | Base path of the routes |
react.router.routes | Route metadata by path, merged over the decorator's: { '/about': { caseSensitive: true, priority: 2000 } } |
Frontend configuration is evaluated at build time and is visible to anyone who loads the page: never put secrets in it.
Widgets and slots
<Slot area="header" /> renders the widgets registered for an area, in order of priority; register one with @Widget({ area: 'header', component: Logo }) from @celljs/widget. Libraries use this to let applications add to their layouts without changing them.
Calling the backend
useRpc<T>(Id) returns a typed proxy of a backend service of @celljs/rpc; its methods return promises, so call them in an effect or an event handler. For other HTTP APIs, use fetch or RestOperations from @celljs/http.
function Greeting() {
const greetingServer = useRpc<GreetingServer>(GreetingServer);
const [text, setText] = React.useState('');
React.useEffect(() => { greetingServer.greet('web').then(setText); }, [greetingServer]);
return <p>{text}</p>;
}Translations with react-intl
react-intl is optional. To use it, install it and import @celljs/react/lib/browser/intl once in src/browser/module.ts: its IntlContext puts an IntlProvider with the messages of the selected locale around the application, so useIntl() and <FormattedMessage> work in every component.