A boilerplate for creating a SPA in React, with a full server.
Index
- Features
- Starting the Dev Server
- Building a Production Package
- What This Repo Demonstrates
- How Things Work
- API's Used
- Uses
expressto serve bundled assets. - Utilizes
glamorfor styling. - Favicon updates on bundle creation to ensure a stale favicon doesn't get stuck in the user's cache.
- Uses
cross-env&cross-conf-envfor multi-platform support. - Uses
concurrentlyto run multiple processes needed to start the server in dev mode, and allows for custom labels for each process to make parsing the output for the dev easier. - Uses
react-loadablefor dynamicimports of chunked files. - SSR (Server-Side Rendering) of components and Client hydration.
- After all's said and done, the whole production app compiles down to a lean
~100kb(gzipped).
yarn start:server:devWhen the server starts in dev mode you can debug server code by visiting
chrome://inspect (if you're using Chrome as your browser). Then go to the
Sources tab and find the file you want to debug.
# builds the deployment package & starts a simple server to verify built code
yarn start:server- How to compile ES6 code that's utilizing Webpack aliasing and imports, down to
CommonJS that the server can utilize. Just compiling over to CommonJS, and not
a bundle, is useful for debugging and inline manipulation.
- I utilize the
envoption which allows me to set up profiles fordevelopment,production,server, andtest. - Then when you run Babel, you just specify the env you
want Babel to run under. Notice the use of
BABEL_ENV=\"production\". The backslashes are for consistent usage on Nix and Windows systems.
- I utilize the
- How to integrate Webpack's aliasing during transpilation.
- The top-level config has a
webpacksection with analiaseslist. I do this to allow for more control of what's considered an alias. - Then I just use the
webpack-aliasBabel plugin to use the aliases wired up in my Webpack config.
- The top-level config has a
- How to generate a
.babelrcfrom a JS config.- I created a JS config in a
.babeldirectory. - Created a
build:babelrccommand that will generate the.babelrc. Then that command is run before other commands that need thercfile.
- I created a JS config in a
- How to use a custom internal Babel plugin.
- When integrating
react-loadableI needed to edit it's Babel plugin to allow for usage with a composable function. So I duplicated it to theappendChunkProps.jsplugin, and made my changes. - Then you just configure and use it like any other plugin,
the only difference being the relative pathing, and ensuring that the pointer
ends with
.js.
- When integrating
- How to define varaiables like the Webpack
DefinePlugindoes, for consistent variable usage.- Just had to use the
transform-defineplugin.
- Just had to use the
- How to set up logging for routes, so you're aware of what routes are actually
processing the page.
- Created a
routesWrapperutil, then I wrap my routes with that util during export.
- Created a
- How to ensure a view has it's data loaded before it's rendered on the server.
- When setting up the config for the route I specify a
ssrprop with a function that loads data. - Then the route utilizes the
awaitSSRDatautil to load that views data before it gets rendered.
- When setting up the config for the route I specify a
- How to use
nodemonandreloadwhile in dev to get automatic server and page reloads when files change.- Like with Babel's config, I generate the
nodemon.jsonfrom a JS file, so that I can utilize the paths defined in the global config. The I just usenodemonlike usual. - For
reloadyou just need to wire it up in the server (for the websocket), and then include a request for it's "script" on the Client. I say "script" because there's no actual file, the request is caught on the server andreloadresponds back with data.
- Like with Babel's config, I generate the
- How to set up a
loggerthat can log the same colored output on the server as it does the client.- The
loggerutil utilizes aclientTransformandserverTransformto allow for a consistent API usage, but allow for the custom coloring syntax required for terminals, or the Client's Dev-Tools. - Admittedly the API's a bit kludgy, but it's what works atm. For any text that you want colorized, you use the constants exported from the logger.
- The
- How to reuse Webpack aliasing for easier file resolution.
- Jest uses a config prop called
moduleNameMapperwhich can be used to tell Jest how to resolve paths. So I just loop over the config aliases to generate the RegEx's for the mapper.- NOTE -
genMockFromModuledoesn't work withmoduleNameMapperso for now you have to manually mock aliased deps. For example, this pattern works:jest.mock('ALIAS/file', () => ({ key:'val' }))
- NOTE -
- Jest uses a config prop called
- How to set up SSR and client data/style hydration.
- For the server, you have to
renderStatic(forglamor) andrenderToString(forreact-dom). The results of those calls return to you thecss,html, andids. We only care aboutidsfor CSS hydration. - We then pass
idsand the current Redux store state to the template that renders the page. - The template then dumps that data into script tags that the Client can read from.
- The Client entry then reads that server data, and determines whether or not anything needs to be changed (which there shouldn't be), and sets up any listeners/handlers.
- For the server, you have to
- How to set up infinite scrolling of items using react-waypoint
and Redux.
- You set up a waypoint in your component so that when the user scrolls to within a certain threshold of that waypoint, it triggers a handler to load more items.
- Once that Redux action has loaded more items and updated the store, the
resultscount increases, causing another render, and so long as there's anextPagepresent, another waypoint is set, and the cycle continues until the user reaches the end.
- How to set up custom view transitions without the use of
react-transition-group(since it doesn't support that out of the box). By that I mean you can have default view transitions for most pages, or custom transitions based on the route you're comingfromandto, or visa versa.- Created the
ViewTransitioncomponent that can tap into thereact-routerdata to allow for a user-definedmiddlewareto temporarily display the current component and next component, and transition between the two. It has default transition styles set forfromandto, but they can be overridden in the passed inmiddleware. You can see that the middleware is checking what path it's comingfromand goingto, and based on that, returning different transition styling.
- Created the
- How to set up a per-view theming mechanism.
- Based on the currently matched route, the server and the
client call the
setShellClassaction which will in turn set a CSS class on the shell allowing for custom theming.
- Based on the currently matched route, the server and the
client call the
- How to set up and utilize top-level
breakpointsfor use with all components.- The
breakpointsfile has only a few definitions,mobilebeing the most used. - Then in a component's
stylesfile, you use it like so.
- The
- How to set up async data loading so components render on the server or show
a spinner on the client.
- There's a lot going on to achieve this, but if I had to boil it down:
- The data is fetched on the server via the
getDatautil. - The data is added into the store under
viewData. - Only then is the view rendered on the server.
- Once the Client loads, it hydrates the view.
- If a user switches a view, the
componentDidMountlifecycle is called within theAsyncChunkHOC, which then triggers a call togetDatawhich will check if the request has already been made - if it has, will return the already fetched data - if not, will fetch the new data, which then will update theviewDatacausing state change from loading to not.
- The data is fetched on the server via the
- There's a lot going on to achieve this, but if I had to boil it down:
- How to set up an image loader so image loads don't block the initial load of
the page.
- The result in
resultscome with an image URL. When looping over those results I check if a_loadedprop's been set - if it has, I just render a normalimgtag - if not, I use the@noxx/react-image-loadercomponent which will still render animgtag, but with a base64 1x1 pixel (so allimgstyling still behaves the same) for thesrc, and adata-srcattribute with the actual image source. On the Client, once the load of the image has begun, a load indicator will display, and on load complete the image will fade in for a smooth transition instead of it just popping into view. Once the image has loaded, it'll trigger theonLoadcallback (if one was passed), which in this case will change the current result's state to_loaded: true. - If it's determined that you'd prefer to have the image sources maintained
during SSR, you could just set the
_loadedstate of the current results totrue.
- The result in
- How to use
react-loadablealong with Webpack chunks to load chunks of code only when necessary.- Wire up Webpack to capture Loadable component during chunk creation.
- Each
Loadableis built out viacomposedChunks. ThecomposeChunkutil sets upLoadablewith the same defaults and allows for less dev set-up. - Those composed chunks are then passed to the
AsyncChunkHOC which allows for a consistent loading state (like when waiting for data, or the component to load), and allows for smooth transition from loading state to the loaded component, error, or timeout views. - To ensure that chunks are rendered on the server properly you have to preload all the chunks before the server starts up.
- You then have to capture all the chunks that were rendered for the current page on the server, so they can be pre-loaded before Client hydration.
- Then on the Client, it ensures that pre-loaded chunks have loaded before the hydration occurs.
- How to load a chunk on-demand (Client only)
- You don't need React for this, but a majority of the integration would in a component.
- In this case I'm keying off of a cookie that's set via
a toggle on the Client. If the cookie is set, it will return the
clientTransformfor logging, otherwise it'll just use a noop and no logging will occur.
- How to use the
DefinePluginto:- Expose (non-sensitive) file-system data on the client with
window.WP_GLOBALS. - Set up requires/imports so that server specific code is stripped out
during compilation so you don't get errors on the client, and smaller bundles.
All from the use of
process.env.IS_CLIENT.
- Expose (non-sensitive) file-system data on the client with
- How to set up aliasing so that imports are clean and don't contain any
../../../../../craziness. Also useful during refactors when folders get moved around, you just have to update the paths inconf.app.jsand you're all set.- Wire up the aliases from the global config.
- Prefix your imports or requires with exposed aliases.
- How to set up bundle filename hashing correctly so that they only change when the file contents have changed (allowing the user to keep old bundles in cache).
Files of note:
.
├── /dist
│ ├── /private # ES Webpack bundles (exposed to the users).
│ └── /public # CommonJS compiled server code (nothing in here should be
│ # exposed to users).
│
├── /src
│ ├── /components # Where all the React components live.
│ │ ├── /AsyncChunk # Ensures data is loaded before the view is rendered.
│ │ ├── /AsyncLoader # Displays the spinner and maintains scroll position.
│ │ ├── /Main # Where all the React routes are set up.
│ │ ├── /Shell # Uses a BrowserRouter or StaticRouter based on the env it's
│ │ │ # running on.
│ │ ├── /views # Where all the views live (think of them like pages).
│ │ └── /ViewTransition # Handles transitioning between pages.
│ │
│ │ # Configurations for each route path that are shared by the Client
│ │ # (react-router) and the Server (Express). Basically they define what
│ │ # view to serve up if a route is matched.
│ ├── /routes
│ │ ├── /configs
│ │ │ └── ... # Individual route configurations so you don't end up with
│ │ │ # a monolith of routes in one file.
│ │ │
│ │ ├── /shared
│ │ │ ├── composedChunks.js # Where `Loadable` chunks are composed for
│ │ │ │ # dynamic imports.
│ │ │ └── middleware.js # If a component is dependent on loaded data,
│ │ │ # these functions update the store with that data.
│ │ │
│ │ └── ... # Route config files.
│ │
│ ├── /state # Redux stuff.
│ │ ├── ...
│ │ └── store.js # A singleton that allows for using/accessing the store
│ │ # anywhere without having to use Connect.
│ │
│ ├── /server # All server specific code.
│ │ ├── /routes # Separate files for each route handler.
│ │ │ ├── catchAll.js # The default route handler.
│ │ │ └── index.js # Where you combine all your routes into something the
│ │ │ # server loads.
│ │ │
│ │ ├── /views # Should only be one view, but you can house any others here.
│ │ │ └── AppShell.js # The template that scaffolds the html, body,
│ │ │ # scripts, & css.
│ │ │
│ │ └── index.js # The heart of the beast.
│ │
│ ├── /state # Where the app state lives.
│ ├── /static # Static assets that'll just be copied over to public.
│ ├── /utils # Individual utility files that export one function and do only
│ │ # one thing well.
│ ├── data.js # Where the app gets it's data from (aside from API calls).
│ └── index.js # The Webpack entry point for the app.
│
└── conf.app.js # The configuration for the app.- Currently there's a
catchAll.jsroute inroutesthat then renders theAppShellwhich controls the document HTML, CSS, and JS. - Each
Viewthat's defined indata.jsis responsible for loading it's owndata. It does that by providing a static value, or via a function that returns a Promise. AsyncChunkwill display a spinner if it's data isn't found in cache, otherwise it'll pass the data on to theViewit was provided.- The
Maincomponent handles the SPA routing. So if you need to add routes that aren't defined innavItemsforheaderorfooter(withindata.js), you need to add them tootherRoutes(withindata.js). - Everything under
srcwill be compiled in some way. Parts ofsrcwill be bundled and dumped indist/publicand everything will be transpiled todist/privateso that the server code can 1 - be debugged easily (not being bundled) and 2 - make use of imports (so no mental hoops of "should I use require or import"). - Not using
webpack-dev-middlewarebecause it obfuscates where the final output will be until a production bundle is created, and you have to add extra Webpack specific code to your server. With the use of theTidyPlugin,reload, andwebpack-assets-manifestin conjunction with thewatchoption - we get a live-reload representation of what the production server will run.
Notes about the dev server:
- Sometimes running
rswhile the server is in dev mode, will exit withCannot read property 'write' of null. This will leave a bunch of zombie node processes, just runpkill nodeto clean those up. - Sometimes after killing the server, you'll see a bunch of
nodeprocesses still hanging around in Activity Monitor or Task Manager, but if you wait a couple seconds they clean themselves up.

