PWA-Starter is a project skeleton for building Single Page & Progressive Web Apps.
The key technology to be familiar with is React. This skeleton embraces
component based architecture, and tries to introduce very few libraries
where an API needs to be learned or memorized. You should have a strong understanding of React and Hooks. Other than the default components used, pwa-starter
is really just a build script and folder structure.
- 100/100 Lighthouse score, right out of the box (proof)
- Small bundle size (Why Preact over React?)
- Pre-rendered pages (via React Snap)
- Efficient networking
- Speedy development
- Simplicity
Simplicity is always encouraged. Generally, this means Components that do one thing. If you component definition is getting dense, think about how you can break it up into smaller components, or move functionality into a Hook.
PWA-starter is a template repository. We recommend visiting the following URL to generate your repo from the pwa-starter
template and then cloning your new repo.
https://github.com/inputlogic/pwa-starter/generate
Once you've generated your repo, clone it, and then simply run:
npm install
# then
npm start
Visit http://localhost:5000
to view your application.
The folder structure of PWA-Starter is pretty straightforward. The major structural concept to understand is the apps/
folders.
src/
consts.js
constant values that will not change during the lifecycle of your appindex.js
the starting point of your approutes.js
define all of your app routes as well as any API routes
src/apps/
Break out your application logic into separate apps, à la Django. More details below.src/assets/
Static assets, such as html, images, fonts, etc.src/elements/
General purpose React Components that are not coupled to an appsrc/store/
Files related to setting up the global store. More details below.src/styles/
General styles setup. More details on styling below.src/util/
Little helper functions.
This is where we organize project specific code into logical groupings. Each app will need to export a Component and {routes}
Object. Inside an app folder you can manage your files how you like. For example, each page could be it's own folder in the app folder like our example main
app: https://github.com/inputlogic/pwa-starter/tree/master/src/apps/main
Or, you could have screens, elements, modals folder. Or, you could keep it flat with some folders, like our auth
app: https://github.com/inputlogic/pwa-starter/tree/master/src/apps/auth
The only requirement for an app is for the index.js
file to export a Component to render and a routes
object. The included marketing
app is a good basic example.
// More details on routing below
export const routes = {
home: {
path: url('home'),
component: Home
}
}
export function MarketingApp () {
return (
<Fragment>
<Router routes={routes} />
<MarketingFooter />
</Fragment>
)
}
The Component exported by your app should include a <Router />
instance with the local routes object passed in. To include a new app, you would:
- Create a new folder in
src/apps/
. - Create at minimum, an
index.js
file that exports a routes object (could be empty) and a Component to render. - Import that Component and routes object in
src/index.js
. - Add the app routes and Component to the top-level routes object.
// src/index.js
import { MyApp, routes as myAppRoutes } from '/apps/myapp'
// Define our top-level routes
const routes = {
main: {
routes: mainRoutes,
component: MainApp
},
// ...
myapp: {
routes: myAppRoutes,
component: MyApp
}
}
Routing is based on the @app-elements/router
. Routes are statically defined. That is, defined outside of the React render tree, which would by dynamically defined.
The details of the <Router />
Component are covered in the Router docs.
In PWA-Starter, instead of referencing routes directly, like blog-posts/archive/2013
we have a system for Named Routes. All route names are defined in the routes.js
file. Then, to reference a named route, you use the url
util function.
const routes = {
myRoute: '/some/:fancy/:route'
}
url('myRoute', {
args: {fancy: 12, route: 'r2d2'},
queries: {search: 'hi'}
})
> '/some/12/r2d2?search=hi'
By default, PWA-Starter is setup with atom, a Redux-inspired state library. The Redux-pattern of state management introduces just a few simple concepts, that allow your code to be more traceable and maintainable. For simple projects, you could get away with just React Component state, but this can easily get complicated when various Components throughout your render tree, require the same data. Generally, our guideline is if your React component needs to share state with another component, it's usually best to move that state to the global store. The global store being the singular Redux/atom state container.
The atom
documentation covers the API functionality, but for a broader overview of the concepts, we recommend An Introduction to Redux's Core Concepts.
The store folder contains our atom
related code.
store/index.js
initializes the store with your reducers, exports various actions to use.store/initial-state.js
defines the object that your store will be initialized with.store/hooks.js
contains store related hooks that are wrapped, so the store value is already passed in.
If you want to add custom hooks, or reducers you can put them in the store/
folder. If you end up with lots of custom reducers, you may wish to store them in a reducers/
folder within each of your apps
.
To load data from an API or back-end server, we recommend using the useRequest
hook (exported from store/hooks.js
).
- First, configure your
API_URL
inconsts.js
- Then, configure the
api
namespace in yourroutes.js
file - Then use the
useRequest
hook in your components!
For details on the useRequest
hook, refer to the @app-elements/use-request
docs.
Not all JavaScript bundlers can guarantee the build order for CSS files. Because of this, we just use LESS directly. You can review the basic build script. Much like JavaScript, we use an index.less
file as the starting point. Using the glob plugin, it's easy to include your less files without needing to specify each one individually.
It's encouraged to create your less files adjacent to the Component that they style. For more generic styles, they can live in the styles/
folder.
You must add the Pupeteer buildpack for prerendering to work as a build step. Order of buildpacks also matter, and should look as so:
❯ heroku buildpacks
=== pwa-starter Buildpack URLs
1. https://github.com/jontewks/puppeteer-heroku-buildpack.git
2. heroku/nodejs
Commands for adding multiple build packs:
heroku buildpacks:set https://github.com/jontewks/puppeteer-heroku-buildpack.git
heroku buildpacks:add --index 2 heroku/nodejs
- Create a new Netlify site
- When you get to
Basic build settings
, set the following values:
Build command: npm run build
Publish Directory: public
- On the same step, click
Show advanced
to define Environment Variables
NODE_ENV: netlify
If you encounter a deploy error on Netlify like:
TypeError: Failed to fetch dynamically imported module: http://localhost:45678/module/index.js
You can try increasing the sleep
time in package.json. There seems to be the occassional race condition related to the file system on Netlify, where the cacheBuster rewrites the js and css includes in index.html
to have a timestamp, but react-snap is invoked with the old index.html
.