The purpose of this document is to walk through the core uses of the @edx/frontend-platform
repository. This repository houses a number of core utilities for working in frontend javascript code in the openedx ecosystem.
The repo is available here and contains pretty good high level documentation in its README, as well as a link to automatically generated jsdocs.
APP_READY
(init)
When configuring your app, wrap the initial app code in a subscription to the APP_READY
event. This waits until the top-level initialization sequence has finished before rendering the jsx.
APP_INIT_ERROR
(error)
Alongside your app code, you should provide an error presentation, by default based on the provided ErrorPage component.
subscribe(APP_INIT_ERROR, (error) => { ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root')); }); |
AppProvider
Your top-level component for your component should be wrapped in an AppProvider
with an optional redux store param.
subscribe(APP_READY, () => { ReactDOM.render( <AppProvider store={myReduxReducer}> <HelloWorld /> </AppProvider> ) }); |
This will provide the following to HelloWorld:
Wraps component in an error boundary which will catch any internal errors and display the ErrorPage
component
Generates and provides an AppContext
provider for React context data.
Provides internationalization support through IntlProvider
from @edx/frontend-platform/i18n
.
Optionally a redux Provider
. Will only be included if a store
property is passed to AppProvider
.
A Router
for react-router.
Page Routes
Each “Page” within your app that you want trackable separately should be wrapped in a unique <PageRoute>
or <AuthenticatedPageRoute>
component (depending on the authentication requirements of the page). This will trigger the analytics sendPageEvent
action for that page.
You should call initialize
at the top level of your app to invoke the application initialization sequence.
As part of this, you should pass in i18n messages to initialize the translation service. The general pattern of messages to add is:
appMessages
(custom messages defined in the individual MFE).
headerMessages
(messages exported from @edx/frontend-component-header
)
footerMessages
(messages exported from @edx/frontend-component-footer
)
paragonMessages
(messages exported from @edx/paragon
).
initialize({ handlers: { config: () => { mergeConfig({ ...custom config overrides }), } }, messages: [ appMessages, headerMessages, footerMessages, paragonMessages, ], }); |
This will run through a number of lifecycle phases, during which related services will be configured.
options:
When working with openedx API endpoints that require authenticated user clients, you should use the provided client from @edx/frontend-platform/auth
.
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; const getUsername= () => { const { username } = getAuthenticatedUser(); return username; }; const myPostRequest = getAuthenticatedHttpClient().post(...); const myGetRequest = getAuthenticatedHttpClient().get(...); |
As a note, if you are using @tanstack/react-query
for your api calls, you can still use this interface to supply authentication for that client. An example can be found here.
The system uses the following default services for logging and analytics:
Logging: NewRelicLoggingService. https://openedx.github.io/frontend-platform/module-Logging.NewRelicLoggingService.html
Analytics: SegmentAnalyticsService. https://openedx.github.io/frontend-platform/module-Analytics.SegmentAnalyticsService.html
As long as you call initialize
at the top level of your app and wrap your pages in PageRoute
or AuthenticatedPageRoute
components, you should get page
and identify
messages sent through the analytics service automatically.
Note: Segment configuration does rely on having a SEGMENT_KEY configured for the repo (generally empty locally and overridden in edx-internal
.
From there you can send a variety of types of messages/signals:
Logging messages @edx/frontend-platform/logging
logInfo(infoStringOrErrorObject, customAttributes?)
Logs a message to the 'info' log level. Can accept custom attributes as a property of the error object, or as an optional second parameter.
logError(errorStringOrObject, customAttributes?)
Logs a message to the 'error' log level. Can accept custom attributes as a property of the error object, or as an optional second parameter.
Analytics messages
sendTrackEvent(eventName, properties)
Sends a track event to Segment and downstream
sendTrackingLogEvent(eventName properties)
Logs events to tracking log and downstream
The generated Context object from the AppRoute
component wrapper is accessible from anywhere descendant from that component, and in all the ways a normal react Context would be accessible.
The default shape of this object is { authenticatedUser, config }
, where config
is drawn from environment variables and app-level overrides by @edx/frontend-platform/config
’s getConfig
method.
default config fields defined here: https://openedx.github.io/frontend-platform/module-Config.html .
Core Libraries:
Note: The JSDoc utilities are more helpful here as API docs than as guides, given the complexity/breadth of these libraries
Analytics @edx/frontend-platform/analtytics
https://openedx.github.io/frontend-platform/module-Auth.html
Logging @edx/frontend-platform/logging
https://openedx.github.io/frontend-platform/module-Logging.html
Authenticated API Client @edx/frontend-platform/auth
https://openedx.github.io/frontend-platform/module-Auth.html
Internationalization (i18n) @edx/frontend-platform/i18n
https://openedx.github.io/frontend-platform/module-Internationalization.html
React @edx/frontend-platform/react
https://openedx.github.io/frontend-platform/module-React.html
Root exports
From utils: - https://openedx.github.io/frontend-platform/module-Utilities.html
modifyObjectKeys
This is the underlying function used by camelCaseObject, snakeCaseObject, and convertKeyNames above.
Given an object (or array) and a modification function, will perform the function on each key it encounters on the object and its tree of children.
The modification function must take a string as an argument and returns a string.
Example:
(key) => { if (key === 'edX') { return 'Open edX'; } return key; } |
This function will turn any key that matches 'edX' into 'Open edX'. All other keys will be passed through unmodified.
Can accept arrays as well as objects, and will perform its conversion on any objects it finds in the array.
camelCaseObject
Performs a deep conversion to camelCase on all keys in the provided object and its tree of children. Uses lodash.camelcase on each key. This is commonly used to convert snake_case keys in models from a backend server into camelCase keys for use in the JavaScript client.
Can accept arrays as well as objects, and will perform its conversion on any objects it finds in the array.
snakeCaseObject
Performs a deep conversion to snake_case on all keys in the provided object and its tree of children. Uses lodash.snakecase on each key. This is commonly used to convert camelCase keys from the JavaScript app into snake_case keys expected by backend servers.
Can accept arrays as well as objects, and will perform its conversion on any objects it finds in the array.
convertKeyNames
Given a map of key-value pairs, performs a deep conversion key names in the specified object from the key to the value. This is useful for updating names in an API request to the names used throughout a client application if they happen to differ. It can also be used in the reverse - formatting names from the client application to names expected by an API.
import { convertKeyNames } from '@edx/frontend-base'; // This object can be of any shape or depth with subobjects/arrays. const myObject = { myKey: 'my value', } const result = convertKeyNames(myObject, { myKey: 'their_key' }); console.log(result) // { their_key: 'my value' } |
Can accept arrays as well as objects, and will perform its conversion on any objects it finds in the array.
ensureDefinedConfig
This function helps catch a certain class of misconfiguration in which configuration variables are not properly defined and/or supplied to a consumer that requires them. Any key that exists is still set to "undefined" indicates a misconfiguration further up in the application, and should be flagged as an error, and is logged to 'warn'.
Keys that are intended to be falsy should be defined using null, 0, false, etc.
from initialization - https://openedx.github.io/frontend-platform/module-Initialization.html
initialize(options)
-
Invokes the application initialization sequence (as described above)
history
A browser history or memory history object created by the history package. Applications are encouraged to use this history object, rather than creating their own, as behavior may be undefined when managing history via multiple mechanisms/instances. Note that in environments where browser history may be inaccessible due to window
being undefined, this falls back to memory history.
from pubsub - https://openedx.github.io/frontend-platform/module-PubSub.html
publish(type, data)
subscribe(type, callback)
→ tokenString
unsubscribe(tokenString)
from config - https://openedx.github.io/frontend-platform/module-Config.html
getConfig()
Getter for the application configuration document. This is synchronous and merely returns a reference to an existing object, and is thus safe to call as often as desired, as long as it is being called from within the application render lifecycle (otherwise the keys will not be populated).
import { getConfig } from '@edx/frontend-platform'; const { LMS_BASE_URL, } = getConfig(); |
setConfig(newConfig)
Replaces the existing ConfigDocument. This is not commonly used, but can be helpful for tests.
The supplied config document will be tested with ensureDefinedConfig
to ensure it does not have any undefined
keys.
Example:
import { setConfig } from '@edx/frontend-platform'; setConfig({ LMS_BASE_URL, // This is overriding the ENTIRE document - this is not merged in! }); |
mergeConfig(newConfig)
Merges additional configuration values into the ConfigDocument returned by getConfig
. Will override any values that exist with the same keys.
mergeConfig({ NEW_KEY: 'new value', OTHER_NEW_KEY: 'other new value', }); |
If any of the key values are `undefined`, an error will be logged to 'warn'.
ensureConfig(keys, requester?)
A method allowing application code to indicate that particular ConfigDocument keys are required for them to function. This is useful for diagnosing development/deployment issues, primarily, by surfacing misconfigurations early. For instance, if the build process fails to supply an environment variable on the command-line, it's possible that one of the process.env
variables will be undefined. Should be used in conjunction with mergeConfig
for custom ConfigDocument
properties. Requester is for informational/error reporting purposes only.
ensureConfig(['LMS_BASE_URL', 'LOGIN_URL'], 'MySpecialComponent'); // Will log a warning with: // "App configuration error: LOGIN_URL is required by MySpecialComponent." // if LOGIN_URL is undefined, for example. |
NOTE: ensureConfig
waits until APP_CONFIG_INITIALIZED
is published to verify the existence of the specified properties. This means that this function is compatible with custom config
phase handlers responsible for loading additional configuration data in the initialization sequence.
from testing - https://openedx.github.io/frontend-platform/module-Testing.html
initializeMockApp
Initializes a mock application for component testing. The mock application includes mock analytics, auth, and logging services, and the real i18n service.
See MockAnalyticsService
, MockAuthService
, and MockLoggingService
for mock implementation details. For the most part, the analytics and logging services just implement their functions with jest.fn() and do nothing else, whereas the MockAuthService
actually has some behavior implemented, it just doesn't make any HTTP calls.
Note: this mock application is not sufficient for testing the full application lifecycle or initialization callbacks/custom handlers as described in the 'initialize' function's documentation. It exists merely to set up the mock services that components themselves tend to interact with most often. It could be extended to allow for setting up custom handlers fairly easily, as this functionality would be more-or-less identical to what the real initialize function does.
Example:
import { initializeMockApp } from '@edx/frontend-platform/testing'; import { logInfo } from '@edx/frontend-platform/logging'; describe('initializeMockApp', () => { it('mocks things correctly', () => { const { loggingService } = initializeMockApp(); logInfo('test', {}); expect(loggingService.logInfo).toHaveBeenCalledWith('test', {}); }); }); |
mockMessages
An empty messages object suitable for fulfilling the i18n service's contract.
@edx/frontend-platform/auth
Simplifies the process of making authenticated API requests to backend edX services by providing common authN/authZ client code that enables the login/logout flow and handles ensuring the presence of a valid JWT cookie.
Note: The documentation for AxiosJwtAuthService is nearly the same as that for the top-level auth interface, except that it contains some Axios-specific details.
Generally, you will configure your api connection at the top level of your app in your initialize
call.
Note: The JSDocs lay out a potential pattern for directly configuring an authenticated client if you are not using module-level initialize
, but it is recommended to use the top-level configuration wherever possible.
getAuthenticatedUser()
If it exists, returns the user data representing the currently authenticated user. If the user is anonymous, returns null.
Note: Generally you should just get this from AppContext
import { AppContext } from '@edx/frontend-platform/react' const { authenticatedUser } = React.useContext(AppContext); |
getAuthenticatedHttpClient({ useCache?: <bool> })
returns an axios HTTP client: https://axios-http.com/docs/api_intro
Gets the authenticated HTTP client for the service.
useCache
option indicates whether to use front end caching for all requests made with the returned client.
getHttpClient({ useCache?: <bool> })
returns an axios HTTP client: https://axios-http.com/docs/api_intro
Gets the unauthenticated HTTP client for the service.
useCache
option indicates whether to use front end caching for all requests made with the returned client.
getLoginRedirectUrl(redirectUrl)
Builds a URL to the login page with a post-login redirect URL attached as a query parameter.
const url = getLoginRedirectUrl('http://localhost/mypage'); console.log(url); // http://localhost/login?next=http%3A%2F%2Flocalhost%2Fmypage |
getLogoutRedirectUrl(redirectUrl)
Builds a URL to the logout page with a post-logout redirect URL attached as a query parameter.
const url = getLogoutRedirectUrl('http://localhost/mypage'); console.log(url); // http://localhost/logout?redirect_url=http%3A%2F%2Flocalhost%2Fmypage |
redirectToLogin(redirectUrl)
Redirects the user to the login page, and then the given redirectUrl after logging in
redirectToLogout(redirectUrl)
Redirects the user to the logout page, and then the given redirectUrl after logging out
The React module provides a variety of React components, hooks, and contexts for use in an application.
App-Level Context
AppContext
provides data from App
in a way that React components can readily consume, even if it's mutable data. AppContext
contains the following data structure:
{ authenticatedUser: <THE App.authenticatedUser OBJECT>, config: <THE App.config OBJECT> } |
If the App.authenticatedUser
or App.config
data changes, AppContext
will be updated accordingly and pass those changes onto React components using the context.
AppContext
is used in a React application like any other React Context
App Provider
<AppProvider store? />
A wrapper component for React-based micro-frontends to initialize a number of common data/ context providers.
subscribe(APP_READY, () => { ReactDOM.render( <AppProvider> <HelloWorld /> </AppProvider> ) }); |
This will provide the following to HelloWorld:
An error boundary as described above.
An AppContext
provider for React context data.
IntlProvider
for @edx/frontend-platform/i18n
internationalization
Optionally a redux Provider
. Will only be included if a store
property is passed to AppProvider
.
A Router
for react-router.
Login Redirect
<LoginRedirect />
A React component that, when rendered, redirects to the login page as a side effect. Uses redirectToLogin
to perform the redirect.
Page Routes
<PageRoute redirectUrl />
A react-router Route component that calls sendPageEvent
when it becomes active.
<AuthenticatedPageRoute redirectUrl />
A react-router route that redirects to the login page when the route becomes active and the user is not authenticated. If the application has been initialized with requireAuthenticatedUser
false, an AuthenticatedPageRoute
can be used to protect a subset of the application's routes, rather than the entire application.
It can optionally accept an override URL to redirect to instead of the login page.
Like a PageRoute
, also calls sendPageEvent
when the route becomes active.
Error Page
<ErrorPage />
An error page that displays a generic message for unexpected errors. Also contains a "Try Again" button to refresh the page.
@edx/frontend-platform/i18n
The i18n module relies on react-intl
and re-exports all of that package's exports.
For each locale we want to support, react-intl
needs 1) the locale-data, which includes information about how to format numbers, handle plurals, etc., and 2) the translations, as an object holding message id / translated string pairs. A locale string and the messages object are passed into the IntlProvider
element that wraps your element hierarchy.
Note that react-intl
has no way of checking if the translations you give it actually have anything to do with the locale you pass it; it will happily use whatever messages object you pass in. However, if the locale data for the locale you passed into the IntlProvider
was not correctly installed with addLocaleData
, all of your translations will fall back to the default (in our case English), even if you gave IntlProvider
the correct messages object for that locale.
configure({ loggingService, config, messages })
Configures the i18n library with messages for your application.
Note: Logs a warning if it detects a locale it doesn't expect (as defined by the supportedLocales list above), or if an expected locale is not provided.
Provider
<IntlProvider />
Note: This is provided by the top-level AppProvider component.
Components
Cleanest implementation, but requires importing different component format type, and not all components from react-intl
have made their way into the library yet.
const myMessageObject = { id: 'my.message', defaultMessage: 'My Default Message', description: 'A simple example', } const MyComponent = () => (<> <FormattedMessage {...myMessageObject} /> <FormattedDate value={new Date()} /> </>); |
Available formatting components:
Imperative API (Hooks)
useIntl()
hook forwards from react-intl
. Relies on being contained within an IntlProvider
Usage:
const { formatMessage, formatDate, ... } = useIntl(); const myMessageObject = { id: 'my.message', defaultMessage: 'My Default Message', description: 'A simple example', } const myTranslatedMessage = formatMessage(myMessageObject); const myTranslatedDate = formatDate(new Date()); |
Available formatters:
handleRtl()
Handles applying the RTL stylesheet and "dir=rtl" attribute to the html tag if the current locale is a RTL language.
isRtl(locale)
Determines if the provided locale is a right-to-left language.
@edx/frontend-platform/analytics
Contains a shared interface for tracking events. Has a default implementation of SegmentAnalyticsService, which supports Segment and the Tracking Log API (hosted in LMS).
Generally, you will configure your analytics interface at the top level of your app in your initialize
call.
Note: The JSDocs lay out a potential pattern for directly configuring an analytics interface if you are not using module-level initialize, but it is recommended to use the top-level configuration wherever possible.
Sends Identify call to Segment
NOTE: This field is generally called automatically by the top-level initialize
call
identifyAnonymousUser(traits)
→ Promise
identifyAuthenticatedUser(userId, traits)
Sends a page event to Segment and downstream
NOTE: This field is generally set automatically by the PageRoute
and AuthenticatedPageRoute
components provided in @edx/frontend-platform/react
sendPageEvent(category, name, properties)
Tracking Event - Sends a track event to Segment and downstream
sendTrackEvent(eventName, properties)
Track Logging Event - Logs events to tracking log and downstream
sendTrackingLogEvent(eventName properties)
Contains a shared interface for logging information (The default implementation is in NewRelicLoggingService.js
). When in development mode, all messages will instead be sent to the console.
Generally, you will configure your logging interface at the top level of your app in your initialize
call.
Note: The JSDocs lay out a potential pattern for directly configuring an logging interface if you are not using module-level initialize, but it is recommended to use the top-level configuration wherever possible.
Info Log
Logs a message to the 'info' log level. Can accept custom attributes as a property of the error object, or as an optional second parameter.
logInfo(infoStringOrErrorObject, customAttributes?)
Error Log
Logs a message to the 'error' log level. Can accept custom attributes as a property of the error object, or as an optional second parameter.
logError(errorStringOrObject, customAttributes?)