- removes Styled Components; adds [Emotion](https://emotion.sh/) CSS-in-JS
- adds [MobX](https://mobx.js.org/)
- adds local state to `src/data/state.ts`
- adds server dehydration/client hydration of MobX state
- adds `src/lib/mobx.ts`, with helper functions for dehydration and feeding state context to React via `<StateProvider>`
- adds `<StateConsumer>`, which takes a function and passes it local state; automatically re-renders React children when state is mutated
- replaces Apollo local state counter example queries, state and mutations, and replaces with an `increment()` function on state
- moves Apollo to `src/lib`; removes redundant `src/apollo`
- removes `src/queries`
- removes `src/mutations`
- replaces Styled Components global with Emotion's `<Global>` type
- adds new multi-build Dockerfile; bumps Node to 11.8
- removes theming
- adds `scripts()` to `src/lib/stats.ts`, for finding related `.js` files to bootstrap on initial HTML render (wip toward adding cached, per-request vendor files)
- dumps a full stack trace to the console in the event of a server error on any route, instead of just the message, for easier debugging
- refactors `src/views/ssr.tsx` component to produce a map of `<script>` tags instead of taking a `js` var
- updates readme
pull/143/head^2 4.0.0
Lee Benson 4 years ago
parent 499c6cd676
commit 6487b66a56
  1. 25
      Dockerfile
  2. 54
      README.md
  3. 291
      package-lock.json
  4. 11
      package.json
  5. 58
      src/components/example/count.tsx
  6. 18
      src/components/example/hackernews.tsx
  7. 5
      src/components/root.tsx
  8. 12
      src/data/state.ts
  9. 20
      src/entry/client.tsx
  10. 72
      src/entry/server.tsx
  11. 17
      src/global/styles.ts
  12. 79
      src/graphql/state.ts
  13. 7
      src/lib/apollo.ts
  14. 44
      src/lib/mobx.tsx
  15. 16
      src/lib/stats.ts
  16. 30
      src/lib/styledComponents.ts
  17. 21
      src/mutations/incrementCount.ts
  18. 21
      src/queries/getCount.ts
  19. 2
      src/runner/app.ts
  20. 17
      src/themes/default.ts
  21. 7
      src/themes/interface.ts
  22. 9
      src/views/ssr.tsx
  23. 8
      src/webpack/common.ts

@ -1,16 +1,29 @@
FROM node:10.11-alpine
FROM node:11.8.0-alpine AS builder
# log most things
ENV NPM_CONFIG_LOGLEVEL notice
# Install NPM packages
WORKDIR /app
# OS packages for compilation
RUN apk add --no-cache python2 make g++
# install NPM packages
WORKDIR /build
ADD package*.json ./
RUN npm i
# add source
ADD . .
# Build
# build
RUN npm run build
EXPOSE 3000
########################
FROM node:11.8.0-alpine
WORKDIR /app
# copy source + compiled `node_modules`
COPY --from=builder /build .
CMD npm run production
# by default, run in production mode
CMD npm run production

@ -13,10 +13,11 @@ https://reactql.org
- [React v16](https://facebook.github.io/react/) for UI.
- [Apollo Client 2.0 (React)](http://dev.apollodata.com/react/) for connecting to GraphQL.
- Fully typed [Styled Components v4](https://www.styled-components.com/), with inline `<style>` tag generation that contains only the CSS that needs to be rendered, and full theming.
- [MobX](https://mobx.js.org/) for declarative, type-safe flux/store state management (automatically re-hydrated from the server.)
- [Emotion](https://emotion.sh/) CSS-in-JS, with inline `<style>` tag generation that contains only the CSS that needs to be rendered.
- [Sass](https://sass-lang.com/), [Less](http://lesscss.org/) and [PostCSS](https://postcss.org/) when importing `.css/.scss/.less` files.
- [React Router 4](https://github.com/ReactTraining/react-router/tree/v4) for declarative browser + server routes.
- [Apollo Link State](https://www.apollographql.com/docs/link/links/state.html) for local flux/store state management (automatically re-hydrated from the server.)
- Declarative/dynamic `<head>` section, using [react-helmet](https://github.com/nfl/react-helmet).
### Server-side rendering
@ -24,7 +25,8 @@ https://reactql.org
- Built-in [Koa 2](http://koajs.com/) web server, with async/await routing.
- Full route-aware server-side rendering (SSR) of initial HTML.
- Universal building - both browser + Node.js web server compile down to static files, for fast server re-spawning.
- Per-request GraphQL local state. Store state is dehydrated via SSR, and rehydrated automatically on the client.
- Per-request GraphQL store. Store state is dehydrated via SSR, and rehydrated automatically on the client.
- MobX for app-wide flux/store state, with a built-in `<StateConsumer>` for automatically re-rendering any React component that 'listens' to state and full client-side rehydration. Fully typed state!
- Full page React via built-in SSR component - every byte of your HTML is React.
- SSR in both development and production, even with hot-code reload.
@ -32,31 +34,31 @@ https://reactql.org
- Hot code reloading; zero refresh, real-time updates in development.
- Development web server that automatically sends patches on code changes, and restarts the built-in Web server for SSR renders that reflect what you'd see in production.
- New in v3.5: WebSocket `subscription` query support for real-time data (just set `WS_SUBSCRIPTIONS=1` in [.env](.env))
- WebSocket `subscription` query support for real-time data (just set `WS_SUBSCRIPTIONS=1` in [.env](.env))
### Code optimisation
- [Webpack v4](https://webpack.js.org/), with [tree shaking](https://webpack.js.org/guides/tree-shaking/) -- dead code paths are automatically eliminated.
- Asynchronous code loading when `import()`'ing inside a function.
- Aggressive code minification.
- Automatic per-vendor chunk splitting/hashing, for aggressive caching (especially good behind a HTTP/2 proxy!)
- Gzip/Brotli minification of static assets.
- CSS code is combined, minified and optimised automatically - even if you use SASS, LESS and CSS together!
### Styles
- [Styled Components v4](https://www.styled-components.com/), for writing CSS styles inline and generating the minimal CSS required to properly render your components. Full type inference on themes, too.
- [Emotion](https://emotion.sh/), for writing CSS styles inline and generating the minimal CSS required to properly render your components.
- [PostCSS v7](http://postcss.org/) with [next-gen CSS](https://preset-env.cssdb.org/) and automatic vendor prefixing when importing `.css`, `.scss` or `.less` files.
- [SASS](http://sass-lang.com) and [LESS](http://lesscss.org/) support (also parsed through PostCSS.)
- Automatic vendor prefixing - write modern CSS, and let the compiler take care of browser compatibility.
- Mix and match SASS, LESS and regular CSS - without conflicts!
- CSS modules - your classes are hashed automatically, to avoid namespace conflicts.
- Compatible with Foundation, Bootstrap, Material and more. Simply configure via a `.global.(css|scss|less)` import to preserve class names.
- Compatible with Foundation, Bootstrap, Material UI and more. Simply configure via a `.global.(css|scss|less)` import to preserve class names.
### Production-ready
- Production bundling via `npm run production`, that generates optimised server and client code.
- [Static compression](https://webpack.js.org/plugins/compression-webpack-plugin/) using the Gzip and [Brotli](https://opensource.googleblog.com/2015/09/introducing-brotli-new-compression.html) algorithms for the serving of static assets as pre-compressed `.gz` and `.br` files (your entire app's `main.js.bz` - including all dependencies - goes from 346kb -> 89kb!)
- Automatic HTTP hardening against common attack vectors via [Koa Helmet](https://github.com/venables/koa-helmet) (highly configurable)
- New in v3.5: Static bundling via `npm run static`. Easily deploy a client-only SPA to any static web host (Netlify, etc.)
- Static bundling via `npm run static`. Don't need server-side rendering? No problem. Easily deploy a client-only SPA to any static web host (Netlify, etc.)
### Developer support
@ -68,8 +70,8 @@ https://reactql.org
Grab and unpack the latest version, install all dependencies, and start a server:
```
wget -qO- https://github.com/leebenson/reactql/archive/3.7.1.tar.gz | tar xvz
cd reactql-3.7.1
wget -qO- https://github.com/leebenson/reactql/archive/4.0.0.tar.gz | tar xvz
cd reactql-4.0.0
npm i
npm start
```
@ -80,11 +82,11 @@ Your development server is now running on [http://localhost:3000](http://localho
Development mode offers a few useful features:
- Hot code reloading. Make a change anywhere in your code base (outside of the Webpack config), and changes will be pushed down the browser automatically - without page reloads. This happens for React, Styled Components, SASS - pretty much anything.
- Hot code reloading. Make a change anywhere in your code base (outside of the Webpack config), and changes will be pushed down the browser automatically - without page reloads. This happens for React, Emotion, SASS - pretty much anything.
- Full source maps for Javascript and CSS
- Full source maps for Javascript and CSS.
- Full server-side rendering, with automatic Koa web server restarting on code changes. This ensures the initial HTML render will always reflect your latest code changes
- Full server-side rendering, with automatic Koa web server restarting on code changes. This ensures the initial HTML render will always reflect your latest code changes.
To get started, simply run:
@ -130,27 +132,21 @@ The important stuff is in [src](src).
Here's a quick run-through of each sub-folder and what you'll find in it:
- [src/components](src/components) - React components. Follow the import flow at [root.tsx](src/components/root.tsx) to figure out the component render chain. I've included an [example](src/components/example) component that shows off some Apollo GraphQL features, including incrementing a local counter and pulling top news stories from Hacker News (a live GraphQL server endpoint.)
- [src/components](src/components) - React components. Follow the import flow at [root.tsx](src/components/root.tsx) to figure out the component render chain. I've included an [example](src/components/example) component that shows off some Apollo GraphQL and MobX features, including incrementing a local counter and pulling top news stories from Hacker News (a live GraphQL server endpoint.)
- [src/data](src/data) - Data used throughout your app. You'll find [routes.ts](src/data/routes.ts), which defines your React Router routes (currently, just the home page -- but you can easily extend this.)
- [src/data](src/data) - Data used throughout your app. You'll find [routes.ts](src/data/routes.ts), which defines your React Router routes (currently, just the home page -- but you can easily extend this.) and [state.ts](src/data/state.ts), to show you how simple it is to define your own state data fields that, when modified, automatically re-render any 'observing' component.
- [src/entry](src/entry) - The client and server entry points, which call on [src/components/root.tsx](src/components/root.tsx) to isomorphically render the React chain in both environments.
- [src/global](src/global) - A good place for anything that's used through your entire app, like global styles. I've started you off with a [styles.ts](src/global/styles.ts) that sets globally inlined Styled Components CSS, as well as pulls in a global `.scss` file -- to show you how both types of CSS work.
- [src/graphql](src/graphql) - GraphQL initialisation goes here. There's an [apollo.ts](src/graphql/apollo.ts) which builds a universal Apollo Client and enables local state, and [state.ts](src/graphql/state.ts) which sets up default state (automatically rehydrated on the client) and some mutation handlers, for incrementing a local counter.
- [src/global](src/global) - A good place for anything that's used through your entire app, like global styles. I've started you off with a [styles.ts](src/global/styles.ts) that sets globally inlined Emotion CSS, as well as pulls in a global `.scss` file -- to show you how both types of CSS work.
- [src/lib](src/lib) - Library functions to handle hot-code reloading, finding the right `main.js` / `main.css` in production (which is automatically hashed for versioning), Webpack stats and Styled Components.
- [src/lib](src/lib) - Internal libraries/helpers. There's an [apollo.ts](src/lib/apollo.ts) which builds a universal Apollo Client, and [mobx.ts](src/graphql/mobx.ts) which sets up default state (automatically rehydrated on the client), for incrementing a local counter. Plus, functions to handle hot-code reloading, Webpack stats helpers (used by the server to automatically load the right `<script>` tags, and some internal stuff to help with hot-code reloading the server when code changes in developmnt.
- [src/mutations](src/mutations) - Your GraphQL mutations. Out-the-box, you'll find the query to increment the local state counter.
- [src/queries](src/queries) - Your GraphQL queries. There are two by default - one that grabs the local counter state, another that pulls the top stories from Hacker News to display in the example component.
- [src/queries](src/queries) - Your GraphQL queries. There's just one by default - for pilling the top stories from Hacker News to display in the example component.
- [src/runner](src/runner) - Development and production runners that spawn the Webpack build process in each environment.
- [src/themes](src/themes) - A sample [interface](src/themes/interface.ts) type for defining a Styled Components theme, and a [default theme](src/themes/default.ts) that's used in the example component to add an orange hover to Hacker News links.
- [src/views](src/views) - View components that fall outside of the usual React component chain, for use on the server. In here, [ssr.tsx](src/views/ssr.tsx) takes care of rendering the root HTML that's sent down the wire to the client. Note this is also a React component - your whole app will render as React!
- [src/views](src/views) - View components that fall outside of the usual React component chain, for use on the server. In here, [ssr.tsx](src/views/ssr.tsx) takes care of rendering the root HTML that's sent down the wire to the client. Note this is also a React component - your whole app will render as React! - and [static.html](src/views/static.html) serves as a template for rendering a client-side SPA. Update it as needed.
- [src/webpack](src/webpack) - The Webpack 4 configuration files that do the heavy lifting to transform our Typescript code, images and CSS into optimised and minified assets that wind up in the `dist` folder at the root. Handles both the client and server environments.
@ -158,15 +154,15 @@ You'll also find some other useful goodies in the [root]()...
- [.env](.env) - Change your `GRAPHQL` server endpoint, and `WS_SUBSCRIPTIONS=1` for built-in WebSocket support.
- [.nvmrc](.nvmrc) - Specify your preferred Node.js version, for use with NVM and used by many continuous deployment tools. Defaults to v10.11
- [.nvmrc](.nvmrc) - Specify your preferred Node.js version, for use with NVM and used by many continuous deployment tools. Defaults to v11.8.0
- [netlify.toml](netlify.toml) - Build instructions for fast [Netlify](https://www.netlify.com/) deployments. **Tip: To quickly deploy a demo ReactQL app, [click here](https://app.netlify.com/start/deploy?repository=https://github.com/leebenson/reactql).**
- [types](types) - Some basic types that allow you to import fonts, images, CSS/SASS/LESS files, and allow use of the global `SERVER` boolean in your IDE.
- Typescript configuration via [tsconfig.json](tsconfig.json) and [tslint.json](tslint.json)
- Typescript configuration via [tsconfig.json](tsconfig.json)
- A sample [Dockerfile](Dockerfile) for quickly deploying your code base to production.
- A sample multi-build [Dockerfile](Dockerfile) based on Node 11.8 and Alpine, for quickly deploying your code base to production.
# Follow @reactql for updates

291
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "reactql",
"version": "3.7.1",
"version": "4.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -80,14 +80,6 @@
"trim-right": "^1.0.1"
}
},
"@babel/helper-annotate-as-pure": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz",
"integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==",
"requires": {
"@babel/types": "^7.0.0"
}
},
"@babel/helper-function-name": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
@ -229,6 +221,44 @@
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
"dev": true
},
"@emotion/cache": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.0.tgz",
"integrity": "sha512-1/sT6GNyvWmxCtJek8ZDV+b+a+NMDx8/61UTnnF3rqrTY7bLTjw+fmXO7WgUIH0owuWKxza/J/FfAWC/RU4G7A==",
"requires": {
"@emotion/sheet": "0.9.2",
"@emotion/stylis": "0.8.3",
"@emotion/utils": "0.11.1",
"@emotion/weak-memoize": "0.2.2"
}
},
"@emotion/core": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.6.tgz",
"integrity": "sha512-S5KkrodTKby1S6pKZnH8LzjzlebHvjactujfVzzu/mYYdVdKYegJuJdrAz3m9zhIeizzeQGD8xWF490ioGpUtw==",
"requires": {
"@emotion/cache": "10.0.0",
"@emotion/css": "^10.0.6",
"@emotion/serialize": "^0.11.3",
"@emotion/sheet": "0.9.2",
"@emotion/utils": "0.11.1"
}
},
"@emotion/css": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.6.tgz",
"integrity": "sha512-/suYOvP0zeKC3UNoIeN/3zvr/ghUKgfWx0Pht5ZY9qgHis68fB+V45OjonzMbdOw4mGX0vjZzJhINk9JbRWVrg==",
"requires": {
"@emotion/serialize": "^0.11.3",
"@emotion/utils": "0.11.1",
"babel-plugin-emotion": "^10.0.6"
}
},
"@emotion/hash": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz",
"integrity": "sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA=="
},
"@emotion/is-prop-valid": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz",
@ -242,11 +272,63 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz",
"integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg=="
},
"@emotion/serialize": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.3.tgz",
"integrity": "sha512-6Q+XH/7kMdHwtylwZvdkOVMydaGZ989axQ56NF7urTR7eiDMLGun//pFUy31ha6QR4C6JB+KJVhZ3AEAJm9Z1g==",
"requires": {
"@emotion/hash": "0.7.1",
"@emotion/memoize": "0.7.1",
"@emotion/unitless": "0.7.3",
"@emotion/utils": "0.11.1",
"csstype": "^2.5.7"
}
},
"@emotion/sheet": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.2.tgz",
"integrity": "sha512-pVBLzIbC/QCHDKJF2E82V2H/W/B004mDFQZiyo/MSR+VC4pV5JLG0TF/zgQDFvP3fZL/5RTPGEmXlYJBMUuJ+A=="
},
"@emotion/styled": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.6.tgz",
"integrity": "sha512-tJqeGmNueLcdVqQKsaXJq8fGsYg5O8E6GFxNCALHhS+BKl8kXZKn1fnPnz2iNTHuXGsxolt4uzAuj0UD/MF4NQ==",
"requires": {
"@emotion/styled-base": "^10.0.5",
"babel-plugin-emotion": "^10.0.6"
}
},
"@emotion/styled-base": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.0.5.tgz",
"integrity": "sha512-rnz0cg+gZttd4zWb6efH7jZojozou1OtcqhjSvd79v2Iu36zJcD0USzefy9k9nVtPmvyLXYpM+zxqFJXtnKiCQ==",
"requires": {
"@emotion/is-prop-valid": "0.7.3",
"@emotion/serialize": "^0.11.3",
"@emotion/utils": "0.11.1",
"object-assign": "^4.1.1"
}
},
"@emotion/stylis": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.3.tgz",
"integrity": "sha512-M3nMfJ6ndJMYloSIbYEBq6G3eqoYD41BpDOxreE8j0cb4fzz/5qvmqU9Mb2hzsXcCnIlGlWhS03PCzVGvTAe0Q=="
},
"@emotion/unitless": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.3.tgz",
"integrity": "sha512-4zAPlpDEh2VwXswwr/t8xGNDGg8RQiPxtxZ3qQEXyQsBV39ptTdESCjuBvGze1nLMVrxmTIKmnO/nAV8Tqjjzg=="
},
"@emotion/utils": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.1.tgz",
"integrity": "sha512-8M3VN0hetwhsJ8dH8VkVy7xo5/1VoBsDOk/T4SJOeXwTO1c4uIqVNx2qyecLFnnUWD5vvUqHQ1gASSeUN6zcTg=="
},
"@emotion/weak-memoize": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz",
"integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA=="
},
"@types/accepts": {
"version": "1.3.5",
"resolved": "http://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz",
@ -594,17 +676,6 @@
"@types/node": "*"
}
},
"@types/styled-components": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.1.6.tgz",
"integrity": "sha512-w/ra/Tk9oPMvWpWId7esZNY1MOa6E9BYUPLl4scVJdYnrYuy5ITLbku8dGDCVH/vjjuegrHBCRYvFLQOYJ+uHg==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/react": "*",
"csstype": "^2.2.0"
}
},
"@types/tapable": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz",
@ -1170,7 +1241,6 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
@ -1465,20 +1535,35 @@
"babel-runtime": "^6.22.0"
}
},
"babel-plugin-styled-components": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.0.tgz",
"integrity": "sha512-sQVKG8irFXx14ZfaK1bBePirfkacl3j8nZwSZK+ZjsbnadRHKQTbhXbe/RB1vT6Vgkz45E+V95LBq4KqdhZUNw==",
"babel-plugin-emotion": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.6.tgz",
"integrity": "sha512-JD2st8enZJn8h7W1s8kcb40r3RBCJwV9E8ZAhyyhELUMP8OYwyI9K1rz7MxRi0CoorX15kVo2NXZ+OJ6CeMY8A==",
"requires": {
"@babel/helper-annotate-as-pure": "^7.0.0",
"@babel/helper-module-imports": "^7.0.0",
"@emotion/hash": "0.7.1",
"@emotion/memoize": "0.7.1",
"@emotion/serialize": "^0.11.3",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.10"
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^1.0.5",
"find-root": "^1.1.0",
"source-map": "^0.5.7"
}
},
"babel-plugin-macros": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.4.5.tgz",
"integrity": "sha512-+/9yteNQw3yuZ3krQUfjAeoT/f4EAdn3ELwhFfDj0rTMIaoHfIdrcLePOfIaL0qmFLpIcgPIL2Lzm58h+CGWaw==",
"requires": {
"cosmiconfig": "^5.0.5",
"resolve": "^1.8.1"
}
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-register": {
@ -2003,7 +2088,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
"integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
"dev": true,
"requires": {
"callsites": "^2.0.0"
}
@ -2012,7 +2096,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
"integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
"dev": true,
"requires": {
"caller-callsite": "^2.0.0"
}
@ -2020,8 +2103,7 @@
"callsites": {
"version": "2.0.0",
"resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
"dev": true
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA="
},
"camel-case": {
"version": "3.0.0",
@ -2474,7 +2556,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
"integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.1"
}
@ -2523,7 +2604,6 @@
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz",
"integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==",
"dev": true,
"requires": {
"import-fresh": "^2.0.0",
"is-directory": "^0.3.1",
@ -2541,6 +2621,17 @@
"elliptic": "^6.0.0"
}
},
"create-emotion": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.6.tgz",
"integrity": "sha512-pfaSo7swLlmHxizPYF5Oi09e1uPseueX/CirE6mZpPeeoH1Yb4I2cmx0VCN6KlCRko4yKFTErorect9y0+ARZQ==",
"requires": {
"@emotion/cache": "10.0.0",
"@emotion/serialize": "^0.11.3",
"@emotion/sheet": "0.9.2",
"@emotion/utils": "0.11.1"
}
},
"create-hash": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
@ -2696,11 +2787,6 @@
}
}
},
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
},
"css-color-names": {
"version": "0.0.4",
"resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@ -2998,37 +3084,6 @@
"regexpu-core": "^1.0.0"
}
},
"css-to-react-native": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.2.2.tgz",
"integrity": "sha512-w99Fzop1FO8XKm0VpbQp3y5mnTnaS+rtCvS+ylSEOK76YXO5zoHQx/QMB1N54Cp+Ya9jB9922EHrh14ld4xmmw==",
"requires": {
"css-color-keywords": "^1.0.0",
"fbjs": "^0.8.5",
"postcss-value-parser": "^3.3.0"
},
"dependencies": {
"core-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
},
"fbjs": {
"version": "0.8.17",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
"integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
"requires": {
"core-js": "^1.0.0",
"isomorphic-fetch": "^2.1.1",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.18"
}
}
}
},
"css-tree": {
"version": "1.0.0-alpha.28",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz",
@ -3256,8 +3311,7 @@
"csstype": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.1.tgz",
"integrity": "sha512-wv7IRqCGsL7WGKB8gPvrl+++HlFM9kxAM6jL1EXNPNTshEJYilMkbfS2SnuHha77uosp/YVK0wAp2jmlBzn1tg==",
"dev": true
"integrity": "sha512-wv7IRqCGsL7WGKB8gPvrl+++HlFM9kxAM6jL1EXNPNTshEJYilMkbfS2SnuHha77uosp/YVK0wAp2jmlBzn1tg=="
},
"currently-unhandled": {
"version": "0.4.1",
@ -3590,6 +3644,15 @@
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
},
"emotion": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.6.tgz",
"integrity": "sha512-k+3eFJ9OqzwZdRVahPB4Gt9C7BLfOnzYHnD++QrGsrDno1VU1IEOBKdCsgkRneNaFVkUjg3DCmBlgE6GGwd27w==",
"requires": {
"babel-plugin-emotion": "^10.0.6",
"create-emotion": "^10.0.6"
}
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
@ -3637,7 +3700,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"requires": {
"is-arrayish": "^0.2.1"
}
@ -3726,8 +3788,7 @@
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esrecurse": {
"version": "4.2.1",
@ -4016,6 +4077,11 @@
"pkg-dir": "^3.0.0"
}
},
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
@ -5256,7 +5322,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
"integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
"dev": true,
"requires": {
"caller-path": "^2.0.0",
"resolve-from": "^3.0.0"
@ -5369,8 +5434,7 @@
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-binary-path": {
"version": "1.0.1",
@ -5464,8 +5528,7 @@
"is-directory": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
"dev": true
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE="
},
"is-extendable": {
"version": "0.1.1",
@ -5689,7 +5752,6 @@
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -5710,8 +5772,7 @@
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
},
"json-schema": {
"version": "0.2.3",
@ -6273,11 +6334,6 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"memoize-one": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz",
"integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw=="
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -6519,6 +6575,20 @@
"minimist": "0.0.8"
}
},
"mobx": {
"version": "4.9.2",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-4.9.2.tgz",
"integrity": "sha512-dMGeM4emHDTaXEA/Va4NhWSiEk8XkdcFQl4uiictXoou0Vbk0Sup4H/HDWGYcXVeRAWGPDYXDDkz1gXK4B1eZw=="
},
"mobx-react": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-5.4.3.tgz",
"integrity": "sha512-WC8yFlwvJ91hy8j6CrydAuFteUafcuvdITFQeHl3LRIf5ayfT/4W3M/byhEYD2BcJWejeXr8y4Rh2H26RunCRQ==",
"requires": {
"hoist-non-react-statics": "^3.0.0",
"react-lifecycles-compat": "^3.0.2"
}
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -7146,7 +7216,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"dev": true,
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
@ -7194,8 +7263,7 @@
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "1.7.0",
@ -9512,7 +9580,8 @@
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true
},
"prebuild-install": {
"version": "5.2.0",
@ -10200,7 +10269,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz",
"integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
@ -10208,8 +10276,7 @@
"resolve-from": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
"dev": true
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
},
"resolve-path": {
"version": "1.4.0",
@ -10739,8 +10806,7 @@
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
},
"source-map-resolve": {
"version": "0.5.2",
@ -10823,8 +10889,7 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sshpk": {
"version": "1.16.0",
@ -11008,24 +11073,6 @@
"dev": true,
"optional": true
},
"styled-components": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.1.3.tgz",
"integrity": "sha512-0quV4KnSfvq5iMtT0RzpMGl/Dg3XIxIxOl9eJpiqiq4SrAmR1l1DLzNpMzoy3DyzdXVDMJS2HzROnXscWA3SEw==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/is-prop-valid": "^0.7.3",
"@emotion/unitless": "^0.7.0",
"babel-plugin-styled-components": ">= 1",
"css-to-react-native": "^2.2.2",
"memoize-one": "^4.0.0",
"prop-types": "^15.5.4",
"react-is": "^16.6.0",
"stylis": "^3.5.0",
"stylis-rule-sheet": "^0.0.10",
"supports-color": "^5.5.0"
}
},
"stylehacks": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.1.tgz",
@ -11067,16 +11114,6 @@
}
}
},
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
},
"stylis-rule-sheet": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
},
"subscriptions-transport-ws": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz",

@ -1,6 +1,6 @@
{
"name": "reactql",
"version": "3.7.1",
"version": "4.0.0",
"description": "ReactQL - front-end React/GraphQL starter kit",
"main": "index.js",
"scripts": {
@ -38,12 +38,11 @@
"@types/react-router-dom": "^4.3.1",
"@types/require-from-string": "^1.2.0",
"@types/source-map-support": "^0.4.1",
"@types/styled-components": "^4.1.6",
"@types/webpack": "^4.4.23",
"@types/webpack-node-externals": "^1.6.3",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.5",
"babel-plugin-styled-components": "^1.10.0",
"babel-plugin-emotion": "^10.0.6",
"brotli-webpack-plugin": "^1.0.0",
"compression-webpack-plugin": "^2.0.0",
"css-hot-loader": "^1.4.3",
@ -71,6 +70,8 @@
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"@emotion/core": "^10.0.6",
"@emotion/styled": "^10.0.6",
"apollo-cache-inmemory": "^1.4.2",
"apollo-client": "^2.4.12",
"apollo-link": "^1.2.6",
@ -83,6 +84,7 @@
"cross-env": "^5.2.0",
"cross-fetch": "^3.0.0",
"dotenv": "^6.2.0",
"emotion": "^10.0.6",
"graphql": "^14.1.1",
"graphql-tag": "^2.10.1",
"history": "^4.7.2",
@ -92,6 +94,8 @@
"koa-send": "^5.0.0",
"lodash": "^4.17.11",
"microseconds": "^0.1.0",
"mobx": "^4.9.2",
"mobx-react": "^5.4.3",
"ora": "^3.0.0",
"react": "^16.7.0",
"react-addons-css-transition-group": "^15.6.2",
@ -101,7 +105,6 @@
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.6.3",
"react-router-dom": "^4.3.1",
"styled-components": "^4.1.3",
"subscriptions-transport-ws": "^0.9.15"
}
}

@ -6,48 +6,26 @@
/* NPM */
import * as React from "react";
// Use the `<Mutation>` component from the React Apollo lib to set-up the
// mutation block that will allow us to fire up GraphQL mutations. We'll also
// grab the `<Query>` component, because we'll want to 'listen' to the current
// count as it changes
import { Mutation, Query } from "react-apollo";
/* Local */
// Get the Typescript types of our local state, so we can use them
// with our GraphQL queries to hint at the data that should be returned
import { IRoot } from "@/graphql/state";
// Get the current count, stored in local Apollo state
import getCount from "@/queries/getCount";
// Mutation to increment the local counter
import incrementCount from "@/mutations/incrementCount";
// `<StateConsumer>` takes a function and passes it our MobX
// state. Any time the state changes, the children will automatically
// re-render -- no HOCs or boilerplate required!
import { StateConsumer } from "@/lib/mobx";
// ----------------------------------------------------------------------------
export default () => (
<Mutation mutation={incrementCount}>
{doIncrementCount => {
return (
<Query<IRoot> query={getCount}>
{({ data }) => {
// Create an `onClick` handler to run the mutation
function buttonClick() {
return doIncrementCount();
}
return (
<>
<h3>
Current count (from local GraphQL state): {data!.state.count}
</h3>
<button onClick={buttonClick}>Increment</button>
</>
);
}}
</Query>
);
}}
</Mutation>
);
export default class Count extends React.Component {
public render() {
return (
<StateConsumer>
{state => (
<>
<h3>Current count (from MobX): {state.count}</h3>
<button onClick={state.increment}>Increment</button>
</>
)}
</StateConsumer>
);
}
}

@ -10,10 +10,10 @@ import * as React from "react";
// fetch the GraphQL data, to display as part of our component
import { Query } from "react-apollo";
/* Local */
// Emotion styled component
import styled from "@emotion/styled";
// Styled component lib for generating CSS in lieu of using SASS
import styled from "@/lib/styledComponents";
/* Local */
// Query to get top stories from HackerNews
import hackerNewsQuery from "@/queries/getHackerNewsTopStories";
@ -36,13 +36,17 @@ interface IHackerNewsTopStories {
};
}
// Unstyled Emotion parent block, to avoid repeating <style> tags
// on child elements -- see https://github.com/emotion-js/emotion/issues/1061
const List = styled.ul``;
// Style the list item so it overrides the default font
const Story = styled.li`
const Story = styled("li")`
font-size: 16px;
a:hover {
/* shows an example of how we can use themes */
color: ${props => props.theme.colors.orange};
color: orange;
}
`;
@ -70,7 +74,7 @@ export default () => (
return (
<>
<h3>Top stories from Hacker News</h3>
<ul>
<List>
{result.data!.hn.topStories.map(story => (
<Story key={story.id}>
<a href={story.url} target="_blank">
@ -78,7 +82,7 @@ export default () => (
</a>
</Story>
))}
</ul>
</List>
</>
);
}}

@ -8,6 +8,7 @@ import * as React from "react";
import Helmet from "react-helmet";
import { hot } from "react-hot-loader";
import { Route, Switch } from "react-router-dom";
import { Global } from "@emotion/core";
/* Local */
@ -15,7 +16,7 @@ import { Route, Switch } from "react-router-dom";
import ScrollTop from "@/components/helpers/scrollTop";
// Global styles
import { GlobalStyles } from "@/global/styles";
import globalStyles from "@/global/styles";
// Routes
import routes from "@/data/routes";
@ -24,7 +25,7 @@ import routes from "@/data/routes";
const Root = () => (
<div>
<GlobalStyles />
<Global styles={globalStyles} />
<Helmet>
<title>ReactQL starter kit - edit me!</title>
</Helmet>

@ -0,0 +1,12 @@
import { action, observable } from "mobx";
export class State {
@observable count = 0;
@action public increment = () => {
console.log("current:", this.count);
this.count = this.count + 1;
};
@action public decrement = () => {
--this.count;
};
}

@ -26,30 +26,34 @@ import { Router } from "react-router-dom";
import Root from "@/components/root";
// Helper function that creates a new Apollo client per request
import { createClient } from "@/graphql/apollo";
import { createClient } from "@/lib/apollo";
// For Styled Components theming
import { ThemeProvider } from "@/lib/styledComponents";
// ... and the actual Styled Components theme
import defaultTheme from "@/themes/default";
// MobX state
import { State } from "@/data/state";
import { rehydrate, StateProvider } from "@/lib/mobx";
// ----------------------------------------------------------------------------
// Create Apollo client
const client = createClient();
// Create new MobX state
const state = new State();
// Create a browser history
const history = createBrowserHistory();
// Rehydrate MobX state, if applicable
rehydrate(state);
// Render
ReactDOM.hydrate(
<ThemeProvider theme={defaultTheme}>
<StateProvider value={state}>
<ApolloProvider client={client}>
<Router history={history}>
<Root />
</Router>
</ApolloProvider>
</ThemeProvider>,
</StateProvider>,
document.getElementById("root")
);

@ -4,13 +4,22 @@
// IMPORTS
/* NPM */
// Implement a global `fetch()` polyfill, for Apollo requests
import "cross-fetch/polyfill";
// React for UI
import * as React from "react";
// The `Context` type for the Koa HTTP server
import { Context } from "koa";
import * as React from "react";
// Apollo GraphQL
import { ApolloProvider, getDataFromTree } from "react-apollo";
// MobX state management
import { toJS } from "mobx";
// React utility to transform JSX to HTML (to send back to the client)
import * as ReactDOMServer from "react-dom/server";
@ -18,15 +27,27 @@ import * as ReactDOMServer from "react-dom/server";
// title, meta info, etc along with the initial HTML
import Helmet from "react-helmet";
// React SSR routers
import { StaticRouter } from "react-router";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
/* Local */
// Root component
import Root from "@/components/root";
import { createClient } from "@/graphql/apollo";
// Utility for creating a per-request Apollo client
import { createClient } from "@/lib/apollo";
// State class, containing all of our user-land state fields
import { State } from "@/data/state";
// <StateProvider> lets us send per-request state down a React chain
import { StateProvider } from "@/lib/mobx";
// Class for handling Webpack stats output
import Output from "@/lib/output";
import { ThemeProvider } from "@/lib/styledComponents";
import defaultTheme from "@/themes/default";
// Every byte sent back to the client is React; this is our main template
import Html from "@/views/ssr";
// ----------------------------------------------------------------------------
@ -37,34 +58,36 @@ export interface IRouterContext {
url?: string;
}
// Everything from this point will be Webpack'd and dumped in `dist/server.js`
// and then loaded into an active Koa server
export default function(output: Output) {
// Create Koa middleware to handle React requests
return async (ctx: Context) => {
// Create a new Apollo client
const client = createClient();
// Create a new styled-components instance
const sheet = new ServerStyleSheet();
// Create new MobX state
const state = new State();
// Create a fresh 'context' for React Router
const routerContext: IRouterContext = {};
// Render our components - passing down MobX state, a GraphQL client,
// and a router for rendering based on our route config
const components = (
<StyleSheetManager sheet={sheet.instance}>
<ThemeProvider theme={defaultTheme}>
<ApolloProvider client={client}>
<StaticRouter location={ctx.request.url} context={routerContext}>
<Root />
</StaticRouter>
</ApolloProvider>
</ThemeProvider>
</StyleSheetManager>
<StateProvider value={state}>
<ApolloProvider client={client}>
<StaticRouter location={ctx.request.url} context={routerContext}>
<Root />
</StaticRouter>
</ApolloProvider>
</StateProvider>
);
// Render the Apollo tree
// Await GraphQL data coming from the API server
await getDataFromTree(components);
// Handle redirects
// Handle 301/302 redirects
if ([301, 302].includes(routerContext.status!)) {
// 301 = permanent redirect, 302 = temporary
ctx.status = routerContext.status!;
@ -76,7 +99,7 @@ export default function(output: Output) {
return;
}
// Handle 404 Not Found
// Handle 404 `Not Found`
if (routerContext.status === 404) {
// By default, just set the status code to 404. You can
// modify this section to do things like log errors to a
@ -91,21 +114,22 @@ export default function(output: Output) {
// Create response HTML
const html = ReactDOMServer.renderToString(components);
// Create the React render via React Helmet
// Create the React render, and inject the `<head>` section
// courtesy of React Helmet.
const reactRender = ReactDOMServer.renderToString(
<Html
css={output.client.main("css")!}
helmet={Helmet.renderStatic()}
html={html}
js={output.client.main("js")!}
styles={sheet.getStyleElement()}
scripts={output.client.scripts()}
window={{
__APOLLO_STATE__: client.extract()
__APOLLO_STATE__: client.extract(), // <-- GraphQL store
__STATE__: toJS(state) // <-- MobX state
}}
/>
);
// Set the return type to `text/html`, and stream the response back to
// Set the return type to `text/html`, and dump the response back to
// the client
ctx.type = "text/html";
ctx.body = `<!DOCTYPE html>${reactRender}`;

@ -9,15 +9,15 @@
which is automatically included along with our SSR / initial HTML. This
is for processing CSS through the SASS/LESS -> PostCSS pipeline.
2. It exports a <GlobalStyles /> component which is used by @components/root.tsx
to add global styles to the React render.
2. It exports a global styles template which is used by Emotion to generate
styles that apply to all pages.
/*
// ----------------------------------------------------------------------------
// IMPORTS
/* NPM */
import { createGlobalStyle } from "@/lib/styledComponents";
import { css } from "@emotion/core";
/* Local */
@ -28,13 +28,10 @@ import "./styles.global.scss";
// ----------------------------------------------------------------------------
// Inject Styled-Components output onto the page. You can add global styles to
// the template tags below, and will be picked up in @components/root.tsx
export const GlobalStyles = createGlobalStyle`
/* Set a default style for all <h1> tags
*/
// Global styles to apply
export default css`
/* Make all <h1> tags orange */
h1 {
background-color: ${props => props.theme.colors.orange};
background-color: orange;
}
`;

@ -1,79 +0,0 @@
// Local GraphQL state
// ----------------------------------------------------------------------------
// IMPORTS
/* NPM */
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloLink } from "apollo-link";
import { ClientStateConfig, withClientState } from "apollo-link-state";
/* Local */
// Queries
import getCountQuery from "@/queries/getCount";
// ----------------------------------------------------------------------------
// Types
/* STATE */
export interface IState {
count: number;
}
// 'Root', which contains the 'State' key
export interface IRoot {
state: IState;
}
export default function createState(cache: InMemoryCache): ApolloLink {
// Helper function to retrieve the state from cache
function getState(query: any): IState {
return cache.readQuery<IRoot>({ query }).state;
}
// Helper function to write data back to the cache
function writeState(state: IState) {
return cache.writeData({ data: { state } });
}
const opt: ClientStateConfig = {
cache,
resolvers: {
Mutation: {
// Sample mutation to increment the local `count` by 1
incrementCount() {
// Get the existing state
const state = getState(getCountQuery);
// Create new state. Note that we're assigning this to a new
// constant, and not simply incrementing the existing `count`
// key on the state we retrieved. We use this immutable pattern
// so Apollo can see that we have a brand new object to write
// to the cache
const newState = {
...state,
count: state.count + 1
};
// Write the new count var to the cache
writeState(newState);
// ... and return it back to the calling function, which will
// then become our response data
return newState;
}
}
}
};
opt.defaults = {
state: {
__typename: "State",
count: 0
}
} as IRoot;
return withClientState(opt);
}

@ -13,9 +13,6 @@ import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import { SubscriptionClient } from "subscriptions-transport-ws";
/* Local */
import createState from "./state";
// ----------------------------------------------------------------------------
export function createClient(): ApolloClient<NormalizedCacheObject> {
@ -64,10 +61,6 @@ export function createClient(): ApolloClient<NormalizedCacheObject> {
}
}),
// Connect local Apollo state. This is our primary mechanism for
// managing 'flux'/local app data, in lieu of Redux or MobX
createState(cache),
// Split on HTTP and WebSockets </