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 + }, +}; 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, +} 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/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/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 new file mode 100644 index 0000000..4bbc950 --- /dev/null +++ b/lib/DataHydrator.js @@ -0,0 +1,34 @@ +import { EJSON } from 'meteor/ejson'; +import { generateQueryId, DATASTORE_MIME } from './utils.js'; + +export default { + decodeData(data) { + const decodedEjsonString = decodeURIComponent(data); + if (!decodedEjsonString) return null; + + return EJSON.parse(decodedEjsonString); + }, + + load() { + // Retrieve the payload from the DOM + const dom = document.querySelectorAll( + `script[type="${DATASTORE_MIME}"]`, + document + ); + const dataString = dom && dom.length > 0 ? dom[0].innerHTML : ''; + const data = this.decodeData(dataString) || {}; + window.grapherQueryStore = data; + + return data; + }, + + getQueryData(query) { + const id = generateQueryId(query); + const data = window.grapherQueryStore[id]; + return data; + }, + + destroy() { + window.grapherQueryStore = null; + }, +}; diff --git a/lib/SSRDataStore.js b/lib/SSRDataStore.js new file mode 100644 index 0000000..a2d6463 --- /dev/null +++ b/lib/SSRDataStore.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { EJSON } from 'meteor/ejson'; +import { generateQueryId, DATASTORE_MIME } 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 ``; + } +} 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/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/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 new file mode 100644 index 0000000..aa7b4c9 --- /dev/null +++ b/lib/utils.js @@ -0,0 +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'; diff --git a/lib/withQueryContainer.js b/lib/withQueryContainer.js index ebda4d5..94ba9b6 100644 --- a/lib/withQueryContainer.js +++ b/lib/withQueryContainer.js @@ -1,51 +1,52 @@ 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'; 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 bf2ce9f..39f51df 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 { 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 @@ -9,47 +11,75 @@ import {ReactiveVar} from 'meteor/reactive-var'; * @param QueryComponent */ export default function withReactiveContainer(handler, config, QueryComponent) { - let subscriptionError = new ReactiveVar(); - - return withTracker((props) => { - const query = handler(props); - - const subscriptionHandle = query.subscribe({ - onStop(err) { - if (err) { - subscriptionError.set(err); - } - }, - onReady() { - subscriptionError.set(null); - } - }); - - const isReady = subscriptionHandle.ready(); - - const data = query.fetch(); - - return { - grapher: { - isLoading: !isReady, - data, - error: subscriptionError, - }, - query, - config, - props, + let subscriptionError = new ReactiveVar(); + + return withTracker(props => { + const query = handler(props); + + 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) { + try { + data = query.fetch(); + } catch (e) { + error = e.message; + } + 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(); + + // 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 || error, + }, + 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 e963f02..c7c4ec7 100644 --- a/lib/withStaticQuery.js +++ b/lib/withStaticQuery.js @@ -1,74 +1,122 @@ import React from 'react'; import getDisplayName from './getDisplayName'; +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 { - state = { - isLoading: true, - error: null, - 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); - componentWillReceiveProps(nextProps) { - const {query} = nextProps; - this.fetch(query); - } + this.state = { + isLoading: true, + error: null, + data: [], + }; - componentDidMount() { - const {query, config} = this.props; - this.fetch(query); + const { query } = props; - if (config.pollingMs) { - this.pollingInterval = setInterval(() => { - this.fetch(query); - }, config.pollingMs) - } + // 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, + }; } + } - componentWillUnmount() { - this.pollingInterval && clearInterval(this.pollingInterval); + // For server-side-rendering, immediately fetch the data + // and save it in the data store for this request + if (Meteor.isServer) { + 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, + }; } + } + } - fetch(query) { - query.fetch((error, data) => { - if (error) { - this.setState({ - error, - data: [], - isLoading: false, - }) - } else { - this.setState({ - error: null, - data, - isLoading: false, - }); - } - }); - } + getUserId() { + if (this.props.user) { + return this.props.user._id; + } + } + + componentWillReceiveProps(nextProps) { + const { query } = nextProps; + this.fetch(query); + } + + componentDidMount() { + const { query, config } = this.props; - refetch = () => { - const {query} = 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); + } - render() { - const {config, props, query} = this.props; + if (config.pollingMs) { + this.pollingInterval = setInterval(() => { + this.fetch(query); + }, config.pollingMs); + } + } + + componentWillUnmount() { + this.pollingInterval && clearInterval(this.pollingInterval); + } - return React.createElement(WrappedComponent, { - grapher: this.state, - config, - query, - props: {...props, refetch: this.refetch}, - }); + 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); + }; + + render() { + const { config, props, query } = this.props; + + 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 4219b6f..d74498c 100644 --- a/main.client.js +++ b/main.client.js @@ -1,18 +1,25 @@ import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; -checkNpmVersions({ +checkNpmVersions( + { react: '15.3 - 16', 'prop-types': '15.0 - 16', -}, 'cultofcoders:grapher-react'); + }, + 'hoist-non-react-statics', + 'cultofcoders:grapher-react', + 'js-cookie' +); -export { - default as setDefaults -} from './setDefaults.js'; +import './lib/authCookie.js'; -export { - default as withQuery -} from './withQuery.js'; +export { default as setDefaults } from './setDefaults.js'; + +export { default as withUser, User } from './lib/User.js'; + +export { default as withQuery } from './withQuery.js'; export { - default as createQueryContainer -} from './legacy/createQueryContainer.js'; \ No newline at end of file + default as createQueryContainer, +} from './legacy/createQueryContainer.js'; + +export { default as DataHydrator } from './lib/DataHydrator.js'; diff --git a/main.server.js b/main.server.js index 4219b6f..480b0d4 100644 --- a/main.server.js +++ b/main.server.js @@ -1,18 +1,22 @@ import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; -checkNpmVersions({ +checkNpmVersions( + { react: '15.3 - 16', 'prop-types': '15.0 - 16', -}, 'cultofcoders:grapher-react'); + }, + 'hoist-non-react-statics', + '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 withUser, User } from './lib/User.js'; export { - default as createQueryContainer -} from './legacy/createQueryContainer.js'; \ No newline at end of file + default as createQueryContainer, +} from './legacy/createQueryContainer.js'; + +export { default as SSRDataStore } from './lib/SSRDataStore.js'; diff --git a/package.js b/package.js index 7fe5c51..3644543 100644 --- a/package.js +++ b/package.js @@ -1,45 +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', - ]); +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 1cf2ede..90b8208 100644 --- a/withQuery.js +++ b/withQuery.js @@ -1,32 +1,73 @@ 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'; 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 function (component) { - const queryContainer = withQueryContainer(component); - - if (!config.reactive) { - const staticQueryContainer = withStaticQuery(queryContainer); - - return function (props) { - const query = handler(props); - - return React.createElement(staticQueryContainer, { - query, - props, - config - }) - } - } else { - return withReactiveQuery(handler, config, queryContainer); - } - }; -} \ No newline at end of file +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; // 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 => ( + + )} + + ); + }; + } else { + // Reactive queries use the withReactiveQuery HOC + const ReactiveQueryContainer = withReactiveQuery( + handler, + config, + queryContainer + ); + C = function withQuery(props) { + return ( + + {dataStore => ( + + )} + + ); + }; + } + + // 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})`; + + // 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; + }; +}