How to use @edx/frontend-platform

 

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

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:

  1. appMessages (custom messages defined in the individual MFE).

  2. headerMessages (messages exported from @edx/frontend-component-header)

  3. footerMessages (messages exported from @edx/frontend-component-footer)

  4. 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:

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

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: https://openedx.github.io/frontend-platform/module-Config.html .

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

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:

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

    • 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:

         

    • 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 with mergeConfig for custom ConfigDocument properties. Requester is for informational/error reporting purposes only.

        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:

         

    • 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: 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.

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

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?)