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
express
to serve bundled assets. - Utilizes
glamor
for 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-env
for multi-platform support. - Uses
concurrently
to 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-loadable
for dynamicimport
s 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:dev
When 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
env
option 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
webpack
section with analiases
list. I do this to allow for more control of what's considered an alias. - Then I just use the
webpack-alias
Babel plugin to use the aliases wired up in my Webpack config.
- The top-level config has a
- How to generate a
.babelrc
from a JS config.- I created a JS config in a
.babel
directory. - Created a
build:babelrc
command that will generate the.babelrc
. Then that command is run before other commands that need therc
file.
- I created a JS config in a
- How to use a custom internal Babel plugin.
- When integrating
react-loadable
I needed to edit it's Babel plugin to allow for usage with a composable function. So I duplicated it to theappendChunkProps.js
plugin, 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
DefinePlugin
does, for consistent variable usage.- Just had to use the
transform-define
plugin.
- 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
routesWrapper
util, 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
ssr
prop with a function that loads data. - Then the route utilizes the
awaitSSRData
util to load that views data before it gets rendered.
- When setting up the config for the route I specify a
- How to use
nodemon
andreload
while in dev to get automatic server and page reloads when files change.- Like with Babel's config, I generate the
nodemon.json
from a JS file, so that I can utilize the paths defined in the global config. The I just usenodemon
like usual. - For
reload
you 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 andreload
responds back with data.
- Like with Babel's config, I generate the
- How to set up a
logger
that can log the same colored output on the server as it does the client.- The
logger
util utilizes aclientTransform
andserverTransform
to 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
moduleNameMapper
which 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 -
genMockFromModule
doesn't work withmoduleNameMapper
so 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 aboutids
for CSS hydration. - We then pass
ids
and 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
results
count increases, causing another render, and so long as there's anextPage
present, 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 comingfrom
andto
, or visa versa.- Created the
ViewTransition
component that can tap into thereact-router
data to allow for a user-definedmiddleware
to temporarily display the current component and next component, and transition between the two. It has default transition styles set forfrom
andto
, but they can be overridden in the passed inmiddleware
. You can see that the middleware is checking what path it's comingfrom
and 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
setShellClass
action 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
breakpoints
for use with all components.- The
breakpoints
file has only a few definitions,mobile
being the most used. - Then in a component's
styles
file, 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
getData
util. - 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
componentDidMount
lifecycle is called within theAsyncChunk
HOC, which then triggers a call togetData
which 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 theviewData
causing 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
results
come with an image URL. When looping over those results I check if a_loaded
prop's been set - if it has, I just render a normalimg
tag - if not, I use the@noxx/react-image-loader
component which will still render animg
tag, but with a base64 1x1 pixel (so allimg
styling still behaves the same) for thesrc
, and adata-src
attribute 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 theonLoad
callback (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
_loaded
state of the current results totrue
.
- The result in
- How to use
react-loadable
along with Webpack chunks to load chunks of code only when necessary.- Wire up Webpack to capture Loadable component during chunk creation.
- Each
Loadable
is built out viacomposedChunks
. ThecomposeChunk
util sets upLoadable
with the same defaults and allows for less dev set-up. - Those composed chunks are then passed to the
AsyncChunk
HOC 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
clientTransform
for logging, otherwise it'll just use a noop and no logging will occur.
- How to use the
DefinePlugin
to:- 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.js
and 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.js
route inroutes
that then renders theAppShell
which controls the document HTML, CSS, and JS. - Each
View
that's defined indata.js
is responsible for loading it's owndata
. It does that by providing a static value, or via a function that returns a Promise. AsyncChunk
will display a spinner if it's data isn't found in cache, otherwise it'll pass the data on to theView
it was provided.- The
Main
component handles the SPA routing. So if you need to add routes that aren't defined innavItems
forheader
orfooter
(withindata.js
), you need to add them tootherRoutes
(withindata.js
). - Everything under
src
will be compiled in some way. Parts ofsrc
will be bundled and dumped indist/public
and everything will be transpiled todist/private
so 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-middleware
because 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-manifest
in conjunction with thewatch
option - we get a live-reload representation of what the production server will run.
Notes about the dev
server:
- Sometimes running
rs
while 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 node
to clean those up. - Sometimes after killing the server, you'll see a bunch of
node
processes still hanging around in Activity Monitor or Task Manager, but if you wait a couple seconds they clean themselves up.