Robust way to avoid the "Office.js has not fully loaded" error in a ReactJS project

167 Views Asked by At

We develop Excel add-ins. From time to time, we observe this Error: Office.js has not fully loaded. Your app must call "Office.onReady()" as part of it's loading sequence (or set the "Office.initialize" function). If your app has this functionality, try reloading this page. It did not seem very blocking. However, now we realize that when this happens in Mac for Excel, the taskpane becomes a blank page. Users have to reload the page again to not see this error and load well the page.

So we would like to find a robust way to avoid this error.

Our add-ins are built with Reactjs and dva, which is a framework based on redux, redux-saga and react-router. Here is frontend/src/index.tsx:

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import dva from 'dva';
import router from './router';
import AuthModel from './models/auth';
import SubscribtionModel from './models/subscription';
import AppModel from './models/app';
import SpreadsheetModel from './models/spreadsheet';
import UsageModel from './models/usage';
import SettingsModel from './models/settings';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';

initializeIcons();

const app = dva();

app.model(AuthModel);
app.model(SubscribtionModel)
app.model(AppModel);
app.model(SpreadsheetModel);
app.model(UsageModel);
app.model(SettingsModel);

app.router(router);

app.start('#root');

Here is src/router.tsx:

import React from 'react';
import { routerRedux, Switch, Route } from 'dva/router';
import Layout from './layouts';
import LoginPage from './components/LoginPage';
import Welcome from './components/welcome';
import SocialLoginSuccess from './components/socialLoginSuccess';
import { AwaitPromiseThenRender } from './components/AwaitPromiseThenRender';

const { ConnectedRouter } = routerRedux;
function RouterConfig({ history }: any) {
  //@ts-ignore
  return (
    <AwaitPromiseThenRender
      //@ts-ignore
      promise={typeof window.__$$notInOffice !== "undefined" ? Promise.resolve(true) : Office.onReady()}
    >
      <ConnectedRouter history={history}>
        <Layout>
          <Switch>
            <Route path="/sign">
              <LoginPage />
            </Route>
            <Route path="/home">
              <Welcome />
            </Route>
            <Route path="/app">
              <App />
            </Route>
            ... ...
            ... ...
        </Layout>
      </ConnectedRouter>
    </AwaitPromiseThenRender>
  );
}

export default RouterConfig;

Here is src/components/AwaitPromiseThenRender/index.tsx:

import React, { Component } from 'react';

interface IProps {
  promise: Promise<any>;
  children: React.ReactNode;
}

interface IState {
  promiseHasResolved: boolean;
}

export class AwaitPromiseThenRender extends Component<IProps> {
  state: IState = { promiseHasResolved: false };
  constructor(props: IProps) {
    super(props);
    props.promise
      .then(() => this.setState({ promiseHasResolved: true }))
      .catch(e => console.log("invokeGlobalErrorHandler(e)"))
  }

  render() {
    const { children } = this.props;
    const { promiseHasResolved } = this.state;

    return promiseHasResolved ? children : null;
  }
}

Does anyone know how to amend our existing code to definitely avoid the Office.js has not fully loaded error?

3

There are 3 best solutions below

2
On

I recommend following the pattern that the yo office React template uses. Don't render until Office.onReady() completes. To see the entire template code, see Install the Yeoman generator — Yo Office. Run 'yo office' and then choose the React template to generate the project.

let isOfficeInitialized = false;

const title = "Contoso Task Pane Add-in";

const render = (Component) => {
  ReactDOM.render(
    <AppContainer>
      <ThemeProvider>
        <Component title={title} isOfficeInitialized={isOfficeInitialized} />
      </ThemeProvider>
    </AppContainer>,
    document.getElementById("container")
  );
};

/* Render application after Office initializes */
Office.onReady(() => {
  isOfficeInitialized = true;
  render(App);
});
0
On

Basically seems like error tells you there is a flow in which you try to do something before office.js has fully loaded. Your logic seems fine, but I would suspect two things in the code: I would suspect a few things:

  • Maybe the issue is not in the rendering phase, but in the initialization phase. Where you call app.model. If that is the case I would try to initialize in onReady
Office.onReady(()=>{
   app.model(AuthModel);
   app.model(SubscribtionModel)
   app.model(AppModel);
   app.model(SpreadsheetModel);
   app.model(UsageModel);
   app.model(SettingsModel);

   app.router(router);

   app.start('#root');
})
  • If window.__$$notInOffice is true/false/null you will not call Office.onReady() at all. Could that be the problematic flow? In such case simply pass Office.onReady()
 return (
    <AwaitPromiseThenRender
      promise={Office.onReady()}
    >
0
On

Ensure Office.js Initialization: You should initialize Office.js properly before rendering any content in your add-in. The Office.onReady() function is designed to notify your add-in when Office.js is fully loaded and ready for interaction. In your current code, it seems you're already using Office.onReady(), but it's important to make sure that it's called before any other code that depends on Office.js.

Restructure the Routing: Your routing code should be placed inside the Office.onReady() callback to ensure that your app doesn't render any content until Office.js is ready. This will help prevent the blank task pane issue. Here's how you can modify your code to achieve this:

In your src/router.tsx file:

import React from 'react';
import { routerRedux, Switch, Route } from 'dva/router';
import Layout from './layouts';
import LoginPage from './components/LoginPage';
import Welcome from './components/welcome';
import App from './components/App'; // Make sure to import your App component
import { AwaitPromiseThenRender } from './components/AwaitPromiseThenRender';

const { ConnectedRouter } = routerRedux;

function RouterConfig({ history }: any) {
  return (
    <ConnectedRouter history={history}>
      <Layout>
        <Switch>
          <Route path="/sign">
            <LoginPage />
          </Route>
          <Route path="/home">
            <Welcome />
          </Route>
          <Route path="/app">
            <App />
          </Route>
          {/* ... Other routes */}
        </Switch>
      </Layout>
    </ConnectedRouter>
  );
}

export default RouterConfig;

In your src/index.tsx file:

import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import dva from 'dva';
import router from './router';
import AuthModel from './models/auth';
import SubscribtionModel from './models/subscription';
import AppModel from './models/app';
import SpreadsheetModel from './models/spreadsheet';
import UsageModel from './models/usage';
import SettingsModel from './models/settings';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';

initializeIcons();

Office.onReady(() => {
  const app = dva();

  app.model(AuthModel);
  app.model(SubscribtionModel);
  app.model(AppModel);
  app.model(SpreadsheetModel);
  app.model(UsageModel);
  app.model(SettingsModel);

  app.router(router);

  app.start('#root');
});

By restructuring your code in this way, your app's routing and rendering logic will only kick in once Office.js is fully loaded, ensuring a smooth and error-free experience for your users.