Skip to content

the0neWhoKnocks/boilerplate.react.spa.full-server

Repository files navigation

React SPA (Full Server)

A boilerplate for creating a SPA in React, with a full server.

spa-and-server-02

Index


Features

  • 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 dynamic imports 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).

spa-and-server--cli-01


Start up dev server

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.


Build and start up the production package

# builds the deployment package & starts a simple server to verify built code
yarn start:server

Demonstrates:

Babel

  • 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.
    1. I utilize the env option which allows me to set up profiles for development, production, server, and test.
    2. 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.
  • How to integrate Webpack's aliasing during transpilation.
    1. The top-level config has a webpack section with an aliases list. I do this to allow for more control of what's considered an alias.
    2. Then I just use the webpack-alias Babel plugin to use the aliases wired up in my Webpack config.
  • How to generate a .babelrc from a JS config.
    1. I created a JS config in a .babel directory.
    2. Created a build:babelrc command that will generate the .babelrc. Then that command is run before other commands that need the rc file.
  • How to use a custom internal Babel plugin.
    1. 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 the appendChunkProps.js plugin, and made my changes.
    2. 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.
  • How to define varaiables like the Webpack DefinePlugin does, for consistent variable usage.
    1. Just had to use the transform-define plugin.

Express

  • How to set up logging for routes, so you're aware of what routes are actually processing the page.
    1. Created a routesWrapper util, then I wrap my routes with that util during export.
  • How to ensure a view has it's data loaded before it's rendered on the server.
    1. When setting up the config for the route I specify a ssr prop with a function that loads data.
    2. Then the route utilizes the awaitSSRData util to load that views data before it gets rendered.
  • How to use nodemon and reload while in dev to get automatic server and page reloads when files change.
    1. 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 use nodemon like usual.
    2. 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 and reload responds back with data.

General JS Stuff

  • How to set up a logger that can log the same colored output on the server as it does the client.
    1. The logger util utilizes a clientTransform and serverTransform to allow for a consistent API usage, but allow for the custom coloring syntax required for terminals, or the Client's Dev-Tools.
    2. 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.

Jest

  • How to reuse Webpack aliasing for easier file resolution.
    1. 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 with moduleNameMapper so for now you have to manually mock aliased deps. For example, this pattern works:
        jest.mock('ALIAS/file', () => ({ key:'val' }))

React

  • How to set up SSR and client data/style hydration.
    1. For the server, you have to renderStatic (for glamor) and renderToString (for react-dom). The results of those calls return to you the css, html, and ids. We only care about ids for CSS hydration.
    2. We then pass ids and the current Redux store state to the template that renders the page.
    3. The template then dumps that data into script tags that the Client can read from.
    4. 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.
  • How to set up infinite scrolling of items using react-waypoint and Redux.
    1. 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.
    2. 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 a nextPage 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 coming from and to, or visa versa.
    1. Created the ViewTransition component that can tap into the react-router data to allow for a user-defined middleware to temporarily display the current component and next component, and transition between the two. It has default transition styles set for from and to, but they can be overridden in the passed in middleware. You can see that the middleware is checking what path it's coming from and going to, and based on that, returning different transition styling.
  • How to set up a per-view theming mechanism.
    1. 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.
  • How to set up and utilize top-level breakpoints for use with all components.
    1. The breakpoints file has only a few definitions, mobile being the most used.
    2. Then in a component's styles file, you use it like so.
  • How to set up async data loading so components render on the server or show a spinner on the client.
    1. 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 the AsyncChunk HOC, which then triggers a call to getData 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 the viewData causing state change from loading to not.
  • How to set up an image loader so image loads don't block the initial load of the page.
    1. 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 normal img tag - if not, I use the @noxx/react-image-loader component which will still render an img tag, but with a base64 1x1 pixel (so all img styling still behaves the same) for the src, and a data-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 the onLoad callback (if one was passed), which in this case will change the current result's state to _loaded: true.
    2. 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 to true.
  • How to use react-loadable along with Webpack chunks to load chunks of code only when necessary.
    1. Wire up Webpack to capture Loadable component during chunk creation.
    2. Each Loadable is built out via composedChunks. The composeChunk util sets up Loadable with the same defaults and allows for less dev set-up.
    3. 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.
    4. To ensure that chunks are rendered on the server properly you have to preload all the chunks before the server starts up.
    5. 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.
    6. 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)
    1. You don't need React for this, but a majority of the integration would in a component.
    2. 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.

Webpack


How Does It All Work?

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 in routes that then renders the AppShell which controls the document HTML, CSS, and JS.
  • Each View that's defined in data.js is responsible for loading it's own data. 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 the View it was provided.
  • The Main component handles the SPA routing. So if you need to add routes that aren't defined in navItems for header or footer (within data.js), you need to add them to otherRoutes (within data.js).
  • Everything under src will be compiled in some way. Parts of src will be bundled and dumped in dist/public and everything will be transpiled to dist/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 the TidyPlugin, reload, and webpack-assets-manifest in conjunction with the watch 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 with Cannot read property 'write' of null. This will leave a bunch of zombie node processes, just run pkill 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.

API's used