Ever dreamed of injecting a beautiful React component across all the pages on a SharePoint site? With the observer pattern, this is easily achievable.

Table of Contents

Introduction

Integrating custom React components into SharePoint’s standard UI can be challenging. It may work in some areas, but not all, for example, when you have to deal with dynamic page elements like the ‘shy’ header that appears upon scrolling. In this blog post, I will show how to use the observer pattern to effectively inject a React element into the SharePoint header. I will also show how to handle SharePoint’s ‘shy’ mode to ensure your injected elements remain visible.

Using the Observer Pattern to Inject React Elements

The observer pattern is a software design pattern in which your observer can monitor any DOM state changes and get notified. In the context of SharePoint and React, we can use the MutationObserver API, which is part of the DOM (Document Object Model) Standard defined by the WHATWG (Web Hypertext Application Technology Working Group) . This API allows us to watch for changes in the DOM and inject our React component when the target element becomes available.

By using the MutationObserver API—widely supported in modern browsers—we can efficiently monitor the DOM without resorting to less efficient methods like polling.

Injecting the MenuWidget Component

Our goal is to inject a MenuWidget React component into the SharePoint header. We’ll observe the DOM for the presence of specific elements and render our component once they’re available.

Simplified Code Snippet:

private observeForElement = (): Promise<Element> => {
  return new Promise((resolve) => {
    let observer: MutationObserver | null = null;
    observer = new MutationObserver(() => {
      const divHook = document.querySelector('*[data-automationid="SiteHeaderFollowButton"]');
      // Other selectors...
      const parent = divHook?.parentElement?.parentElement;
      if (parent) {
        if (observer) observer.disconnect(); // Stop observing once found
        resolve(parent); // Resolve with the found parent
      }
    });
    const targetNode = document.querySelector('*[data-automationid="SiteHeader"]');
    if (targetNode) {
      observer.observe(targetNode, { childList: true, subtree: true });
    } else {
      throw new Error("Target node 'SiteHeader' not found");
    }
  });
};

In the code above:

  • We create a new Promise that resolves when the target element SiteHeaderFollowButton is found.
  • A MutationObserver watches for changes within the SiteHeader div.
  • Once the target element is found, we disconnect the observer and resolve the promise with the parent element.
  • We then render our MenuWidget component into this parent element.

Visualizing the Injection Process

%%{init: {'theme': 'base', 'themeVariables': { 'background': '#CFA63D', 'primaryColor': '#CFA63D', 'primaryTextColor': '#0aa8a7', 'primaryBorderColor': '#c7a42b', 'secondaryColor': '#e6e6e6', 'secondaryTextColor': '#333333', 'secondaryBorderColor': '#999999', 'lineColor': '#e74c3c', 'fontFamily': 'ui-sans-serif,system-ui,-apple-system,BlinkMacSystemfont,sans-serif', 'fontSize': '14px', 'actorBorderColor': '#2980b9', 'actorBkgColor': '#ffcc00', 'actorTextColor': '#225f78', 'labelBoxBkgColor': '#ffffff', 'labelBoxBorderColor': '#cccccc', 'labelTextColor': '#000000', 'noteBkgColor': '#E5E4E2', 'noteTextColor': '#225f78', 'noteBorderColor': '#bbbab8', 'sequenceNumberColor': '#e67e22', 'taskBkgColor': '#3498db', 'taskBorderColor': '#2980b9', 'taskTextColor': '#ffffff' }}}%% sequenceDiagram participant SPFx as SPFx Extension participant Observer as MutationObserver participant DOM as SharePoint DOM participant ReactComponent as MenuWidget SPFx->>Observer: Start observing SiteHeader activate Observer DOM-->>Observer: SiteHeader changes Observer-->>SPFx: Target element found deactivate Observer SPFx->>ReactComponent: Inject MenuWidget into parent element ReactComponent->>DOM: Rendered and visible to user

Production code may be more robust by using multiple observers to ensure it works in various situations, such as displaying document libraries, lists, and more.

Handling SharePoint’s ‘Shy’ Mode

SharePoint’s ‘shy’ mode refers to the behavior where the header changes when the user scrolls down, you may have noticed that the header changes, causing injected elements to disappear if they’re not handled correctly. To ensure our MenuWidget remains visible, we need to observe changes to the header and re-inject our component when necessary.

Activating the Shy Observer

We can set up another MutationObserver to monitor changes related to the ‘shy’ header and render our component accordingly.

Simplified Code Snippet:

private activateShyObserver = () => {
  const observer = new MutationObserver((mutations_list) => {
    mutations_list.forEach((mutation) => {
      mutation.addedNodes.forEach(() => {
        const parent = document.querySelector('div[class^=shyHeader]');
        if (parent) {
          let shyApps = document.getElementById('UserAppsShy');
          if (!shyApps) {
            shyApps = document.createElement('div');
            shyApps.id = 'UserAppsShy';
            parent.appendChild(shyApps);

            const appContext = new AppContext(
              this.context,
              this.logger
            );
            const element: React.ReactElement = React.createElement(
              AppContextProvider,
              { appContext },
              React.createElement(MenuWidget)
            );
            ReactDom.render(element, shyApps);
          }
          observer.disconnect();
        }
      });
    });
  });
  const headerRow = document.querySelector(`div[class^=headerRow]`);
  if (headerRow) {
    observer.observe(headerRow, { subtree: false, childList: true });
  }
};

In this code:

  • We observe the headerRow element for any child list changes.
  • When the ‘shy’ header appears, we inject our MenuWidget into it.
  • We ensure that the component is only injected once by checking if it already exists.

Visualizing the Shy Mode Handling

%%{init: {'theme': 'base', 'themeVariables': { 'background': '#CFA63D', 'primaryColor': '#CFA63D', 'primaryTextColor': '#0aa8a7', 'primaryBorderColor': '#c7a42b', 'secondaryColor': '#e6e6e6', 'secondaryTextColor': '#333333', 'secondaryBorderColor': '#999999', 'lineColor': '#e74c3c', 'fontFamily': 'ui-sans-serif,system-ui,-apple-system,BlinkMacSystemfont,sans-serif', 'fontSize': '14px', 'actorBorderColor': '#2980b9', 'actorBkgColor': '#ffcc00', 'actorTextColor': '#225f78', 'labelBoxBkgColor': '#ffffff', 'labelBoxBorderColor': '#cccccc', 'labelTextColor': '#000000', 'noteBkgColor': '#E5E4E2', 'noteTextColor': '#225f78', 'noteBorderColor': '#bbbab8', 'sequenceNumberColor': '#e67e22', 'taskBkgColor': '#3498db', 'taskBorderColor': '#2980b9', 'taskTextColor': '#ffffff' }}}%% sequenceDiagram participant User participant SPFx as SPFx Extension participant ShyObserver as MutationObserver participant DOM as SharePoint DOM participant ReactComponent as MenuWidget SPFx->>ShyObserver: Start observing headerRow activate ShyObserver User->>DOM: Scrolls down DOM-->>ShyObserver: shyHeader appears ShyObserver-->>SPFx: shyHeader found deactivate ShyObserver SPFx->>ReactComponent: Inject MenuWidget into shyHeader ReactComponent->>DOM: Rendered in shyHeader

Putting It All Together

When we combine these two observers, we ensure that our MenuWidget component is always present in the SharePoint header, regardless of user interactions like scrolling.

Optimization and Performance Considerations

The observer pattern is effective for injecting React components into SharePoint. Here are some best practices from the field :

  • Monitoring DOM Structure Changes (Note: Not Officially Supported by SharePoint): Manipulating the DOM directly, as shown in this post, is not officially supported by the SharePoint team. This means that such customizations may break without notice if Microsoft makes updates to SharePoint Online. Always proceed with caution and ensure you regularly test your solutions after SharePoint updates.

  • Specificity of Observed Elements: To optimize performance, limit the scope of the HTML elements you’re observing. By targeting the most specific parent element possible, you reduce the number of DOM mutations the observer needs to process. This minimizes overhead and prevents unnecessary performance degradation.

  • Understanding Observer Parameters: Experiment & test with the subtree and childList options when creating your observer. These parameters determine which mutations are observed:

    • childList: Set to true to monitor the addition or removal of direct child nodes.
    • subtree: Set to true to extend monitoring to all descendants of the target node.

    By configuring these options appropriately, you can fine-tune the observer to watch only for relevant changes, and ensure optimal performance.

Detailed Implementation & Code

You can find the full implementation of this pattern in the User Apps Application Customizer .

Conclusion

Injecting React components into SharePoint can be made reliable and efficient by leveraging the observer pattern. By observing the DOM for specific changes, we ensure our components are injected at the right time and remain visible, even when SharePoint’s dynamic behaviors, like the ‘shy’ header, come into play.