I am building a Microsoft office add-in and using React. Usually, the navigation on the web is done using react-router-dom. However, I am not able to initialize the HashRouter in the index.js file, since it gives me only a white page. I am not sure where the bug is, since it is extremely difficult to debug apps for Office.
Did anyone encounter the same problem?
import App from "./components/App";
import { AppContainer } from "react-hot-loader";
import { initializeIcons } from "#fluentui/font-icons-mdl2";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./redux/store";
import { HashRouter as Router, Route } from "react-router-dom";
/* global document, Office, module, require */
initializeIcons();
let isOfficeInitialized = false;
const title = "Contoso Task Pane Add-in";
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<Router>
<Component title={title} isOfficeInitialized={isOfficeInitialized} />
</Router>
</Provider>
</AppContainer>,
document.getElementById("container")
);
};
/* Render application after Office initializes */
Office.onReady(() => {
isOfficeInitialized = true;
render(App);
});
/* Initial render showing a progress bar */
render(App);
if (module.hot) {
module.hot.accept("./components/App", () => {
const NextApp = require("./components/App").default;
render(NextApp);
});
}
I tried changing the render method to
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<Router>
<Route exact path="/" element={<Component title={title} isOfficeInitialized={isOfficeInitialized} />} />
</Router>
</Provider>
</AppContainer>,
document.getElementById("container")
);
};
This does not solve the issue, however. I am using react#17 and react-router-dom#6.
Strongly suspect this is due to serving your add-in over an unsecure connection which will render a white screen and provide little signs of what's wrong until it's fixed.
Ensure you are serving your app over a secure (HTTPS) connection. Office addins will not render unless they are served over a secured connection and react apps do not default to serving over a local secure connection.
If you're testing your addin locally on your machine, you'll need to setup your local environment to serve securely as if it was going over HTTPS. Typically this involves generating security certificates and a few other steps described decently well in this article.
Fortunately, Microsoft has saved you some trouble and provided a couple handy libraries to shortcut this process.
First ensure you have installed the following dev packages:
"office-addin-debugging": "^4.6.7",
"office-addin-dev-certs": "^1.11.1"
Second, in your package.json file, you'll want to modify your start script so that it utilises office-addin-debugging as such:
"scripts": {
"start": "office-addin-debugging start manifest.xml", <---TRY THIS!
"start:desktop": "office-addin-debugging start manifest.xml desktop",
"start:web": "office-addin-debugging start manifest.xml web",
"stop": "office-addin-debugging stop manifest.xml",
"watch": "webpack --mode development --watch"
},
Third, add a config section to your package.json file just before your scripts section like so:
"config": {
"app_to_debug": "excel",
"app_type_to_debug": "desktop",
"dev_server_port": 3000
},
"scripts": {
Then, in theory when you run npm run start, you will be prompted to install security certificates and an instance of excel will automatically launch, ideally with your addin that is now working... Good luck!
For Next.JS users: if you're building an addin using next.js rather than just react, you can still rely on office-add-in-dev-certs, but you'll need to do one extra step and write your own server. Easier than it sounds. Simply, install the same office-addin-dev-certs package I note above, add a file called server.js to the root of your project and include the following code:
const { createServer } = require('https')
const { parse } = require('url')
const next = require('next')
const devCerts = require('office-addin-dev-certs')
const dev = process.env.NODE_ENV !== 'production'
const hostname = 'localhost'
const port = 3000
const app = next({ dev, hostname, port })
const handle = app.getRequestHandler()
app.prepare().then(async () => {
const options = await devCerts.getHttpsServerOptions()
createServer(options, async (req, res) => {
try {
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
if (pathname === '/a') {
await app.render(req, res, '/a', query)
} else if (pathname === '/b') {
await app.render(req, res, '/b', query)
} else {
await handle(req, res, parsedUrl)
}
} catch (err) {
console.error('Error occurred handling', req.url, err)
res.statusCode = 500
res.end('internal server error')
}
}).listen(port, err => {
if (err) throw err
console.log(`> Ready on https://${hostname}:${port}`)
})
})
I solved the problem with the help of the comments provided by Drew Reese. The problem was two-fold:
The BrowserRouter component does not work in a Microsoft Office Add-In. We need to use HashRouter instead.
The routing pattern used in the newest react-router-dom#6 library does not work with react#17 in a Microsoft Office Add-In. We need to use react-router-dom#5 instead, which uses the <Switch /> component for routing.
The code in index.js looks like this:
import App from "./main/App";
import { AppContainer } from "react-hot-loader";
import { initializeIcons } from "#fluentui/font-icons-mdl2";
import { Provider } from "react-redux";
import store from "./store";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { HashRouter } from "react-router-dom";
/* global document, Office, module, require */
initializeIcons();
let isOfficeInitialized = false;
const title = "Cool title";
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<HashRouter>
<Component title={title} isOfficeInitialized={isOfficeInitialized} />
</HashRouter>
</Provider>
</AppContainer>,
document.getElementById("container")
);
};
/* Render application after Office initializes */
Office.onReady(() => {
isOfficeInitialized = true;
render(App);
});
/* Initial render showing a progress bar */
render(App);
if (module.hot) {
module.hot.accept("./main/App", () => {
const NextApp = require("./main/App").default;
render(NextApp);
});
}
And the actual routing is implement in App.js and looks like this:
import * as React from "react";
import PropTypes from "prop-types";
import { Switch, Route } from "react-router-dom";
function App() {
return (
<main>
<h1>App Title</h1>
<Stack>
<Switch>
<Route exact path="/">
<div>Hello World</div>
</Route>
<Route exact path="/main">
<div>Hello Main</div>
</Route>
</Switch>
</Stack>
</main>
);
}
export default App;
App.propTypes = {
title: PropTypes.string,
isOfficeInitialized: PropTypes.bool,
};
Related
all,
I am new to react native, currently, I am having an issue with customized fonts usage.
My issue is: font files not exists though I already put my fonts files in directory: ./assets/fonts
error image
I am following steps of Expo documentation of using customized fonts, which is installing expo-font and using useFont hook. My code as following:
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import AuthStack from './routes/authStack'
import store, { persistor } from './store'
import { useFonts } from 'expo-font'
import AppLoading from 'expo-app-loading'
import { Font } from 'expo'
import Loading from './components/loading'
function App() {
const [fontLoaded] = useFonts({
Arial: require('./assets/fonts/ARIAL.TTF'),
ArialBold: require('./assets/fonts/ARIALBD.TTF'),
BlairMd: require('./assets/fonts/BlairMdITCTTMediumFont.ttf'),
})
console.log('app font loaded====', fontLoaded)
return fontLoaded ? (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<AuthStack />
</PersistGate>
</Provider>
) : (
<AppLoading />
)
Everything looks fine for me, so I am thinking might because of project setting reasons so I have tried to restart the project, uninstall all packages and install them again, clear cache by running expo r -c. But nothing works.
I appreciate if anyone can help, thank you
//USE THIS INSTEAD
import { Provider } from 'react-redux';
import store from './store';
import { useFonts } from 'expo-font';
function App() {
const [fontLoaded] = useFonts({
Arial: require('./assets/fonts/ARIAL.TTF'),
ArialBold: require('./assets/fonts/ARIALBD.TTF'),
BlairMd: require('./assets/fonts/BlairMdITCTTMediumFont.ttf'),
});
if(!fontLoaded){
return null; //AppLoading is deprecated
};
return(
<Provider store={store}>
//Stack Screen to be loaded
</Provider>
);
I faced the same issue. I was able to fix it by naming my object key the same as the file name
eg:-
const [fontsLoaded] = useFonts({
PoppinsRegular: require('./../../assets/Fonts/PoppinsRegular.otf'),
});
I want 2 pages in my Chrome extension. For example: first(default) page with list of users and second with actions for this user.
I want to display second page by clicking on user(ClickableListItem in my case). I use React and React Router. Here the component in which I have:
class Resents extends React.Component {
constructor(props) {
super(props);
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
console.log('navigate to next page');
const path = '/description-view';
browserHistory.push(path);
}
render() {
const ClickableListItem = clickableEnhance(ListItem);
return (
<div>
<List>
<ClickableListItem
primaryText="Donald Trump"
leftAvatar={<Avatar src="img/some-guy.jpg" />}
rightIcon={<ImageNavigateNext />}
onClick={this.handleOnClick}
/>
// some code missed for simplicity
</List>
</div>
);
}
}
I also tried to wrap ClickableListItem into Link component(from react-router) but it does nothing.
Maybe the thing is that Chrome Extensions haven`t their browserHistory... But I don`t see any errors in console...
What can I do for routing with React?
I know this post is old. Nevertheless, I'll leave my answer here just in case somebody still looking for it and want a quick answer to fix their existing router.
In my case, I get away with just switching from BrowserRouter to MemoryRouter. It works like charm without a need of additional memory package!
import { MemoryRouter as Router } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<Router>
<OptionsComponent />
</Router>
</React.StrictMode>,
document.querySelector('#root')
);
You can try other methods, that suits for you in the ReactRouter Documentation
While you wouldn't want to use the browser (or hash) history for your extension, you could use a memory history. A memory history replicates the browser history, but maintains its own history stack.
import { createMemoryHistory } from 'history'
const history = createMemoryHistory()
For an extension with only two pages, using React Router is overkill. It would be simpler to maintain a value in state describing which "page" to render and use a switch or if/else statements to only render the correct page component.
render() {
let page = null
switch (this.state.page) {
case 'home':
page = <Home />
break
case 'user':
page = <User />
break
}
return page
}
I solved this problem by using single routes instead of nested. The problem was in another place...
Also, I created an issue: https://github.com/ReactTraining/react-router/issues/4309
This is a very lightweight solution I just found. I just tried it - simple and performant: react-chrome-extension-router
I just had to use createMemoryHistory instead of createBrowserHistory:
import React from "react";
import ReactDOM from "react-dom";
import { Router, Switch, Route, Link } from "react-router-dom";
import { createMemoryHistory } from "history";
import Page1 from "./Page1";
import Page2 from "./Page2";
const history = createMemoryHistory();
const App: React.FC<{}> = () => {
return (
<Router history={history}>
<Switch>
<Route exact path="/">
<Page1 />
</Route>
<Route path="/page2">
<Page2 />
</Route>
</Switch>
</Router>
);
};
const root = document.createElement("div");
document.body.appendChild(root);
ReactDOM.render(<App />, root);
import React from "react";
import { useHistory } from "react-router-dom";
const Page1 = () => {
const history = useHistory();
return (
<button onClick={() => history.push("/page2")}>Navigate to Page 2</button>
);
};
export default Page1;
A modern lightweight option has presented itself with the package wouter.
You can create a custom hook to change route based on the hash.
see wouter docs.
import { useState, useEffect } from "react";
import { Router, Route } from "wouter";
// returns the current hash location in a normalized form
// (excluding the leading '#' symbol)
const currentLocation = () => {
return window.location.hash.replace(/^#/, "") || "/";
};
const navigate = (to) => (window.location.hash = to);
const useHashLocation = () => {
const [loc, setLoc] = useState(currentLocation());
useEffect(() => {
// this function is called whenever the hash changes
const handler = () => setLoc(currentLocation());
// subscribe to hash changes
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
return [loc, navigate];
};
const App = () => (
<Router hook={useHashLocation}>
<Route path="/about" component={About} />
...
</Router>
);
I recently had to format my computer, but was given the option to save my files which I did. My project files are all there and good as far as I could tell. But when I do npm start it spins of the project without any errors, but then the URL bar displays this address:
http://localhost:3000/pages%20/%20login%20-%20page
instead of:
http://localhost:3000/pages/login-page
Note: This did not happen before I had to format my computer.
Steps I tried:
Remove node modules, then run npm i
Remove the %20 and hit enter, returns same thing in the URL bar
index.js
import { createBrowserHistory } from "history";
import jwt_decode from "jwt-decode";
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { Router, Route, Switch } from "react-router-dom";
import "./assets/scss/material-dashboard-pro-react.css";
import Pages from "./layouts/Pages.jsx";
import { setCurrentUser, logoutUser } from "./oldComp/actions/authActions";
import { clearCurrentProfile } from "./oldComp/actions/profileActions";
import store from "./oldComp/store";
import setAuthToken from "./oldComp/utils/setAuthToken";
import PrivateRoute from "./PrivateRoute";
import { indexRoutes } from "./routes/index.jsx";
import Profile from "./views/Community/ProfilePage/ProfilePage";
import { fetchAllLessons } from "./oldComp/actions/mentorLessonActions";
const hist = createBrowserHistory();
// Check for token
if (localStorage.jwtToken) {
// Set auth token header auth
setAuthToken(localStorage.jwtToken);
// Decode token and get user info and exp
const decoded = jwt_decode(localStorage.jwtToken);
// Set user and isAuthenticated
store.dispatch(setCurrentUser(decoded));
fetchAllLessons();
// Check for expired token
const currentTime = Date.now() / 1000;
if (decoded.exp < currentTime) {
// Logout user
store.dispatch(logoutUser());
// Clear current Profile
store.dispatch(clearCurrentProfile());
// Redirect to login
window.location.href = "/pages";
}
}
ReactDOM.render(
<Provider store={store}>
<Router history={hist}>
<div>
<Route path="/pages" name="Pages" component={Pages} />
<Switch>
{indexRoutes.map((prop, key) => {
return (
<PrivateRoute
path={prop.path}
component={prop.component}
key={key}
/>
);
})}
</Switch>
<Switch>
<PrivateRoute component={Profile} path="/partner-profile/:handle" />
</Switch>
</div>
</Router>
</Provider>,
document.getElementById("root")
);
I searched my files and had a redirect that had spaces in it.
This probably happened because I downloaded Beautify and it may have formatted some files that I wasn't aware of
I've been playing with Server Side Rendering (SSR) with React and I've pretty much got it working, with the exception of the homepage not loading from the server.
If I use alternative routes e.g /test or /404 it loads from the server. If I go to the / index path, it just loads my react app, but not html from the server. I tested this by disabling javascript and noticed it happening.
Here is my server code I'm using:
import path from 'path';
import fs from 'fs';
import React from 'react';
import express from 'express';
import cors from 'cors';
import configureStore from '../src/redux/store/configureStore';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import Client from '../src/client/index.jsx';
/* Express settings */
const app = express();
const PORT = process.env.PORT || 3334;
app.disable('x-powered-by');
app.use(cors());
app.use(express.static('./dist'));
app.use(handleRender);
/* Handle React Application */
function handleRender(req, res) {
const context = {};
const store = configureStore({});
const indexFile = path.resolve('./dist/index.html');
const html = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<Client />
</StaticRouter>
</Provider>
);
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
}
res.status(context.status||200).send(
data.replace(
'<div id="root"></div>',
`
<div id="root">${html}</div>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(store)}
</script>
`
)
);
});
}
/* Listen for connections */
app.listen(PORT, () => {
console.log(`💻 😎 Server is running on port ${PORT}`);
});
Here is my primary react component with the routes:
import React from 'react';
import {
Switch,
Route
} from 'react-router-dom';
import App from './components/App/index.jsx';
import NotFound from './NotFound.jsx';
const Test = () => (
<h1>Test!</h1>
);
const Bruh = () => (
<h1>Bruh!</h1>
);
const Client = () => (
<Switch>
<Route path="/" exact component={App} />
<Route path="/test" exact component={Test} />
<Route path="/bruh" exact component={Bruh} />
<Route component={NotFound} />
</Switch>
);
export default Client;
Finally, here is my index.js file for my react app:
import React from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import configureStore from './redux/store/configureStore';
import Client from './client/index.jsx';
import './styles/index.css';
/* Redux */
const preloadedState = window.__PRELOADED_STATE__ = {}; //initial state
delete window.__PRELOADED_STATE__;
const store = configureStore(preloadedState);
const unsubscribe = store.subscribe( () => {
console.log("Store: ", store.getState());
});
/* Render / Hydrate App */
hydrate(
<Provider store={ store }>
<BrowserRouter>
<Client />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
I think that's everything. Let me know if you need more info. Any advice and help would be greatly appreciated.
Thanks!
On the server an API only path has been set under /api.
When calling this path on the client-side, react-router takes over and responds with:
browser.js:49 Warning: [react-router] Location "/api/my_request" did not match any routes
How can we tell react-router to bypass this and just send the request out to the server?
Update:
This is how the request is being sent:
const sock = new SockJS('http://localhost:3030/api')
sock.onopen = () => {
console.log('open')
}
sock.onmessage = (e) => {
console.log('message', e.data)
}
sock.onclose = () => {
console.log('close')
}
sock.send('test')
Here is how I bypass React Router for my API routes with React Router 4.
I had to remember that React Router is in a React component. We can use JavaScript within it and we can test the URL.
In this case, I check for /api/ in the pathname of the URL. If it exists, then I do nothing and skip React Router entirely. Else, it has a route that does not begin with /api/ and I let React Router handle it.
Here is my App.js file in my client folder:
import React from 'react'
import Home from './components/Home'
import AnotherComponent from './components/AnotherComponent'
import FourOhFour from './components/FourOhFour'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
export default function App() {
const api_regex = /^\/api\/.*/
// if using "/api/" in the pathname, don't use React Router
if (api_regex.test(window.location.pathname)) {
return <div /> // must return at least an empty div
} else {
// use React Router
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/another-link" component={AnotherComponent} />
<Route component={FourOhFour} />
</Switch>
</div>
</Router>
)
}
}
Hi i had the same issue and this was my solution, this is my DefaultController the entry point of the React app.
class DefaultController extends AbstractController
{
/**
* #Route("/{reactRouting}", name="home", requirements={"reactRouting"="^(?!api).+"}, defaults={"reactRouting": null})
*/
public function index()
{
return $this->render('default/index.html.twig', [
'controller_name' => 'DefaultController',
]);
}
}
Notice that the "requirements" defines the regex for bypass /api path, and with this react router will ignore this route and the request will be handle by the server. I hope this works for you. Regards!