How to use @edx/frontend-platform
- 1 How should I use it?
- 2 What is in it? (Deeper Dive)
- 2.1 Authentication
- 2.1.1 Initialization
- 2.1.2 Axios Utilities
- 2.1.3 Login/Logout Utilities
- 2.2 React
- 2.2.1 Utilities
- 2.2.2 Components
- 2.3 Internationalization
- 2.3.1 Initialization
- 2.3.2 General translation
- 2.3.3 RTL Utilities
- 2.4 Analytics
- 2.4.1 Initialization
- 2.4.2 Utilities
- 2.4.3 Identify Event
- 2.4.4 PageEvent
- 2.5 Logging
- 2.5.1 Initialization
- 2.5.2 Utilities
- 2.1 Authentication
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.
How should I use it?
App setup/Configuration
App-State Subscription
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'));
});
React Component Wrappers
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
componentGenerates 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 astore
property is passed toAppProvider
.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.
Service Initialization
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:
Authenticated network requests and user info
When working with openedx API endpoints that require authenticated user clients, you should use the provided client from @edx/frontend-platform/auth
.
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.
Logging
The system uses the following default services for logging and analytics:
Logging: NewRelicLoggingService. JSDoc: Class: NewRelicLoggingService
Analytics: SegmentAnalyticsService. JSDoc: Class: SegmentAnalyticsService
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 downstreamsendTrackingLogEvent(eventName properties)
Logs events to tracking log and downstream
Consume AppContext (for environment config or user object)
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: JSDoc: Config .
What is in it? (Deeper Dive)
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
JSDoc: AuthLogging
@edx/frontend-platform/logging
JSDoc: LoggingAuthenticated API Client
@edx/frontend-platform/auth
JSDoc: AuthInternationalization (i18n)
@edx/frontend-platform/i18n
JSDoc: InternationalizationReact
@edx/frontend-platform/react
JSDoc: React
Root exports
From utils: - JSDoc: Utilities
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:
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.
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 - JSDoc: Initialization
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 - JSDoc: PubSub
publish(type, data)
subscribe(type, callback)
→tokenString
unsubscribe(tokenString)
from config - JSDoc: Config
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).
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 anyundefined
keys.Example:
mergeConfig(newConfig)
Merges additional configuration values into the ConfigDocument returned by
getConfig
. Will override any values that exist with the same keys.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 withmergeConfig
for customConfigDocument
properties. Requester is for informational/error reporting purposes only.NOTE:
ensureConfig
waits untilAPP_CONFIG_INITIALIZED
is published to verify the existence of the specified properties. This means that this function is compatible with customconfig
phase handlers responsible for loading additional configuration data in the initialization sequence.
from testing - JSDoc: Testing
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
, andMockLoggingService
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 theMockAuthService
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:
mockMessages
An empty messages object suitable for fulfilling the i18n service's contract.
Authentication
@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.
Initialization
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.
Axios Utilities
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
getAuthenticatedHttpClient({ useCache?: <bool> })
returns an axios HTTP client: Axios API | Axios Docs
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: Axios API | Axios Docs
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.
Login/Logout Utilities
getLoginRedirectUrl(redirectUrl)
Builds a URL to the login page with a post-login redirect URL attached as a query parameter.
getLogoutRedirectUrl(redirectUrl)
Builds a URL to the logout page with a post-logout redirect URL attached as a query parameter.
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
React
The React module provides a variety of React components, hooks, and contexts for use in an application.
Utilities
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:
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
Components
App Provider
<AppProvider store? />
A wrapper component for React-based micro-frontends to initialize a number of common data/ context providers.
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
internationalizationOptionally a redux
Provider
. Will only be included if astore
property is passed toAppProvider
.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.
Internationalization
@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.
Initialization
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.
General translation
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.
Available formatting components:
Imperative API (Hooks)
useIntl()
hook forwards from react-intl
. Relies on being contained within an IntlProvider
Usage:
Available formatters:
RTL Utilities
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.
Analytics
@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).
Initialization
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.
Utilities
Identify Event
Sends Identify call to Segment
NOTE: This field is generally called automatically by the top-level initialize
call
identifyAnonymousUser(traits)
→ Promise
identifyAuthenticatedUser(userId, traits)
PageEvent
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)
Logging
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.
Initialization
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.
Utilities
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?)