Meteor 1.5 and Code Splitting with React and React Router
By Vince
Meteor 1.5 is out and we can finally do code splitting! If you are not familiar with code splitting, this allows us to separate our logic in a single page web app (SPA) and only send the client the relevant portions of code. This is useful in a couple immediate ways:
- You have a giant application that is too large to send at once (any script files over 1MB often take too long and the user experience is terrible)
- You want to keep some portions of code hidden until the user is authorized to see it
We are going to cover option 2 since that is a use case I just ran into. Sometimes splitting the code up can make the files larger (or the same size) so Meteor has provided a nice tool that helps visual how large the file sizes are once they are bundled. You can read more about that in this great post: Bundle Visualizer. Use the tool before the split, and after the split to make sure it doesn’t effect your code in the wrong way (compare the two).
I am going to show the use of React with React Router 4 and how to split out an admin section of your code.
Here is a component called “AdminPage”. This loads when the user tries to connect to /admin/home which is the portal for all things admin. AdminPage is in our main app code base, and in the top level React Routes. So once someone hits /admin/home, this component is called (and it’s always sent to the client since we need something for them to code split!). I am not showing that route here for brevity.
class AdminPage extends React.Component {
//our logic is in the lifecycle after the component is mounted
componentDidMount() {
//Only run this if they have a userId we can check, they are logged in
if (Meteor.userId()) {
//Here we are going to call a Meteor method to see if they are authorized
//Note this is not going to stop the very sophisticated users from getting your admin
// code. So always use software best practices on the back-end and verify EVERYTHING.
// They can see the client code, but don't have access to any of the publishers or
// methods that require admin access.
Meteor.call('isAdmin', (err, res) => {
//if there is an error, show it
if (err) {
throwError(err);
}
//if the response came back false, they are not authorized
if (!res) {
throwWarn('Unauthorized', 'Redirecting to login page.');
Meteor.logout();
this.props.history.replace('/app/login'); //react router 4 passes everything in as props
}
//if the response came back truthy, they are authorized to get the routes
if (res) {
//this is the magic here, only import this route file. It creates a promise
// once the promise fulfills then we need to act on it
import('./ADMRoutes').then(ADMRoutes =>
{
//here we are connecting this.ADMRoutes to the imported code
//we need to add .default to make this work in this way
this.ADMRoutes = ADMRoutes.default;
//a side-effect is we need to update our component manually
//since react doesn't know the code has changed
//there might be a way to do this with state, need to play with that
this.forceUpdate();
})
}
}); //if they don't have a userID, put them back to the login page
} else {
throwWarn('Unauthorized', 'Redirecting to login page.');
Meteor.logout(); //just in case
this.props.history.replace('/app/login');
}
}
render() {
//here we can do some additional checks if they are an admin, which comes from props
//so we are checking two different ways, the meteor method, and props (both can be faked though)
if (this.props.dataReady && this.props.role === 'admin') {
return (
<div>
//here we are checking if this.ADMRoutes exists, if it does, send the component
//if it doesn't, don't send anything.
//on the first render, nothing will be sent. The forceupdate above fixes that.
{this.ADMRoutes ? <this.ADMRoutes
dataReady={this.props.dataReady}
role={this.props.role}
otherStuff={this.props.otherStuff}
/> : null } //the other side of this tertiary is null
</div>
);
}
else {
return <div></div>; //if they are not admins, the page will be blank
}
}
}
Here is our Admin Route that we are importing, which includes the admin pages (not going to show those here, they can be anything you want):
import AdminPage from './AdminPage';
import AdminReports from './AdminReports';
import {
Route,
Switch,
} from 'react-router-dom';
//This is the functional component we are importing above (splitting)
//So this is only sent to the client if they pass our tests above
const ADMRoutes = (props) => {
return (<div>
<Switch>
<Route path="/admin/reports" render={() => (
<AdminReports {...props} />
)} />
//here is the catch-all route, it will send down the AdminPage if users go to /admin/home
//this works because they are only sent the route when they are going into the admin component
<Route render={({history}) => (
<AdminPage {...props} history={history} />
)} />
</Switch>
</div>
);
};
export default ADMRoutes;
That’s the deal, not too complicated once the syntax is learned. The odd part is swapping the components around for React.
There are many others ways to use this, for restricting admin pages this is the one I found to work the best.
Also note that the code that is split gets sent down through DDP within meteor (the distributed data protocol - the always on connection), not through a new http request and bundle like webpack does.