├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── both.js ├── client-collection.js ├── client-connector.js ├── client.js ├── package.js ├── package.json ├── server-collection.js ├── server-connector.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.2.1] 8 | - Increment required versions to the latest. 9 | 10 | ## [0.2.0] 11 | - Change `createListHook` to `createConnector`. The method still returns a react hook, but it also does other things like setting up server methods. It can now set up a single document data query. For these reasons, a more general name is appropriate. 12 | - Add a `single` configuration property, to create a connector for a single document. 13 | - Always call `validate` before `run` on server and client. 14 | - No need to run through `useTracker` in server rendering. 15 | - Switched to official `react-meteor-data` hook implementation. 16 | - No longer export `useTracker` - users must migrate to import from `meteor:react-meteor-data`. 17 | 18 | ## [0.1.3] 19 | - Fix error with importing useTracker correctly after making a local copy. 20 | 21 | ## [0.1.2] 22 | - Include a copy of `useTracker` until the official package is released. This will be removed at some point, please don't rely on it. 23 | 24 | ## [0.1.1] 25 | - **Breaking change**: Changed the format of the return data from an object with data, and loading props named after the connector name or nameProp value, to a tuple. Much simpler, and more flexible, but a breaking change from 0.1.0. 26 | 27 | ## [0.1.0] 28 | - First version! 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin Newman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NP Dev Collections 2 | ========================== 3 | 4 | Foundational code for NP Dev Collections was generously donated by [PixStori](https://www.pixstoriplus.com), an aural history social media tool built on Meteor. Tell your stori. 5 | 6 | NPDev Collections combines a mix of technologies to facilitate the creation of offline first collections, data over methods with pagination, and support for SSR out of the box. 7 | 8 | Install with: 9 | 10 | `$ meteor add npdev:collections` 11 | 12 | [Check out my starter](https://github.com/CaptainN/meteor-react-starter) for a complete example site using NP Dev Collections and other server-render tools. 13 | 14 | Why 15 | --- 16 | 17 | Basically, I like Mongo and MiniMongo for it's low bar for entry, and it's simple iteration. It's great for starting new projects and prototyping early and rapidly. I wanted to create something that added a few of the modern touches, like offline-first data, and server side rendering, while retaining that wonderful simplicity as much as possible. 18 | 19 | What 20 | ---- 21 | 22 | There are a lot moving parts involved getting all the parts working together. Here are some of the hurdles: 23 | 24 | - Use offline storage client side by default. `ground:db` is a great solution. 25 | - SSR - for SSR, we have a number of tricky parts to choreograph. 26 | - Query Mongo directly, and avoid setting up subscriptions. 27 | - Capture the results of queries on the server during SSR, and serialize that data to send to the client. 28 | - Hydrate the data from the server on the client, before rendering the react tree. 29 | - On first render, don't fetch remote data - we just got that through hydration. 30 | - Those same queries must work in an isomorphic way - on the server they must run against mongo directly, on the client, they must fetch data. 31 | - Use methods for data transfer by default, instead of reactive pub/sub (on the list of TODOs is to allow pub/sub instead of methods). 32 | - Also provide a method of pagination by default in the methods. 33 | 34 | That's a lot of stuff to keep track of, and it's kind of a pain to do it manually. NPDev;Collections keep track of all that for you, and provides the tools to easily set it all up. 35 | 36 | How 37 | --- 38 | 39 | - We'll use mongo queries on the server and client, in an isomorphic way. NPDev:Collections provides a createCollection method with a single import location, which delivers Mongo on the server, and GroundDB on the client. This keeps the code isomorphic, and canonical. The server will use Mongo directly, and never display a loading screen. 40 | - On the server, during SSR, we'll capture all query data, and output it as JSON (actually, EJSON) for hydration to use. This is done with a Context Provider, and a utility method, which must be configured in SSR code using Meteor's `server-render` package. 41 | - On the client, that data gets hydrated before React is hydrated. We also have to make sure there is no attempt to hit the server to grab data in the first render, since we already got the data. This is accomplished through another simple Provider, and a utility method, which must be configured in the client side React startup code. First hydrate the data, then hydrate react. 42 | - After the first run (during hydration), the client will fetch data via meteor methods. The isLoading property will only be true during syncing (loading) events. 43 | 44 | Quick Start! 45 | ------------ 46 | 47 | Install with: 48 | 49 | `meteor add npdev:collections` 50 | 51 | We also need react and react-dom npm packages of course. These are not defined in this package, so that the version can be kept up to date in the main project. This package requires a version of react which contains support for hooks - 16.8+. 52 | 53 | The first thing we need are our collections. They can be created using the simple `createCollection` method. All this is does is create a Meteor Collection on the server, or a Ground:DB server on the client, and connect the schema using `aldeed:collection2`'s attachSchema (on the server). It's necessary to use this method to create your collections to register them with NPDev:Collections. I'll probably add some facilities to allow more granular control over these in the future. Of course, these collections should all be included in the server bundle. 54 | 55 | ```js 56 | import { createCollection } from 'meteor/npdev:collections' 57 | import { CommentSchema } from './CommentSchema' 58 | 59 | const Comments = createCollection('comments', CommentSchema) 60 | 61 | export default Comments 62 | ``` 63 | 64 | Basic use requires the creation of custom hooks for each query you want to set up. The `createConnector` utility function accept a set of properties - name, collection, an isomorphic validation method, and an isomorphic query generator. This API is inspired by, and builds on the API of mdg:validated-method. Here is an example from PixStori: 65 | 66 | ```js 67 | import { createListHood } from 'meteor/npdev:collections' 68 | 69 | // getPublicQuery builds a query which selects the appropriate public documents 70 | const getPublicQuery = () => ({ 71 | public: true 72 | }) 73 | 74 | export const useComments = createConnector({ 75 | name: 'tiles', 76 | collection: Comments, 77 | // This runs on client and server, and in both methods and SSR contexts. 78 | validate () {}, 79 | // So does this! Be careful with security. 80 | query () { 81 | return getPublicQuery() 82 | } 83 | }) 84 | 85 | export const useGroupComments = createConnector({ 86 | name: 'groupComments', 87 | collection: Comments, 88 | validate ({ groupId }) { 89 | // Here we could use SimpleSchema and/or throw a validated-error, etc. 90 | // See the ValidateMethod documentation for more. 91 | check(groupId, String) 92 | }, 93 | query: ({ groupId }) => ({ 94 | $and: [ 95 | { groupId }, 96 | getPublicQuery() 97 | ] 98 | }) 99 | }) 100 | 101 | ``` 102 | 103 | **NOTE:** *These hooks must be included in the server build, not just in the react tree, but somewhere statically, so they can set up the necessary methods. They don't need to be included statically in the client bundle, which allows for code splitting using the `dynamic-import` package.* 104 | 105 | Using this, along with `createConnector`, it sets up everything we need on the server, and on the client to do offline-first, data-over-methods, with pagination, and SSR, with data hydration, etc. (along with using a set of providers in SSR and hydration code). Super spiffy! In use, it looks like this: 106 | 107 | ```js 108 | // Here we use the group comments. 109 | const GroupFeedPage = ({ limit, offset, order, orderBy, groupId }) => { 110 | const [ groupComments, groupCommentsAreLoading ] = useGroupComments({ groupId, limit, offset, order, orderBy }) 111 | return 112 | } 113 | ``` 114 | 115 | That's already a pretty easy way to grab data! But we also want to have SSR with data hydration. 116 | 117 | ```js 118 | import React from 'react' 119 | import { StaticRouter } from 'react-router' 120 | import { renderToString } from 'react-dom/server' 121 | import { onPageLoad } from 'meteor/server-render' 122 | import App from '/imports/App' 123 | import { DataCaptureProvider } from 'meteor/npdev:collections' 124 | 125 | onPageLoad(sink => { 126 | const context = {} 127 | 128 | // use the DataCaptureProvider with a scoped dataHandle 129 | const dataHandle = {} 130 | const app = 131 | 132 | 133 | 134 | 135 | 136 | // render the app to html 137 | const content = renderToString(app) 138 | 139 | // render out the html 140 | sink.renderIntoElementById('root', content) 141 | 142 | // render out the captured data 143 | sink.appendToBody(dataHandle.toScriptTag()) 144 | }) 145 | ``` 146 | 147 | Behind the scenes this provider is watching for all the queries that happen during rendering of the current route, and captures that data in a property on `dataHandle`. Then the `toScriptTag` method is used to render out a `` 14 | ) 15 | return 16 | {children} 17 | 18 | } 19 | 20 | export const createConnector = ({ name, collection, validate, query, single = false }) => { 21 | const run = single 22 | ? makeSingleRun(collection, query) 23 | : makePagedRun(collection, query) 24 | makeDataMethod(name, validate, run) 25 | makePruneMethod(name, collection, validate, query) 26 | return (args = {}) => { 27 | validate(args) 28 | const captureData = useContext(ConnectorContext) 29 | const docs = run(args) 30 | captureData.push({ name: collection._name, docs: single ? [docs] : docs }) 31 | return [docs, false] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | export { createCollection, getCollectionByName } from './server-collection' 2 | export { DataCaptureProvider, createConnector } from './server-connector' 3 | --------------------------------------------------------------------------------