Skip to content

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.

bash
npm install @celljs/react react
npm install -D @types/react

The 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.

tsx
// 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.

tsx
// 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.

tsx
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) ​

KeyDefault
react.router.typebrowserhash for hash-based URLs
react.path${server.path ?? '/'}Base path of the routes
react.router.routesRoute 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.

tsx
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.