From 0e89dee96cb00da10a82a918fe6b23cc25265fb2 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Tue, 17 Apr 2018 10:16:42 -0700 Subject: [PATCH 01/14] Add automatic SSR data hydration support --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++ lib/DataHydrator.js | 45 ++++++++++++++++++++++++++++++++++ lib/SSRDataStore.js | 39 +++++++++++++++++++++++++++++ lib/utils.js | 3 +++ lib/withReactiveQuery.js | 52 +++++++++++++++++++++++++++++---------- lib/withStaticQuery.js | 41 +++++++++++++++++++++++++++---- main.client.js | 6 ++++- main.server.js | 6 ++++- package.js | 1 + withQuery.js | 22 +++++++++++------ 10 files changed, 241 insertions(+), 27 deletions(-) create mode 100644 lib/DataHydrator.js create mode 100644 lib/SSRDataStore.js create mode 100644 lib/utils.js diff --git a/README.md b/README.md index b0fc799..ba1503b 100644 --- a/README.md +++ b/README.md @@ -255,3 +255,56 @@ export default withQuery((props) => { loadingComponent: AnotherLoadingComponent, })(UserProfile) ``` +### Server Side Rendering + +One common challenge with server-side rendering is hydrating client. The data used in the view needs to be available when the page loads so that React can hydrate the DOM with no differences. This technique will allow you send all of the necessary data with the initial HTML payload. The code below works with `withQuery` to track and hydrate data automatically. Works with static queries and subscriptions. + +On the server: + +```jsx harmony +import { onPageLoad } from 'meteor/server-render' +import { SSRDataStore } from 'meteor/cultofcoders:grapher-react' + +onPageLoad(async sink => { + const store = new SSRDataStore() + + sink.renderIntoElementById( + 'root', + renderToString( + store.collectData() + ) + ) + + const storeTags = store.getScriptTags() + sink.appendToBody(storeTags) +}) +``` + +On the client: + +```jsx harmony +import { DataHydrator } from 'meteor/cultofcoders:grapher-react' + +Meteor.startup(async () => { + await DataHydrator.load() + ReactDOM.hydrate(, document.getElementById('root')) +}) +``` + +Use `withQuery` on a component: + +```jsx harmony +const SomeLoader = ({ data, isLoading, error }) => { + if (error) { + return
{error.reason}
+ } + + return +} + +export default withQuery( + props => { + return GetSome.clone() + }, +)(SomeLoader) +``` \ No newline at end of file diff --git a/lib/DataHydrator.js b/lib/DataHydrator.js new file mode 100644 index 0000000..70da06c --- /dev/null +++ b/lib/DataHydrator.js @@ -0,0 +1,45 @@ +import { Promise } from 'meteor/promise' +import { generateQueryId } from './utils.js'; + +export default { + + decodeData(data) { + const decodedEjsonString = decodeURIComponent(data); + if (!decodedEjsonString) return null; + + return EJSON.parse(decodedEjsonString); + }, + + load(optns) { + const defaults = { + selfDestruct: 3000 + } + const options = Object.assign({}, defaults, optns) + + return new Promise((resolve, reject) => { + // Retrieve the payload from the DOM + const dom = document.querySelectorAll( + 'script[type="text/grapher-data"]', + document + ); + const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; + const data = this.decodeData(dataString) || {}; + window.grapherQueryStore = data + + // Self destruct the store so that dynamically loaded modules + // do not pull from the store in the future + setTimeout(() => { + window.grapherQueryStore = {}; + }, options.selfDestruct) + + resolve(data); + }); + }, + + getQueryData(query) { + const id = generateQueryId(query); + const data = window.grapherQueryStore[id] + return data + } + +} \ No newline at end of file diff --git a/lib/SSRDataStore.js b/lib/SSRDataStore.js new file mode 100644 index 0000000..0be4fbc --- /dev/null +++ b/lib/SSRDataStore.js @@ -0,0 +1,39 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import { EJSON } from 'meteor/ejson'; +import { generateQueryId } from './utils.js'; +export const SSRDataStoreContext = React.createContext(null); + +class DataStore { + storage = {} + + add(query, value) { + const key = generateQueryId(query) + this.storage[key] = value + } + + getData() { + return this.storage + } +} + +export default class SSRDataStore{ + constructor() { + this.store = new DataStore() + } + + collectData(children) { + return {children} + } + + encodeData(data) { + data = EJSON.stringify(data) + return encodeURIComponent(data) + } + + getScriptTags() { + const data = this.store.getData() + + return `` + } +} \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..39e3dd6 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,3 @@ +export const generateQueryId = function (query) { + return `${query.queryName}::${EJSON.stringify(query.params)}`; +} \ No newline at end of file diff --git a/lib/withReactiveQuery.js b/lib/withReactiveQuery.js index bf2ce9f..86c73f1 100644 --- a/lib/withReactiveQuery.js +++ b/lib/withReactiveQuery.js @@ -1,5 +1,7 @@ import {withTracker} from 'meteor/react-meteor-data'; import {ReactiveVar} from 'meteor/reactive-var'; +import DataHydrator from './DataHydrator.js'; +import { Meteor } from 'meteor/meteor'; /** * Wraps the query and provides reactive data fetching utility @@ -14,24 +16,48 @@ export default function withReactiveContainer(handler, config, QueryComponent) { return withTracker((props) => { const query = handler(props); - const subscriptionHandle = query.subscribe({ - onStop(err) { - if (err) { - subscriptionError.set(err); - } - }, - onReady() { - subscriptionError.set(null); - } - }); + let isLoading + let data + let error - const isReady = subscriptionHandle.ready(); + // For server-side-rendering, immediately fetch the data + // and save it in the data store for this request + + if(Meteor.isServer) { + data = query.fetch(); + isLoading = false; + props.dataStore.add(query, data); + } else { + const subscriptionHandle = query.subscribe({ + onStop(err) { + if (err) { + subscriptionError.set(err); + } + }, + onReady() { + subscriptionError.set(null); + } + }); + + isLoading = !subscriptionHandle.ready(); - const data = query.fetch(); + // Check the SSR query store for data + if(Meteor.isClient && window && window.grapherQueryStore) { + const ssrData = DataHydrator.getQueryData(query); + if(ssrData) { + isLoading = false; + data = ssrData; + } + } + if(!data) { + data = query.fetch(); + } + } + return { grapher: { - isLoading: !isReady, + isLoading, data, error: subscriptionError, }, diff --git a/lib/withStaticQuery.js b/lib/withStaticQuery.js index e963f02..1cbf39f 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -1,5 +1,7 @@ import React from 'react'; import getDisplayName from './getDisplayName'; +import { Meteor } from 'meteor/meteor'; +import DataHydrator from './DataHydrator.js'; export default function withStaticQueryContainer(WrappedComponent) { /** @@ -7,11 +9,40 @@ export default function withStaticQueryContainer(WrappedComponent) { * This is a standard pattern in HOCs */ class GrapherStaticQueryContainer extends React.Component { - state = { - isLoading: true, - error: null, - data: [], - }; + constructor(props) { + super(props); + + this.state = { + isLoading: true, + error: null, + data: [], + }; + + const { query } = props; + + // Check the SSR query store for data + if(Meteor.isClient && window && window.grapherQueryStore) { + const data = DataHydrator.getQueryData(query); + if(data) { + this.state = { + isLoading: false, + data + }; + + } + } + + // For server-side-rendering, immediately fetch the data + // and save it in the data store for this request + if(Meteor.isServer) { + const data = query.fetch(); + this.state = { + isLoading: false, + data + }; + props.dataStore.add(query, data); + } + } componentWillReceiveProps(nextProps) { const {query} = nextProps; diff --git a/main.client.js b/main.client.js index 4219b6f..02d7579 100644 --- a/main.client.js +++ b/main.client.js @@ -15,4 +15,8 @@ export { export { default as createQueryContainer -} from './legacy/createQueryContainer.js'; \ No newline at end of file +} from './legacy/createQueryContainer.js'; + +export { + default as DataHydrator +} from './lib/DataHydrator.js'; \ No newline at end of file diff --git a/main.server.js b/main.server.js index 4219b6f..1ac78c1 100644 --- a/main.server.js +++ b/main.server.js @@ -15,4 +15,8 @@ export { export { default as createQueryContainer -} from './legacy/createQueryContainer.js'; \ No newline at end of file +} from './legacy/createQueryContainer.js'; + +export { + default as SSRDataStore +} from './lib/SSRDataStore.js'; \ No newline at end of file diff --git a/package.js b/package.js index 7fe5c51..0578aa4 100644 --- a/package.js +++ b/package.js @@ -20,6 +20,7 @@ Package.onUse(function (api) { 'react-meteor-data@0.2.15', 'cultofcoders:grapher@1.2.8_1', 'tmeasday:check-npm-versions@0.2.0', + 'ejson' ]); api.mainModule('main.client.js', 'client'); diff --git a/withQuery.js b/withQuery.js index 1cf2ede..17444a0 100644 --- a/withQuery.js +++ b/withQuery.js @@ -5,6 +5,7 @@ import withReactiveQuery from './lib/withReactiveQuery'; import withQueryContainer from './lib/withQueryContainer'; import withStaticQuery from './lib/withStaticQuery'; import checkOptions from './lib/checkOptions'; +import { SSRDataStoreContext } from './lib/SSRDataStore.js' export default function (handler, _config = {}) { checkOptions(_config); @@ -14,19 +15,26 @@ export default function (handler, _config = {}) { const queryContainer = withQueryContainer(component); if (!config.reactive) { - const staticQueryContainer = withStaticQuery(queryContainer); + const StaticQueryContainer = withStaticQuery(queryContainer); return function (props) { const query = handler(props); - return React.createElement(staticQueryContainer, { - query, - props, - config - }) + return ( + + {dataStore => } + + ) } } else { - return withReactiveQuery(handler, config, queryContainer); + const ReactiveQueryContainer = withReactiveQuery(handler, config, queryContainer); + return function(props) { + return ( + + {dataStore => } + + ) + } } }; } \ No newline at end of file From 4ad37cdca1ed325c6604035671bd11e2b63cf9b1 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Tue, 17 Apr 2018 10:23:14 -0700 Subject: [PATCH 02/14] Do not load static queries twice --- lib/withStaticQuery.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/withStaticQuery.js b/lib/withStaticQuery.js index 1cbf39f..61d1766 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -51,7 +51,11 @@ export default function withStaticQueryContainer(WrappedComponent) { componentDidMount() { const {query, config} = this.props; - this.fetch(query); + + // Do not fetch is we already have the data from SSR hydration + if(this.state.isLoading === true) { + this.fetch(query); + } if (config.pollingMs) { this.pollingInterval = setInterval(() => { From 72dbfa55f7498e1edeabe5671d1aac442f901c40 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Mon, 23 Apr 2018 15:51:21 -0700 Subject: [PATCH 03/14] Add props to ReactiveQueryContainer --- withQuery.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/withQuery.js b/withQuery.js index 17444a0..66ff4ef 100644 --- a/withQuery.js +++ b/withQuery.js @@ -31,7 +31,7 @@ export default function (handler, _config = {}) { return function(props) { return ( - {dataStore => } + {dataStore => } ) } From 203a1048c4ebab4c5c38afa868ea6a3202db05d3 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Tue, 24 Apr 2018 16:55:54 -0700 Subject: [PATCH 04/14] Fix component names in dev tools --- withQuery.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/withQuery.js b/withQuery.js index 66ff4ef..ce98109 100644 --- a/withQuery.js +++ b/withQuery.js @@ -1,6 +1,5 @@ import React from 'react'; import defaults from './defaults'; -import {withTracker} from 'meteor/react-meteor-data'; import withReactiveQuery from './lib/withReactiveQuery'; import withQueryContainer from './lib/withQueryContainer'; import withStaticQuery from './lib/withStaticQuery'; @@ -12,12 +11,13 @@ export default function (handler, _config = {}) { const config = Object.assign({}, defaults, _config); return function (component) { + let C const queryContainer = withQueryContainer(component); if (!config.reactive) { const StaticQueryContainer = withStaticQuery(queryContainer); - return function (props) { + C = function (props) { const query = handler(props); return ( @@ -28,7 +28,7 @@ export default function (handler, _config = {}) { } } else { const ReactiveQueryContainer = withReactiveQuery(handler, config, queryContainer); - return function(props) { + C = function(props) { return ( {dataStore => } @@ -36,5 +36,11 @@ export default function (handler, _config = {}) { ) } } + + + C.displayName = `withQuery(${component.displayName || component.name})` + C.WrappedComponent = component + + return C }; } \ No newline at end of file From 5f00bef854b64947514a1471de469113f24f2be8 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 11:55:00 -0700 Subject: [PATCH 05/14] Add prettier --- .prettierrc.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .prettierrc.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..7ae51fb --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + trailingComma: 'es5', + singleQuote: true, + tabWidth: 2, +} From 5902caab02cd5353ab72ef99d7d77a874af708d0 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 11:55:04 -0700 Subject: [PATCH 06/14] Add eslint --- .eslintrc.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2fb17e9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + extends: [ + 'standard', + 'plugin:meteor/recommended', + 'plugin:react/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + ], + plugins: ['react', 'meteor', 'import'], + settings: { + 'import/resolver': { + meteor: { + extensions: ['.js', '.jsx'], + }, + }, + }, + rules: { + // "import/no-duplicates": 0, + // "react/display-name": 0, + 'comma-dangle': 0, + 'space-before-function-paren': 0, + // "indent": [ + // "error", + // "tab" + // ], + semi: 0, + // "meteor/no-session": 0 + }, +}; From 26efcf62662634798bfbe8792c12531fdf14994d Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 11:55:10 -0700 Subject: [PATCH 07/14] Prettier reformat --- .vscode/settings.json | 9 ++ defaults.js | 6 +- legacy/createQueryContainer.js | 122 +++++++++++----------- lib/DataHydrator.js | 82 ++++++++------- lib/SSRDataStore.js | 62 ++++++------ lib/checkOptions.js | 28 +++--- lib/getDisplayName.js | 4 +- lib/utils.js | 6 +- lib/withQueryContainer.js | 76 +++++++------- lib/withReactiveQuery.js | 120 +++++++++++----------- lib/withStaticQuery.js | 179 +++++++++++++++++---------------- main.client.js | 21 ++-- main.server.js | 21 ++-- package.js | 74 +++++++------- setDefaults.js | 4 +- withQuery.js | 92 +++++++++-------- 16 files changed, 466 insertions(+), 440 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1af217f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.tabSize": 2, + "editor.formatOnSave": true, + "eslint.enable": true, + "eslint.provideLintTask": true, + "eslint.autoFixOnSave": false, + "javascript.validate.enable": false, + "git.ignoreLimitWarning": true +} diff --git a/defaults.js b/defaults.js index b01e788..7166687 100644 --- a/defaults.js +++ b/defaults.js @@ -1,4 +1,4 @@ export default { - reactive: false, - single: false, -} \ No newline at end of file + reactive: false, + single: false, +}; diff --git a/legacy/createQueryContainer.js b/legacy/createQueryContainer.js index 63daf4d..af9e803 100644 --- a/legacy/createQueryContainer.js +++ b/legacy/createQueryContainer.js @@ -1,76 +1,80 @@ import React from 'react'; -import {createContainer} from 'meteor/react-meteor-data'; +import { createContainer } from 'meteor/react-meteor-data'; export default (query, component, options = {}) => { - if (Meteor.isDevelopment) { - console.warn('createQueryContainer() is deprecated, please use withQuery() instead') - } + if (Meteor.isDevelopment) { + console.warn( + 'createQueryContainer() is deprecated, please use withQuery() instead' + ); + } - if (options.reactive) { - return createContainer((props) => { - if (props.params) { - query.setParams(props.params); - } + if (options.reactive) { + return createContainer(props => { + if (props.params) { + query.setParams(props.params); + } - const handler = query.subscribe(); + const handler = query.subscribe(); - return { - query, - loading: !handler.ready(), - [options.dataProp]: options.single ? _.first(query.fetch()) : query.fetch(), - ...props - } - }, component); - } + return { + query, + loading: !handler.ready(), + [options.dataProp]: options.single + ? _.first(query.fetch()) + : query.fetch(), + ...props, + }; + }, component); + } - class MethodQueryComponent extends React.Component { - constructor() { - super(); - this.state = { - [options.dataProp]: undefined, - error: undefined, - loading: true - } - } + class MethodQueryComponent extends React.Component { + constructor() { + super(); + this.state = { + [options.dataProp]: undefined, + error: undefined, + loading: true, + }; + } - componentWillReceiveProps(nextProps) { - this._fetch(nextProps.params); - } + componentWillReceiveProps(nextProps) { + this._fetch(nextProps.params); + } - componentDidMount() { - this._fetch(this.props.params); - } + componentDidMount() { + this._fetch(this.props.params); + } - _fetch(params) { - if (params) { - query.setParams(params); - } + _fetch(params) { + if (params) { + query.setParams(params); + } - query.fetch((error, data) => { - const state = { - error, - [options.dataProp]: options.single ? _.first(data) : data, - loading: false - }; + query.fetch((error, data) => { + const state = { + error, + [options.dataProp]: options.single ? _.first(data) : data, + loading: false, + }; - this.setState(state); - }); - } + this.setState(state); + }); + } - render() { - const {state, props} = this; + render() { + const { state, props } = this; - return React.createElement(component, { - query, - ...state, - ...props - }) - } + return React.createElement(component, { + query, + ...state, + ...props, + }); } + } - MethodQueryComponent.propTypes = { - params: React.PropTypes.object - }; + MethodQueryComponent.propTypes = { + params: React.PropTypes.object, + }; - return MethodQueryComponent; -} \ No newline at end of file + return MethodQueryComponent; +}; diff --git a/lib/DataHydrator.js b/lib/DataHydrator.js index 70da06c..dd83234 100644 --- a/lib/DataHydrator.js +++ b/lib/DataHydrator.js @@ -1,45 +1,43 @@ -import { Promise } from 'meteor/promise' +import { Promise } from 'meteor/promise'; import { generateQueryId } from './utils.js'; export default { - - decodeData(data) { - const decodedEjsonString = decodeURIComponent(data); - if (!decodedEjsonString) return null; - - return EJSON.parse(decodedEjsonString); - }, - - load(optns) { - const defaults = { - selfDestruct: 3000 - } - const options = Object.assign({}, defaults, optns) - - return new Promise((resolve, reject) => { - // Retrieve the payload from the DOM - const dom = document.querySelectorAll( - 'script[type="text/grapher-data"]', - document - ); - const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; - const data = this.decodeData(dataString) || {}; - window.grapherQueryStore = data - - // Self destruct the store so that dynamically loaded modules - // do not pull from the store in the future - setTimeout(() => { - window.grapherQueryStore = {}; - }, options.selfDestruct) - - resolve(data); - }); - }, - - getQueryData(query) { - const id = generateQueryId(query); - const data = window.grapherQueryStore[id] - return data - } - -} \ No newline at end of file + decodeData(data) { + const decodedEjsonString = decodeURIComponent(data); + if (!decodedEjsonString) return null; + + return EJSON.parse(decodedEjsonString); + }, + + load(optns) { + const defaults = { + selfDestruct: 3000, + }; + const options = Object.assign({}, defaults, optns); + + return new Promise((resolve, reject) => { + // Retrieve the payload from the DOM + const dom = document.querySelectorAll( + 'script[type="text/grapher-data"]', + document + ); + const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; + const data = this.decodeData(dataString) || {}; + window.grapherQueryStore = data; + + // Self destruct the store so that dynamically loaded modules + // do not pull from the store in the future + setTimeout(() => { + window.grapherQueryStore = {}; + }, options.selfDestruct); + + resolve(data); + }); + }, + + getQueryData(query) { + const id = generateQueryId(query); + const data = window.grapherQueryStore[id]; + return data; + }, +}; diff --git a/lib/SSRDataStore.js b/lib/SSRDataStore.js index 0be4fbc..647fcbc 100644 --- a/lib/SSRDataStore.js +++ b/lib/SSRDataStore.js @@ -1,39 +1,43 @@ -import React from 'react' +import React from 'react'; import PropTypes from 'prop-types'; import { EJSON } from 'meteor/ejson'; import { generateQueryId } from './utils.js'; export const SSRDataStoreContext = React.createContext(null); class DataStore { - storage = {} + storage = {}; - add(query, value) { - const key = generateQueryId(query) - this.storage[key] = value - } + add(query, value) { + const key = generateQueryId(query); + this.storage[key] = value; + } - getData() { - return this.storage - } + getData() { + return this.storage; + } } -export default class SSRDataStore{ - constructor() { - this.store = new DataStore() - } - - collectData(children) { - return {children} - } - - encodeData(data) { - data = EJSON.stringify(data) - return encodeURIComponent(data) - } - - getScriptTags() { - const data = this.store.getData() - - return `` - } -} \ No newline at end of file +export default class SSRDataStore { + constructor() { + this.store = new DataStore(); + } + + collectData(children) { + return ( + + {children} + + ); + } + + encodeData(data) { + data = EJSON.stringify(data); + return encodeURIComponent(data); + } + + getScriptTags() { + const data = this.store.getData(); + + return ``; + } +} diff --git a/lib/checkOptions.js b/lib/checkOptions.js index b2bec9a..ca6f4de 100644 --- a/lib/checkOptions.js +++ b/lib/checkOptions.js @@ -1,16 +1,18 @@ import React from 'react'; -import {check, Match} from 'meteor/check'; +import { check, Match } from 'meteor/check'; -export default function (options) { - check(options, { - reactive: Match.Maybe(Boolean), - single: Match.Maybe(Boolean), - pollingMs: Match.Maybe(Number), - errorComponent: Match.Maybe(React.Component), - loadingComponent: Match.Maybe(React.Component), - }); +export default function(options) { + check(options, { + reactive: Match.Maybe(Boolean), + single: Match.Maybe(Boolean), + pollingMs: Match.Maybe(Number), + errorComponent: Match.Maybe(React.Component), + loadingComponent: Match.Maybe(React.Component), + }); - if (options.reactive && options.poll) { - throw new Meteor.Error(`You cannot have a query that is reactive and it is with polling`) - } -} \ No newline at end of file + if (options.reactive && options.poll) { + throw new Meteor.Error( + `You cannot have a query that is reactive and it is with polling` + ); + } +} diff --git a/lib/getDisplayName.js b/lib/getDisplayName.js index 426e22c..88f7348 100644 --- a/lib/getDisplayName.js +++ b/lib/getDisplayName.js @@ -1,3 +1,3 @@ export default function getDisplayName(WrappedComponent) { - return WrappedComponent.displayName || WrappedComponent.name || 'Component'; -} \ No newline at end of file + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} diff --git a/lib/utils.js b/lib/utils.js index 39e3dd6..ff0a34a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,3 @@ -export const generateQueryId = function (query) { - return `${query.queryName}::${EJSON.stringify(query.params)}`; -} \ No newline at end of file +export const generateQueryId = function(query) { + return `${query.queryName}::${EJSON.stringify(query.params)}`; +}; diff --git a/lib/withQueryContainer.js b/lib/withQueryContainer.js index ebda4d5..085b786 100644 --- a/lib/withQueryContainer.js +++ b/lib/withQueryContainer.js @@ -1,51 +1,53 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Query, NamedQuery} from 'meteor/cultofcoders:grapher-react'; +import { Query, NamedQuery } from 'meteor/cultofcoders:grapher-react'; import getDisplayName from './getDisplayName'; -import {withTracker} from 'meteor/react-meteor-data'; +import { withTracker } from 'meteor/react-meteor-data'; const propTypes = { - grapher: PropTypes.shape({ - isLoading: PropTypes.bool.isRequired, - error: PropTypes.object, - data: PropTypes.array, - query: PropTypes.oneOfType([ - PropTypes.instanceOf(Query), - PropTypes.instanceOf(NamedQuery), - ]) - }).isRequired, - config: PropTypes.object.isRequired, - props: PropTypes.object, + grapher: PropTypes.shape({ + isLoading: PropTypes.bool.isRequired, + error: PropTypes.object, + data: PropTypes.array, + query: PropTypes.oneOfType([ + PropTypes.instanceOf(Query), + PropTypes.instanceOf(NamedQuery), + ]), + }).isRequired, + config: PropTypes.object.isRequired, + props: PropTypes.object, }; export default function withQueryContainer(WrappedComponent) { - let GrapherQueryContainer = function({grapher, config, query, props}) { - const {isLoading, error, data} = grapher; + let GrapherQueryContainer = function({ grapher, config, query, props }) { + const { isLoading, error, data } = grapher; - if (error && config.errorComponent) { - return React.createElement(config.errorComponent, { - error, - query, - }) - } + if (error && config.errorComponent) { + return React.createElement(config.errorComponent, { + error, + query, + }); + } - if (isLoading && config.loadingComponent) { - return React.createElement(config.loadingComponent, { - query, - }) - } + if (isLoading && config.loadingComponent) { + return React.createElement(config.loadingComponent, { + query, + }); + } - return React.createElement(WrappedComponent, { - ...props, - isLoading: error ? false : isLoading, - error, - data: config.single ? data[0] : data, - query - }) - }; + return React.createElement(WrappedComponent, { + ...props, + isLoading: error ? false : isLoading, + error, + data: config.single ? data[0] : data, + query, + }); + }; - GrapherQueryContainer.propTypes = propTypes; - GrapherQueryContainer.displayName = `GrapherQuery(${getDisplayName(WrappedComponent)})`; + GrapherQueryContainer.propTypes = propTypes; + GrapherQueryContainer.displayName = `GrapherQuery(${getDisplayName( + WrappedComponent + )})`; - return GrapherQueryContainer; + return GrapherQueryContainer; } diff --git a/lib/withReactiveQuery.js b/lib/withReactiveQuery.js index 86c73f1..d8775b9 100644 --- a/lib/withReactiveQuery.js +++ b/lib/withReactiveQuery.js @@ -1,5 +1,5 @@ -import {withTracker} from 'meteor/react-meteor-data'; -import {ReactiveVar} from 'meteor/reactive-var'; +import { withTracker } from 'meteor/react-meteor-data'; +import { ReactiveVar } from 'meteor/reactive-var'; import DataHydrator from './DataHydrator.js'; import { Meteor } from 'meteor/meteor'; @@ -11,71 +11,71 @@ import { Meteor } from 'meteor/meteor'; * @param QueryComponent */ export default function withReactiveContainer(handler, config, QueryComponent) { - let subscriptionError = new ReactiveVar(); + let subscriptionError = new ReactiveVar(); - return withTracker((props) => { - const query = handler(props); + return withTracker(props => { + const query = handler(props); - let isLoading - let data - let error + let isLoading; + let data; + let error; - // For server-side-rendering, immediately fetch the data - // and save it in the data store for this request - - if(Meteor.isServer) { - data = query.fetch(); - isLoading = false; - props.dataStore.add(query, data); - } else { - const subscriptionHandle = query.subscribe({ - onStop(err) { - if (err) { - subscriptionError.set(err); - } - }, - onReady() { - subscriptionError.set(null); - } - }); - - isLoading = !subscriptionHandle.ready(); + // For server-side-rendering, immediately fetch the data + // and save it in the data store for this request - // Check the SSR query store for data - if(Meteor.isClient && window && window.grapherQueryStore) { - const ssrData = DataHydrator.getQueryData(query); - if(ssrData) { - isLoading = false; - data = ssrData; - } - } + if (Meteor.isServer) { + data = query.fetch(); + isLoading = false; + props.dataStore.add(query, data); + } else { + const subscriptionHandle = query.subscribe({ + onStop(err) { + if (err) { + subscriptionError.set(err); + } + }, + onReady() { + subscriptionError.set(null); + }, + }); - if(!data) { - data = query.fetch(); - } - } - - return { - grapher: { - isLoading, - data, - error: subscriptionError, - }, - query, - config, - props, + isLoading = !subscriptionHandle.ready(); + + // Check the SSR query store for data + if (Meteor.isClient && window && window.grapherQueryStore) { + const ssrData = DataHydrator.getQueryData(query); + if (ssrData) { + isLoading = false; + data = ssrData; } - })(errorTracker(QueryComponent)) -} + } -const errorTracker = withTracker((props) => { - const error = props.grapher.error.get(); + if (!data) { + data = query.fetch(); + } + } return { - ...props, - grapher: { - ...props.grapher, - error, - } + grapher: { + isLoading, + data, + error: subscriptionError, + }, + query, + config, + props, }; -}); \ No newline at end of file + })(errorTracker(QueryComponent)); +} + +const errorTracker = withTracker(props => { + const error = props.grapher.error.get(); + + return { + ...props, + grapher: { + ...props.grapher, + error, + }, + }; +}); diff --git a/lib/withStaticQuery.js b/lib/withStaticQuery.js index 61d1766..b318539 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -4,106 +4,107 @@ import { Meteor } from 'meteor/meteor'; import DataHydrator from './DataHydrator.js'; export default function withStaticQueryContainer(WrappedComponent) { - /** - * We use it like this so we can have naming inside React Dev Tools - * This is a standard pattern in HOCs - */ - class GrapherStaticQueryContainer extends React.Component { - constructor(props) { - super(props); - - this.state = { - isLoading: true, - error: null, - data: [], - }; - - const { query } = props; - - // Check the SSR query store for data - if(Meteor.isClient && window && window.grapherQueryStore) { - const data = DataHydrator.getQueryData(query); - if(data) { - this.state = { - isLoading: false, - data - }; - - } - } - - // For server-side-rendering, immediately fetch the data - // and save it in the data store for this request - if(Meteor.isServer) { - const data = query.fetch(); - this.state = { - isLoading: false, - data - }; - props.dataStore.add(query, data); - } + /** + * We use it like this so we can have naming inside React Dev Tools + * This is a standard pattern in HOCs + */ + class GrapherStaticQueryContainer extends React.Component { + constructor(props) { + super(props); + + this.state = { + isLoading: true, + error: null, + data: [], + }; + + const { query } = props; + + // Check the SSR query store for data + if (Meteor.isClient && window && window.grapherQueryStore) { + const data = DataHydrator.getQueryData(query); + if (data) { + this.state = { + isLoading: false, + data, + }; } + } + + // For server-side-rendering, immediately fetch the data + // and save it in the data store for this request + if (Meteor.isServer) { + const data = query.fetch(); + this.state = { + isLoading: false, + data, + }; + props.dataStore.add(query, data); + } + } - componentWillReceiveProps(nextProps) { - const {query} = nextProps; - this.fetch(query); - } + componentWillReceiveProps(nextProps) { + const { query } = nextProps; + this.fetch(query); + } - componentDidMount() { - const {query, config} = this.props; + componentDidMount() { + const { query, config } = this.props; - // Do not fetch is we already have the data from SSR hydration - if(this.state.isLoading === true) { - this.fetch(query); - } + // Do not fetch is we already have the data from SSR hydration + if (this.state.isLoading === true) { + this.fetch(query); + } - if (config.pollingMs) { - this.pollingInterval = setInterval(() => { - this.fetch(query); - }, config.pollingMs) - } - } + if (config.pollingMs) { + this.pollingInterval = setInterval(() => { + this.fetch(query); + }, config.pollingMs); + } + } - componentWillUnmount() { - this.pollingInterval && clearInterval(this.pollingInterval); - } + componentWillUnmount() { + this.pollingInterval && clearInterval(this.pollingInterval); + } - fetch(query) { - query.fetch((error, data) => { - if (error) { - this.setState({ - error, - data: [], - isLoading: false, - }) - } else { - this.setState({ - error: null, - data, - isLoading: false, - }); - } - }); + fetch(query) { + query.fetch((error, data) => { + if (error) { + this.setState({ + error, + data: [], + isLoading: false, + }); + } else { + this.setState({ + error: null, + data, + isLoading: false, + }); } + }); + } - refetch = () => { - const {query} = this.props; - this.fetch(query); - }; + refetch = () => { + const { query } = this.props; + this.fetch(query); + }; - render() { - const {config, props, query} = this.props; + render() { + const { config, props, query } = this.props; - return React.createElement(WrappedComponent, { - grapher: this.state, - config, - query, - props: {...props, refetch: this.refetch}, - }); - } + return React.createElement(WrappedComponent, { + grapher: this.state, + config, + query, + props: { ...props, refetch: this.refetch }, + }); } + } - GrapherStaticQueryContainer.displayName = `StaticQuery(${getDisplayName(WrappedComponent)})`; + GrapherStaticQueryContainer.displayName = `StaticQuery(${getDisplayName( + WrappedComponent + )})`; - return GrapherStaticQueryContainer; -} \ No newline at end of file + return GrapherStaticQueryContainer; +} diff --git a/main.client.js b/main.client.js index 02d7579..98ae6e5 100644 --- a/main.client.js +++ b/main.client.js @@ -1,22 +1,19 @@ import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; -checkNpmVersions({ +checkNpmVersions( + { react: '15.3 - 16', 'prop-types': '15.0 - 16', -}, 'cultofcoders:grapher-react'); + }, + 'cultofcoders:grapher-react' +); -export { - default as setDefaults -} from './setDefaults.js'; +export { default as setDefaults } from './setDefaults.js'; -export { - default as withQuery -} from './withQuery.js'; +export { default as withQuery } from './withQuery.js'; export { - default as createQueryContainer + default as createQueryContainer, } from './legacy/createQueryContainer.js'; -export { - default as DataHydrator -} from './lib/DataHydrator.js'; \ No newline at end of file +export { default as DataHydrator } from './lib/DataHydrator.js'; diff --git a/main.server.js b/main.server.js index 1ac78c1..3e790a6 100644 --- a/main.server.js +++ b/main.server.js @@ -1,22 +1,19 @@ import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; -checkNpmVersions({ +checkNpmVersions( + { react: '15.3 - 16', 'prop-types': '15.0 - 16', -}, 'cultofcoders:grapher-react'); + }, + 'cultofcoders:grapher-react' +); -export { - default as setDefaults -} from './setDefaults.js'; +export { default as setDefaults } from './setDefaults.js'; -export { - default as withQuery -} from './withQuery.js'; +export { default as withQuery } from './withQuery.js'; export { - default as createQueryContainer + default as createQueryContainer, } from './legacy/createQueryContainer.js'; -export { - default as SSRDataStore -} from './lib/SSRDataStore.js'; \ No newline at end of file +export { default as SSRDataStore } from './lib/SSRDataStore.js'; diff --git a/package.js b/package.js index 0578aa4..3644543 100644 --- a/package.js +++ b/package.js @@ -1,46 +1,46 @@ Package.describe({ - name: 'cultofcoders:grapher-react', - version: '0.1.1', - // Brief, one-line summary of the package. - summary: 'Provides HOCs for easily wrapping components with Grapher queries', - // URL to the Git repository containing the source code for this package. - git: 'https://github.com/cult-of-coders/grapher-react', - // By default, Meteor will default to using README.md for documentation. - // To avoid submitting documentation, set this field to null. - documentation: 'README.md' + name: 'cultofcoders:grapher-react', + version: '0.1.1', + // Brief, one-line summary of the package. + summary: 'Provides HOCs for easily wrapping components with Grapher queries', + // URL to the Git repository containing the source code for this package. + git: 'https://github.com/cult-of-coders/grapher-react', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md', }); -Package.onUse(function (api) { - api.versionsFrom('1.3'); - api.use([ - 'ecmascript', - 'tracker', - 'check', - 'reactive-var', - 'react-meteor-data@0.2.15', - 'cultofcoders:grapher@1.2.8_1', - 'tmeasday:check-npm-versions@0.2.0', - 'ejson' - ]); +Package.onUse(function(api) { + api.versionsFrom('1.3'); + api.use([ + 'ecmascript', + 'tracker', + 'check', + 'reactive-var', + 'react-meteor-data@0.2.15', + 'cultofcoders:grapher@1.2.8_1', + 'tmeasday:check-npm-versions@0.2.0', + 'ejson', + ]); - api.mainModule('main.client.js', 'client'); - api.mainModule('main.server.js', 'server'); + api.mainModule('main.client.js', 'client'); + api.mainModule('main.server.js', 'server'); }); -Package.onTest(function (api) { - api.use([ - 'cultofcoders:grapher-react', - 'cultofcoders:grapher', - 'ecmascript', - 'mongo' - ]); +Package.onTest(function(api) { + api.use([ + 'cultofcoders:grapher-react', + 'cultofcoders:grapher', + 'ecmascript', + 'mongo', + ]); - api.use([ - 'coffeescript@1.12.7_3', - 'practicalmeteor:mocha@2.4.5_6', - 'practicalmeteor:chai' - ]); + api.use([ + 'coffeescript@1.12.7_3', + 'practicalmeteor:mocha@2.4.5_6', + 'practicalmeteor:chai', + ]); - api.addFiles('__tests__/main.server.js', 'server'); - api.addFiles('__tests__/main.client.js', 'client'); + api.addFiles('__tests__/main.server.js', 'server'); + api.addFiles('__tests__/main.client.js', 'client'); }); diff --git a/setDefaults.js b/setDefaults.js index 7dc6516..481bd37 100644 --- a/setDefaults.js +++ b/setDefaults.js @@ -1,5 +1,5 @@ import defaults from './defaults'; export default function setDefaults(newDefaults) { - Object.assign(defaults, newDefaults); -} \ No newline at end of file + Object.assign(defaults, newDefaults); +} diff --git a/withQuery.js b/withQuery.js index ce98109..f3282e0 100644 --- a/withQuery.js +++ b/withQuery.js @@ -4,43 +4,55 @@ import withReactiveQuery from './lib/withReactiveQuery'; import withQueryContainer from './lib/withQueryContainer'; import withStaticQuery from './lib/withStaticQuery'; import checkOptions from './lib/checkOptions'; -import { SSRDataStoreContext } from './lib/SSRDataStore.js' - -export default function (handler, _config = {}) { - checkOptions(_config); - const config = Object.assign({}, defaults, _config); - - return function (component) { - let C - const queryContainer = withQueryContainer(component); - - if (!config.reactive) { - const StaticQueryContainer = withStaticQuery(queryContainer); - - C = function (props) { - const query = handler(props); - - return ( - - {dataStore => } - - ) - } - } else { - const ReactiveQueryContainer = withReactiveQuery(handler, config, queryContainer); - C = function(props) { - return ( - - {dataStore => } - - ) - } - } - - - C.displayName = `withQuery(${component.displayName || component.name})` - C.WrappedComponent = component - - return C - }; -} \ No newline at end of file +import { SSRDataStoreContext } from './lib/SSRDataStore.js'; + +export default function(handler, _config = {}) { + checkOptions(_config); + const config = Object.assign({}, defaults, _config); + + return function(component) { + let C; + const queryContainer = withQueryContainer(component); + + if (!config.reactive) { + const StaticQueryContainer = withStaticQuery(queryContainer); + + C = function withQuery(props) { + const query = handler(props); + + return ( + + {dataStore => ( + + )} + + ); + }; + } else { + const ReactiveQueryContainer = withReactiveQuery( + handler, + config, + queryContainer + ); + C = function withQuery(props) { + return ( + + {dataStore => ( + + )} + + ); + }; + } + + C.displayName = `withQuery(${component.displayName || component.name})`; + C.WrappedComponent = component; + + return C; + }; +} From 0aa5744e4d7c8eed7bee12a523aad399a8d83e4a Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 13:01:51 -0700 Subject: [PATCH 08/14] Add withUser --- lib/DataHydrator.js | 16 ++++------ lib/User.js | 63 +++++++++++++++++++++++++++++++++++++++ lib/withQueryContainer.js | 1 - lib/withStaticQuery.js | 10 +++++-- main.client.js | 3 ++ main.server.js | 3 ++ withQuery.js | 23 +++++++++++--- 7 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 lib/User.js diff --git a/lib/DataHydrator.js b/lib/DataHydrator.js index dd83234..da717f8 100644 --- a/lib/DataHydrator.js +++ b/lib/DataHydrator.js @@ -1,4 +1,5 @@ import { Promise } from 'meteor/promise'; +import { EJSON } from 'meteor/ejson'; import { generateQueryId } from './utils.js'; export default { @@ -10,11 +11,6 @@ export default { }, load(optns) { - const defaults = { - selfDestruct: 3000, - }; - const options = Object.assign({}, defaults, optns); - return new Promise((resolve, reject) => { // Retrieve the payload from the DOM const dom = document.querySelectorAll( @@ -25,12 +21,6 @@ export default { const data = this.decodeData(dataString) || {}; window.grapherQueryStore = data; - // Self destruct the store so that dynamically loaded modules - // do not pull from the store in the future - setTimeout(() => { - window.grapherQueryStore = {}; - }, options.selfDestruct); - resolve(data); }); }, @@ -40,4 +30,8 @@ export default { const data = window.grapherQueryStore[id]; return data; }, + + destroy() { + window.grapherQueryStore = null; + }, }; diff --git a/lib/User.js b/lib/User.js new file mode 100644 index 0000000..b601d42 --- /dev/null +++ b/lib/User.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import hoistNonReactStatic from 'hoist-non-react-statics'; +import { withTracker } from 'meteor/react-meteor-data'; + +export const UserContext = React.createContext(null); + +class UserContextProvider extends React.Component { + static propTypes = { + user: PropTypes.object, + children: PropTypes.node, + }; + + render() { + return ( + + {this.props.children} + + ); + } +} + +export const User = withTracker(props => { + let user; + if (Meteor.isServer) { + if (props.token) { + user = Meteor.users.findOne( + { + 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken( + props.token + ), + }, + { reactive: false } + ); + } + } else { + user = Meteor.user(); + } + + return { + user, + }; +})(UserContextProvider); + +const withUser = function(Component) { + const C = props => { + return ( + + {value => { + return ; + }} + + ); + }; + + C.displayName = `withUser(${Component.displayName || Component.name})`; + + hoistNonReactStatic(C, Component); + return C; +}; +export default withUser; diff --git a/lib/withQueryContainer.js b/lib/withQueryContainer.js index 085b786..94ba9b6 100644 --- a/lib/withQueryContainer.js +++ b/lib/withQueryContainer.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Query, NamedQuery } from 'meteor/cultofcoders:grapher-react'; import getDisplayName from './getDisplayName'; -import { withTracker } from 'meteor/react-meteor-data'; const propTypes = { grapher: PropTypes.shape({ diff --git a/lib/withStaticQuery.js b/lib/withStaticQuery.js index b318539..9ac0179 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -34,7 +34,7 @@ export default function withStaticQueryContainer(WrappedComponent) { // For server-side-rendering, immediately fetch the data // and save it in the data store for this request if (Meteor.isServer) { - const data = query.fetch(); + const data = query.fetch({ userId: this.getUserId() }); this.state = { isLoading: false, data, @@ -43,6 +43,12 @@ export default function withStaticQueryContainer(WrappedComponent) { } } + getUserId() { + if (this.props.user) { + return this.props.user._id; + } + } + componentWillReceiveProps(nextProps) { const { query } = nextProps; this.fetch(query); @@ -68,7 +74,7 @@ export default function withStaticQueryContainer(WrappedComponent) { } fetch(query) { - query.fetch((error, data) => { + query.fetch({ userId: this.getUserId() }, (error, data) => { if (error) { this.setState({ error, diff --git a/main.client.js b/main.client.js index 98ae6e5..1df2778 100644 --- a/main.client.js +++ b/main.client.js @@ -5,11 +5,14 @@ checkNpmVersions( react: '15.3 - 16', 'prop-types': '15.0 - 16', }, + 'hoist-non-react-statics', 'cultofcoders:grapher-react' ); export { default as setDefaults } from './setDefaults.js'; +export { default as withUser, User } from './lib/User.js'; + export { default as withQuery } from './withQuery.js'; export { diff --git a/main.server.js b/main.server.js index 3e790a6..480b0d4 100644 --- a/main.server.js +++ b/main.server.js @@ -5,6 +5,7 @@ checkNpmVersions( react: '15.3 - 16', 'prop-types': '15.0 - 16', }, + 'hoist-non-react-statics', 'cultofcoders:grapher-react' ); @@ -12,6 +13,8 @@ export { default as setDefaults } from './setDefaults.js'; export { default as withQuery } from './withQuery.js'; +export { default as withUser, User } from './lib/User.js'; + export { default as createQueryContainer, } from './legacy/createQueryContainer.js'; diff --git a/withQuery.js b/withQuery.js index f3282e0..90b8208 100644 --- a/withQuery.js +++ b/withQuery.js @@ -5,27 +5,34 @@ import withQueryContainer from './lib/withQueryContainer'; import withStaticQuery from './lib/withStaticQuery'; import checkOptions from './lib/checkOptions'; import { SSRDataStoreContext } from './lib/SSRDataStore.js'; +import hoistNonReactStatic from 'hoist-non-react-statics'; +import withUser from './lib/User'; export default function(handler, _config = {}) { checkOptions(_config); const config = Object.assign({}, defaults, _config); + // Return a function that wraps a given component return function(component) { - let C; + let C; // C will be the new wrapped component that we return const queryContainer = withQueryContainer(component); + // Non-reactive queries use the withStaticQuery HOC if (!config.reactive) { const StaticQueryContainer = withStaticQuery(queryContainer); C = function withQuery(props) { const query = handler(props); + const { user, ...remainingProps } = props; + return ( {dataStore => ( @@ -34,6 +41,7 @@ export default function(handler, _config = {}) { ); }; } else { + // Reactive queries use the withReactiveQuery HOC const ReactiveQueryContainer = withReactiveQuery( handler, config, @@ -50,9 +58,16 @@ export default function(handler, _config = {}) { }; } + // Convention: Wrap the Display Name for Easy Debugging + // https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging C.displayName = `withQuery(${component.displayName || component.name})`; - C.WrappedComponent = component; - return C; + // Static Methods Must Be Copied Over + // https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over + hoistNonReactStatic(C, component); + + return withUser(C); + + // return C; }; } From 48df697c1274329e88751e6f951efc061c1e8dd0 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 13:05:07 -0700 Subject: [PATCH 09/14] Add FastRender's cookie logic --- lib/authCookie.js | 43 +++++++++++++++++++++++++++++++++++++++++++ main.client.js | 5 ++++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 lib/authCookie.js diff --git a/lib/authCookie.js b/lib/authCookie.js new file mode 100644 index 0000000..0be6224 --- /dev/null +++ b/lib/authCookie.js @@ -0,0 +1,43 @@ +import Cookies from 'js-cookie'; +import { Meteor } from 'meteor/meteor'; + +Meteor.startup(function() { + resetToken(); +}); + +// override Meteor._localStorage methods and resetToken accordingly +var originalSetItem = Meteor._localStorage.setItem; +Meteor._localStorage.setItem = function(key, value) { + if (key === 'Meteor.loginToken') { + Meteor.defer(resetToken); + } + originalSetItem.call(Meteor._localStorage, key, value); +}; + +var originalRemoveItem = Meteor._localStorage.removeItem; +Meteor._localStorage.removeItem = function(key) { + if (key === 'Meteor.loginToken') { + Meteor.defer(resetToken); + } + originalRemoveItem.call(Meteor._localStorage, key); +}; + +function resetToken() { + var loginToken = Meteor._localStorage.getItem('Meteor.loginToken'); + var loginTokenExpires = new Date( + Meteor._localStorage.getItem('Meteor.loginTokenExpires') + ); + + if (loginToken) { + setToken(loginToken, loginTokenExpires); + } else { + Cookies.remove('meteor_login_token'); + } +} + +function setToken(loginToken, expires) { + Cookies.set('meteor_login_token', loginToken, { + path: '/', + expires: expires, + }); +} diff --git a/main.client.js b/main.client.js index 1df2778..d74498c 100644 --- a/main.client.js +++ b/main.client.js @@ -6,9 +6,12 @@ checkNpmVersions( 'prop-types': '15.0 - 16', }, 'hoist-non-react-statics', - 'cultofcoders:grapher-react' + 'cultofcoders:grapher-react', + 'js-cookie' ); +import './lib/authCookie.js'; + export { default as setDefaults } from './setDefaults.js'; export { default as withUser, User } from './lib/User.js'; From 81c34c600cc8f0ad15e87643611b8f4d2dd87860 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 13:05:35 -0700 Subject: [PATCH 10/14] Remove unused arg --- lib/DataHydrator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DataHydrator.js b/lib/DataHydrator.js index da717f8..2a21dc3 100644 --- a/lib/DataHydrator.js +++ b/lib/DataHydrator.js @@ -10,7 +10,7 @@ export default { return EJSON.parse(decodedEjsonString); }, - load(optns) { + load() { return new Promise((resolve, reject) => { // Retrieve the payload from the DOM const dom = document.querySelectorAll( From c0389b19476602d95c7637385bf6cf12c9d34dfb Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 13:06:24 -0700 Subject: [PATCH 11/14] No need for promise here --- lib/DataHydrator.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/DataHydrator.js b/lib/DataHydrator.js index 2a21dc3..28ab9cb 100644 --- a/lib/DataHydrator.js +++ b/lib/DataHydrator.js @@ -11,18 +11,16 @@ export default { }, load() { - return new Promise((resolve, reject) => { - // Retrieve the payload from the DOM - const dom = document.querySelectorAll( - 'script[type="text/grapher-data"]', - document - ); - const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; - const data = this.decodeData(dataString) || {}; - window.grapherQueryStore = data; + // Retrieve the payload from the DOM + const dom = document.querySelectorAll( + 'script[type="text/grapher-data"]', + document + ); + const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; + const data = this.decodeData(dataString) || {}; + window.grapherQueryStore = data; - resolve(data); - }); + return data; }, getQueryData(query) { From ba57e5e035bb5a153ecd9c0494094321da70d247 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 13:09:35 -0700 Subject: [PATCH 12/14] Add constant for mime type --- lib/DataHydrator.js | 5 ++--- lib/SSRDataStore.js | 4 ++-- lib/utils.js | 4 ++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/DataHydrator.js b/lib/DataHydrator.js index 28ab9cb..4bbc950 100644 --- a/lib/DataHydrator.js +++ b/lib/DataHydrator.js @@ -1,6 +1,5 @@ -import { Promise } from 'meteor/promise'; import { EJSON } from 'meteor/ejson'; -import { generateQueryId } from './utils.js'; +import { generateQueryId, DATASTORE_MIME } from './utils.js'; export default { decodeData(data) { @@ -13,7 +12,7 @@ export default { load() { // Retrieve the payload from the DOM const dom = document.querySelectorAll( - 'script[type="text/grapher-data"]', + `script[type="${DATASTORE_MIME}"]`, document ); const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; diff --git a/lib/SSRDataStore.js b/lib/SSRDataStore.js index 647fcbc..a2d6463 100644 --- a/lib/SSRDataStore.js +++ b/lib/SSRDataStore.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EJSON } from 'meteor/ejson'; -import { generateQueryId } from './utils.js'; +import { generateQueryId, DATASTORE_MIME } from './utils.js'; export const SSRDataStoreContext = React.createContext(null); class DataStore { @@ -38,6 +38,6 @@ export default class SSRDataStore { getScriptTags() { const data = this.store.getData(); - return ``; + return ``; } } diff --git a/lib/utils.js b/lib/utils.js index ff0a34a..aa7b4c9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,7 @@ +import { EJSON } from 'meteor/ejson'; + export const generateQueryId = function(query) { return `${query.queryName}::${EJSON.stringify(query.params)}`; }; + +export const DATASTORE_MIME = 'text/grapher-data'; From ef6534110aa674ae1da2be1107dae781cb8b4612 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 13:12:26 -0700 Subject: [PATCH 13/14] Add error handling --- lib/withReactiveQuery.js | 8 ++++++-- lib/withStaticQuery.js | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/withReactiveQuery.js b/lib/withReactiveQuery.js index d8775b9..39f51df 100644 --- a/lib/withReactiveQuery.js +++ b/lib/withReactiveQuery.js @@ -24,7 +24,11 @@ export default function withReactiveContainer(handler, config, QueryComponent) { // and save it in the data store for this request if (Meteor.isServer) { - data = query.fetch(); + try { + data = query.fetch(); + } catch (e) { + error = e.message; + } isLoading = false; props.dataStore.add(query, data); } else { @@ -59,7 +63,7 @@ export default function withReactiveContainer(handler, config, QueryComponent) { grapher: { isLoading, data, - error: subscriptionError, + error: subscriptionError || error, }, query, config, diff --git a/lib/withStaticQuery.js b/lib/withStaticQuery.js index 9ac0179..b27655c 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -34,12 +34,18 @@ export default function withStaticQueryContainer(WrappedComponent) { // For server-side-rendering, immediately fetch the data // and save it in the data store for this request if (Meteor.isServer) { - const data = query.fetch({ userId: this.getUserId() }); - this.state = { - isLoading: false, - data, - }; - props.dataStore.add(query, data); + try { + const data = query.fetch({ userId: this.getUserId() }); + this.state = { + isLoading: false, + data, + }; + props.dataStore.add(query, data); + } catch (e) { + this.state = { + error: e.message, + }; + } } } From 6e6fce823ca8566b7e069f7d18569aedd7b9e3e7 Mon Sep 17 00:00:00 2001 From: Andrew Becker Date: Wed, 25 Apr 2018 15:03:53 -0700 Subject: [PATCH 14/14] Fix staticQuery on client --- lib/withStaticQuery.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/withStaticQuery.js b/lib/withStaticQuery.js index b27655c..c7c4ec7 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -80,7 +80,7 @@ export default function withStaticQueryContainer(WrappedComponent) { } fetch(query) { - query.fetch({ userId: this.getUserId() }, (error, data) => { + query.fetch((error, data) => { if (error) { this.setState({ error,