A shared React component library for Open edX authoring/FBR frontends. It provides
reusable, tree-shakeable, TypeScript-typed UI components built on top of
Paragon so that multiple Open edX MFEs
(frontend-app-authoring, frontend-app-fbr-admin, etc.) can share a single
implementation instead of duplicating components across repos.
- Overview
- Prerequisites
- Installation
- Usage
- Development
- Using the Library in Open edX MFEs
- Tutor Development Setup
- Local Development Workflow
- Publishing
- Troubleshooting
- Contributing
This package compiles TypeScript/TSX source under src/ into a CommonJS dist/
build with accompanying .d.ts declaration files. It is consumed as an npm
dependency by Open edX MFEs and is not a standalone application — it has no
dev server, no routes, and no bundling of its own. Consumers bring their own
React, react-dom, @openedx/paragon and the other libraries listed under
Peer dependencies (declared as peerDependencies so
only one copy of each ends up in the final app bundle).
| Area | Exports |
|---|---|
| Program calendar | ProgramCalendar (props programKey, basePath), ProgramCalendarProvider, useProgramCalendar, buildRosterPath, buildAuditPath, CalendarPdfDownloadButton |
| Shared components | AuditLogTable, DatepickerControl, SearchableSelect, MobileRowCardsList / MobileRowCard / MobileRowField, SectionHeading, RequestStatusBadge, ScopeBadge, InstructingBadge, RequestDetailCell, RequestSummary, SessionLeavesPanel, UserIdentity, DebouncedSearch, certificate components |
| Data | FBR API functions (sessions, programs, holidays, locations, requests, audit logs, /config/) and TanStack Query hooks for them (useCalendarSessions, useConfig, useCreateSession, ...), plus fbrKeys query keys |
| Utils | Date and format helpers, FBR constants (USER_ROLE, REQUEST_STATUS, ...), useModalParams, useIsBelowLg |
Components that fetch data need a TanStack QueryClientProvider, a
react-router v6 router and an IntlProvider (the MFE's AppProvider) above
them. Every library query sets its own staleTime, so the host's QueryClient
defaults don't make library data stale. All library query keys start with
'fbr'.
- Node.js and npm matching the versions used by the target Open edX MFE
(check that MFE's
.nvmrc/package.json engines). This library relies on@openedx/frontend-build, which pins the toolchain (Babel, ESLint, Jest, TypeScript) used across Open edX frontends. - Git with SSH access to
git@github.com:edly-io/frontend-component-fbr.gitif you plan to install from GitHub or clone the repo for local development. - A working copy of the consuming Open edX MFE (
frontend-app-authoring,frontend-app-fbr-admin, or any other Open edX MFE) if you're developing against it locally.
npm install @edly-io/frontend-component-fbrPeer dependencies must already be present in the consuming project:
npm install @openedx/paragon@^23.5.0 react@^18.3.1 react-dom@^18.3.1 \
@edx/frontend-platform@^8.4.0 react-router-dom@^6.30.1 @tanstack/react-query@^5.90.5 \
react-datepicker@^4.25.0 date-fns@^2.30.0 @react-pdf/renderer@^4.9.0 classnames@^2.3.2react-datepicker must be a 4.x release. Its CSS
(react-datepicker/dist/react-datepicker.css) is imported by the host app.
@react-pdf/renderer is only loaded when a user downloads the calendar PDF.
Useful for testing a branch or commit before it's published:
npm install git+ssh://git@github.com/edly-io/frontend-component-fbr.git#<branch-or-tag>Note that consumers only get the compiled dist/ output if it's committed, or
if the package defines a prepare script that builds on install. See
Troubleshooting if you hit a "missing dist" error with this
method.
For active local development against a sibling checkout:
npm install file:../frontend-component-fbrThis adds an entry like "@edly-io/frontend-component-fbr": "file:../frontend-component-fbr"
to the consumer's package.json. See Local Development Workflow
for the tradeoffs of this approach versus npm link and npm pack.
Useful for testing exactly what will ship to npm, without publishing:
cd frontend-component-fbr
npm run build
npm pack
# produces edly-io-frontend-component-fbr-1.0.0.tgz
cd ../<your-mfe-repo> # e.g. frontend-app-authoring, frontend-app-fbr-admin, or any other Open edX MFE
npm install ../frontend-component-fbr/edly-io-frontend-component-fbr-1.0.0.tgzAll public components and types are re-exported from the package root, so consumers never need deep imports:
import { UserIdentity } from '@edly-io/frontend-component-fbr';import React from 'react';
import { UserIdentity } from '@edly-io/frontend-component-fbr';
const TraineeRow = () => (
<UserIdentity
name="Jane Doe"
badges={['Instructor']}
size="default"
showAvatar
enableHoverCard
/>
);
export default TraineeRow;Types are shipped alongside the compiled JS (dist/index.d.ts), so no
@types/* package is needed. Prop types are exported for consumers who want
to type their own wrapper components:
import { UserIdentity, UserIdentityProps } from '@edly-io/frontend-component-fbr';
const buildProps = (overrides: Partial<UserIdentityProps>): UserIdentityProps => ({
name: 'Unnamed user',
badges: [],
size: 'default',
...overrides,
});
const CompactUser: React.FC<{ name: string }> = ({ name }) => (
<UserIdentity {...buildProps({ name, size: 'compact' })} />
);Clone the repo and install dependencies:
git clone git@github.com:edly-io/frontend-component-fbr.git
cd frontend-component-fbr
npm installAvailable scripts:
| Script | Command | Purpose |
|---|---|---|
npm run build |
Babel-compiles src/ to dist/, then generates .d.ts files |
Produces the publishable output |
npm run build:types |
tsc --emitDeclarationOnly --project tsconfig.build.json |
Generates only the .d.ts files into dist/ |
npm test |
fedx-scripts jest --coverage |
Runs the unit test suite with coverage |
npm run test:ci |
fedx-scripts jest --silent --coverage |
CI-friendly variant of the test run |
npm run lint |
fedx-scripts eslint . |
Lints src/ against the Open edX ESLint config |
npm run lint:fix |
fedx-scripts eslint --fix . |
Lints and auto-fixes what it can |
npm run types |
tsc --noEmit |
Type-checks the whole project without emitting files |
Before opening a pull request, run all of the above — npm run build will
catch issues the others won't (e.g. Babel --ignore patterns, .d.ts output
paths), and vice versa.
Once installed (see Installation), import components exactly
as you would from any other npm package — no MFE-specific configuration is
required for the published package. Because react, react-dom, and
@openedx/paragon are peer dependencies, the MFE's own copies are used at
runtime; this library never bundles its own React.
The nuance only shows up when you consume the package via a file: link (see
Local Development Workflow) — that's covered
separately since it introduces webpack/Docker considerations that don't apply
to a normal npm install.
Open edX MFEs are typically run inside Tutor
containers. If you're developing frontend-component-fbr alongside an MFE
running under Tutor, the container needs to see both repos.
mkdir -p ~/workspace/edx && cd ~/workspace/edx
git clone git@github.com:edly-io/frontend-component-fbr.git
git clone git@github.com:<org>/<your-mfe-repo>.git # e.g. frontend-app-authoring, frontend-app-fbr-admin, or any other Open edX MFEcd <your-mfe-repo>
npm install file:../frontend-component-fbrTutor only bind-mounts what you explicitly tell it to. The MFE repo is
typically already mounted; the library repo needs an explicit mount so it
lands at a real path inside the container (a file: link that resolves
outside the container's mounted paths will fail with Module not found; see
Troubleshooting):
tutor mounts add <mfe-service>,<mfe-service>-dev:/absolute/path/to/frontend-component-fbr:/openedx/frontend-component-fbrExplicit mount syntax is service[,service...]:/host/path:/container/path.
Replace <mfe-service> with whichever Tutor service(s) actually run the dev
server for your MFE (e.g. authoring,authoring-dev for
frontend-app-authoring — check tutor mounts list for the service names
already in use in your setup). Verify the mount:
tutor mounts listRestart the affected containers so the new mount takes effect:
tutor dev restart <mfe-service>- Edits to files already inside
dist/(i.e., already-built output) are picked up by the MFE's dev server automatically, the same as any othernode_moduleschange — webpack dev server watchesfile:-linked packages like any other dependency. - Edits to
src/*.tsxin the library are not picked up automatically, because the MFE imports compileddist/output, not source. You must rebuild the library for changes to propagate.
Run this in the frontend-component-fbr repo after any source change:
npm run buildFor a tighter loop, keep a terminal open running the build on save (e.g. via
nodemon or an editor task) rather than re-running npm run build by hand
after every edit.
See the Troubleshooting section below — the "Module not found" and "Tutor not picking up changes" entries apply directly here.
There are three common ways to consume an unpublished, in-progress version of this library from another local repo. They trade off convenience against correctness:
| Approach | How | Pros | Cons |
|---|---|---|---|
Local path (file:) |
npm install file:../frontend-component-fbr |
Simple; no extra commands; works with Docker mounts (see above) | Symlinks its own node_modules unless deduped — can cause duplicate React instances (see Troubleshooting); requires a manual rebuild after every change |
npm link |
npm link in the library, then npm link @edly-io/frontend-component-fbr in the consumer |
No package.json changes needed in the consumer; easy to unlink |
Same duplicate-package/symlink pitfalls as file:; global link state is easy to forget about and can cause "works on my machine" surprises |
npm pack + install tarball |
npm pack in the library, then npm install ../frontend-component-fbr/*.tgz in the consumer |
Closest simulation of a real npm install — no symlinks, no duplicate-package issues | Slowest loop: requires npm pack + npm install after every change, not just a rebuild |
Recommendation: use the file: approach for day-to-day development (fastest
loop, and works inside Tutor once mounted correctly), but validate with an
npm pack install before publishing a release — it will catch packaging bugs
(e.g. missing files, wrong main/types paths) that file: linking hides.
If you use file: or npm link and hit duplicate-React errors, see
Troubleshooting.
FBR apps install this library from GitHub and pin an exact release tag in
package.json, e.g. "@edly-io/frontend-component-fbr": "github:edly-io/frontend-component-fbr#v2.0.0".
A release is:
- In the feature PR, bump the version following semver, without creating a tag:
npm version <major|minor|patch> --no-git-tag-version. Use a major bump when apps must change something to upgrade, for example a new or narrower peer dependency. - Merge the PR to
master. - Tag the merge commit and push the tag:
git checkout master && git pull && git tag v<version> && git push origin v<version>. Optionally create a GitHub Release from the tag with a short changelog. - In each app, pin the new tag (
npm install github:edly-io/frontend-component-fbr#v<version>) in its own PR.
Always bump the version for a release. webpack (dev server and builds) treats a
package in node_modules as unchanged while its version is unchanged, so a new
build under the same version can be served stale.
-
Bump the version following semver:
npm version patch # or minor / major -
Build:
npm run build
-
Verify the package contents before publishing anything:
npm pack --dry-run
Confirm the output includes
dist/(per thefilesfield inpackage.json) and thatdist/index.jsanddist/index.d.tsexist at the top level — not nested underdist/src/. -
Publish:
npm publish --access public
(
publishConfig.accessis already set topublicinpackage.json, but passing--access publicexplicitly avoids surprises for scoped packages like@edly-io/frontend-component-fbron your first publish.) -
Install the published version in a consumer to confirm it resolves correctly:
npm install @edly-io/frontend-component-fbr@latest
Module not found: Can't resolve '@edly-io/frontend-component-fbr'
Usually means a file:-linked package's symlink target isn't visible inside
the environment resolving it. Inside Tutor/Docker, this happens when the
library repo lives outside any path bind-mounted into the container — the
symlink resolves to a host path the container can't see. Fix by explicitly
mounting the library repo (see Tutor Development Setup).
"Missing dist" / cannot find module after installing from GitHub or a file: link
The published npm package includes a prebuilt dist/ (per the files field),
but a GitHub install or a fresh file: link installs from source and does
not run a build automatically unless a prepare script exists. Run
npm run build inside the library repo yourself after linking it, and after
every subsequent source change.
Missing prepare script warnings / build not happening automatically
This package intentionally does not define a prepare script, so installing
via git+ssh://... or file: will not auto-build. This is deliberate — an
auto-build on every npm install would slow down installs in every consumer.
Always run npm run build manually after cloning or linking.
Peer dependency warnings (react, react-dom, @openedx/paragon)
These are declared as peerDependencies, not dependencies, so npm expects
the consumer to already provide compatible versions. A warning (not an error)
means versions are close but not exact — check the ranges in package.json
against what the consuming MFE has installed. An error means the consumer is
missing one of them entirely; install it there.
TypeError: Cannot read properties of null (reading 'useId') / "Invalid hook call"
This means two separate copies of React (or @openedx/paragon) ended up in
the same app — typically because a file:-linked package resolves to its own
nested node_modules/react instead of the consumer's copy. Webpack follows
symlinks to their real path by default, which defeats normal deduplication.
Fix in the consuming MFE's webpack.dev.config.js:
resolve: {
alias: {
react: path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
'@openedx/paragon': path.resolve(__dirname, 'node_modules/@openedx/paragon'),
},
symlinks: false,
},Setting symlinks: false keeps webpack from re-resolving relative to the
symlink's real path, and the explicit alias entries force a single shared
copy of React/Paragon regardless of what the linked package ships internally.
Tutor not picking up changes to the library Two separate causes, often confused:
- You edited
src/but didn't rebuild — the MFE only ever seesdist/. Runnpm run build. - You rebuilt, but the container's webpack dev server didn't notice —
restart it (
tutor dev restart <service>), since file-watching across a bind mount sometimes misses rapid rebuilds depending on your host OS's filesystem event support.
Docker cache issues
If dependency or build behavior seems stale even after a rebuild, the image
layer cache may be reusing an old node_modules. Rebuild the image without
cache:
tutor images build openedx-dev --no-cacheWebpack resolution issues beyond React/Paragon
If other packages also get duplicated through the same symlink mechanism,
extend the alias/symlinks fix above to those packages too, rather than
disabling symlinks resolution only partially.
TypeScript declaration (.d.ts) issues — missing, or nested under dist/src/
npm run build:types uses tsconfig.build.json, which sets an explicit
rootDir/outDir and narrows include to src/**/* only. If declarations
reappear under dist/src/... or stray .d.ts files show up for root config
files (babel.config.d.ts, jest.config.d.ts, etc.), something has
regressed that config — verify build:types in package.json still points
at tsconfig.build.json and hasn't reverted to compiling against the root
tsconfig.json (which is intentionally broader, for editor/lint use).
-
Create a new folder under
src/components/<ComponentName>/containing:<ComponentName>.tsx— the implementationtypes.ts— prop types and any component-local types<ComponentName>.test.tsx— unit tests (seeUserIdentity.test.tsxfor the expected coverage: rendering, required/optional props, interactions, callbacks, conditional rendering, accessibility attributes, edge cases; prefer behavioral assertions over snapshots)index.ts— re-exports the component and its types- an accompanying
.scssfile, if the component needs component-scoped styles
-
Export it from
src/index.tsso it's part of the package's public API:export * from './components/<ComponentName>';
-
Run the full check before opening a PR:
npm run types && npm run lint && npm test && npm run build
-
Update this README if the change affects usage, installation, or the project structure — in particular, add the component to the Usage section if it introduces a new usage pattern.
- Keep components tree-shakeable: avoid side effects at module scope, and
export named exports rather than relying solely on a default export from
src/index.ts. - Type props explicitly in
types.tsrather than inline in the component file — this keeps prop types importable by consumers (see the TypeScript usage example above). - Match the testing stack and conventions already used in this repo (Jest,
React Testing Library,
@testing-library/user-event,jest-dom) — don't introduce new testing libraries without discussion, since versions here are intentionally kept in sync withfrontend-app-authoring.