├── .eslintrc.js
├── .github
└── workflows
│ └── test-react-packages.yml
├── .gitignore
├── LICENSE
├── README.md
├── examples
├── material-ui-leaderboard
│ ├── .meteor
│ │ ├── .finished-upgraders
│ │ ├── .gitignore
│ │ ├── .id
│ │ ├── cordova-plugins
│ │ ├── packages
│ │ ├── platforms
│ │ ├── release
│ │ └── versions
│ ├── README.md
│ ├── client
│ │ ├── components
│ │ │ ├── app.jsx
│ │ │ └── leaderboard.jsx
│ │ ├── index.html
│ │ ├── startup.jsx
│ │ └── style.css
│ ├── lib
│ │ └── collection.js
│ ├── public
│ │ ├── Ada Lovelace.png
│ │ ├── Carl Friedrich Gauss.png
│ │ ├── Claude Shannon.png
│ │ ├── Grace Hopper.png
│ │ ├── Marie Curie.png
│ │ └── Nikola Tesla.png
│ ├── screenshot.png
│ └── server
│ │ └── fixtures.js
└── react-in-blaze
│ ├── .meteor
│ ├── .finished-upgraders
│ ├── .gitignore
│ ├── .id
│ ├── packages
│ ├── platforms
│ ├── release
│ └── versions
│ ├── README.md
│ ├── client
│ ├── app.css
│ ├── app.html
│ ├── app.js
│ └── lib
│ │ ├── app.browserify.js
│ │ └── app.browserify.options.json
│ ├── lib
│ └── collection.js
│ ├── packages.json
│ └── packages
│ └── npm-container
│ ├── .npm
│ └── package
│ │ ├── .gitignore
│ │ ├── README
│ │ └── npm-shrinkwrap.json
│ ├── index.js
│ └── package.js
├── package-lock.json
├── package.json
├── packages
├── react-meteor-accounts
│ ├── .gitignore
│ ├── .meteorignore
│ ├── .versions
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── index.tests.ts
│ ├── index.ts
│ ├── package-lock.json
│ ├── package.js
│ ├── package.json
│ ├── react-accounts.tests.tsx
│ ├── react-accounts.tsx
│ ├── tsconfig.json
│ └── types
│ │ └── react-accounts.d.ts
├── react-meteor-data
│ ├── .meteorignore
│ ├── .npm
│ │ └── package
│ │ │ ├── .gitignore
│ │ │ ├── README
│ │ │ └── npm-shrinkwrap.json
│ ├── .versions
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── index.ts
│ ├── package-types.json
│ ├── package.js
│ ├── package.json
│ ├── react-meteor-data.d.ts
│ ├── suspense
│ │ ├── index.js
│ │ ├── react-meteor-data.d.ts
│ │ ├── useFind.tests.js
│ │ ├── useFind.ts
│ │ ├── useSubscribe.tests.js
│ │ ├── useSubscribe.ts
│ │ └── useTracker.ts
│ ├── tests.js
│ ├── useFind.tests.js
│ ├── useFind.ts
│ ├── useSubscribe.ts
│ ├── useTracker.tests.js
│ ├── useTracker.ts
│ ├── withTracker.tests.js
│ └── withTracker.tsx
└── react-template-helper
│ ├── .versions
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.js
│ ├── react-template-helper.html
│ └── react-template-helper.js
├── tests
├── react-meteor-accounts-harness
│ ├── .meteor
│ │ ├── .finished-upgraders
│ │ ├── .gitignore
│ │ ├── .id
│ │ ├── packages
│ │ ├── platforms
│ │ ├── release
│ │ └── versions
│ ├── package-lock.json
│ └── package.json
└── react-meteor-data-harness
│ ├── .meteor
│ ├── .finished-upgraders
│ ├── .gitignore
│ ├── .id
│ ├── packages
│ ├── platforms
│ ├── release
│ └── versions
│ ├── package-lock.json
│ └── package.json
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: true,
5 | es2021: true
6 | },
7 | settings: {},
8 | extends: [
9 | 'plugin:react/recommended',
10 | 'standard-with-typescript'
11 | ],
12 | overrides: [],
13 | parserOptions: {
14 | ecmaVersion: 'latest',
15 | sourceType: 'module',
16 | project: './tsconfig.json'
17 | },
18 | plugins: [
19 | 'react'
20 | ],
21 | rules: {
22 | 'object-curly-spacing': ['error', 'always'],
23 | 'space-before-function-paren': ['error',
24 | {
25 | anonymous: 'never',
26 | named: 'never',
27 | asyncArrow: 'always'
28 | }
29 | ],
30 | '@typescript-eslint/space-before-function-paren': ['error',
31 | {
32 | anonymous: 'never',
33 | named: 'never',
34 | asyncArrow: 'always'
35 | }],
36 | '@typescript-eslint/return-await': 'off',
37 | '@typescript-eslint/strict-boolean-expressions': 'off'
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/test-react-packages.yml:
--------------------------------------------------------------------------------
1 | name: Meteor CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | name: Meteor react-packages tests
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Cache Meteor
20 | uses: actions/cache@v4
21 | with:
22 | path: ~/.meteor
23 | key: meteor-cache
24 |
25 | - name: Setup Meteor
26 | uses: meteorengineer/setup-meteor@v2
27 | with:
28 | meteor-release: '3.1.2'
29 |
30 | - name: Prepare mtest
31 | run: |
32 | meteor npm i -g @zodern/mtest
33 |
34 | - name: Install and test in react-meteor-data
35 | working-directory: tests/react-meteor-data-harness
36 | run: |
37 | meteor npm install
38 | meteor npm run test:ci
39 |
40 | - name: Install and test in react-meteor-accounts-harness
41 | working-directory: tests/react-meteor-accounts-harness
42 | run: |
43 | meteor npm install
44 | meteor npm run test:ci
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /.idea/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011 - present Meteor Software Ltd.
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 |
23 |
24 | ====================================================================
25 | This license applies to all code in Meteor that is not an externally
26 | maintained library. Externally maintained libraries have their own
27 | licenses, included in the LICENSES directory.
28 | ====================================================================
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-packages
2 |
3 | Meteor packages for a great React developer experience
4 |
5 | ### Linting
6 |
7 | Run
8 |
9 | ```
10 | npm run lint
11 | ```
12 |
13 | Note this does not yet all lint. Working on it.
14 |
15 | ### Testing
16 |
17 | Due to difficulties in testing packages with "peer" NPM dependencies, we've worked around by moving package tests into harness test apps. You can find them in `tests/`.
18 |
19 | To run tests for a Meteor react package, navigate to its `tests/*` app and execute `npm test`.
20 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | notices-for-0.9.0
8 | notices-for-0.9.1
9 | 0.9.4-platform-file
10 | notices-for-facebook-graph-api-2
11 | notices-for-0.9.0
12 | notices-for-0.9.1
13 | 0.9.4-platform-file
14 | notices-for-facebook-graph-api-2
15 | 1.2.0-standard-minifiers-package
16 | 1.2.0-meteor-platform-split
17 | 1.2.0-cordova-changes
18 | 1.2.0-breaking-changes
19 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | f98bo41q74rpv12upjp8
8 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/cordova-plugins:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/.meteor/cordova-plugins
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base # Packages every Meteor app needs to have
8 | mobile-experience # Packages for a great mobile UX
9 | mongo # The database Meteor supports right now
10 |
11 | standard-minifiers # JS/CSS minifiers run for production mode
12 | es5-shim # ECMAScript 5 compatibility for older browsers.
13 | ecmascript # Enable ECMAScript2015+ syntax in app code
14 |
15 | autopublish # Publish all data to the clients (for prototyping)
16 | insecure # Allow all DB writes from clients (for prototyping)
17 |
18 | react
19 | jsx
20 | static-html
21 | markoshust:material-ui
22 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 | android
4 | ios
5 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.2.1
2 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/.meteor/versions:
--------------------------------------------------------------------------------
1 | autopublish@1.0.4
2 | autoupdate@1.2.4
3 | babel-compiler@5.8.24_1
4 | babel-runtime@0.1.4
5 | base64@1.0.4
6 | binary-heap@1.0.4
7 | blaze@2.1.3
8 | blaze-tools@1.0.4
9 | boilerplate-generator@1.0.4
10 | caching-compiler@1.0.0
11 | caching-html-compiler@1.0.2
12 | callback-hook@1.0.4
13 | check@1.1.0
14 | coffeescript@1.0.11
15 | cosmos:browserify@0.9.2
16 | ddp@1.2.2
17 | ddp-client@1.2.1
18 | ddp-common@1.2.2
19 | ddp-server@1.2.2
20 | deps@1.0.9
21 | diff-sequence@1.0.1
22 | ecmascript@0.1.6
23 | ecmascript-runtime@0.2.6
24 | ejson@1.0.7
25 | es5-shim@4.1.14
26 | fastclick@1.0.7
27 | geojson-utils@1.0.4
28 | hot-code-push@1.0.0
29 | html-tools@1.0.5
30 | htmljs@1.0.5
31 | http@1.1.1
32 | id-map@1.0.4
33 | insecure@1.0.4
34 | jquery@1.11.4
35 | jsx@0.2.3
36 | launch-screen@1.0.4
37 | livedata@1.0.15
38 | logging@1.0.8
39 | markoshust:material-ui@0.13.1_5
40 | meteor@1.1.10
41 | meteor-base@1.0.1
42 | minifiers@1.1.7
43 | minimongo@1.0.10
44 | mobile-experience@1.0.1
45 | mobile-status-bar@1.0.6
46 | mongo@1.1.3
47 | mongo-id@1.0.1
48 | npm-mongo@1.4.39_1
49 | observe-sequence@1.0.7
50 | ordered-dict@1.0.4
51 | promise@0.5.1
52 | random@1.0.5
53 | react@0.14.3
54 | react-meteor-data@0.2.3
55 | react-runtime@0.14.3
56 | react-runtime-dev@0.14.3
57 | react-runtime-prod@0.14.3
58 | reactive-var@1.0.6
59 | reload@1.1.4
60 | retry@1.0.4
61 | routepolicy@1.0.6
62 | spacebars@1.0.7
63 | spacebars-compiler@1.0.7
64 | standard-minifiers@1.0.2
65 | static-html@1.0.3
66 | templating-tools@1.0.0
67 | tracker@1.0.9
68 | ui@1.0.8
69 | underscore@1.0.4
70 | url@1.0.5
71 | webapp@1.2.3
72 | webapp-hashing@1.0.5
73 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/README.md:
--------------------------------------------------------------------------------
1 | # Material UI Leaderboard
2 |
3 |
4 |
5 | This is an example of building the leaderboard app using React and the [Material UI](http://material-ui.com/) components library.
6 |
7 | ### Using material-ui package
8 |
9 | We are using the [markoshust:material-ui](https://atmospherejs.com/markoshust/material-ui) package, which uses `material-ui` React component library from NPM.
10 |
--------------------------------------------------------------------------------
/examples/material-ui-leaderboard/client/components/app.jsx:
--------------------------------------------------------------------------------
1 | const {
2 | RaisedButton,
3 | Styles
4 | } = mui;
5 | const ThemeManager = Styles.ThemeManager;
6 |
7 | App = React.createClass({
8 | mixins: [ReactMeteorData],
9 | getInitialState: function () {
10 | return {
11 | selectedPlayerId: null
12 | };
13 | },
14 | childContextTypes: {
15 | muiTheme: React.PropTypes.object
16 | },
17 | getChildContext: function() {
18 | return {
19 | muiTheme: ThemeManager.getMuiTheme(Styles.LightRawTheme)
20 | };
21 | },
22 | getMeteorData() {
23 | return {
24 | players: Players.find({}, { sort: { score: -1, name: 1 } }).fetch(),
25 | selectedPlayer: Players.findOne(this.state.selectedPlayerId)
26 | }
27 | },
28 | selectPlayer(playerId) {
29 | this.setState({
30 | selectedPlayerId: playerId
31 | });
32 | },
33 | addPointsToPlayer(playerId) {
34 | Players.update(playerId, {$inc: {score: 5}});
35 | },
36 | getBottomBar() {
37 | return this.state.selectedPlayerId
38 | ? (
39 |
15 | {{> React component=Griddle
16 | results=players
17 | columns=columns
18 | initialSort="id"
19 | initialSortAscending=false
20 | useGriddleStyles=false
21 | tableClassName="table table-striped table-bordered" }}
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/client/app.js:
--------------------------------------------------------------------------------
1 | var displayColumns = ["id", "name", "city", "state", "country"];
2 | var formColumns = displayColumns.slice(1);
3 |
4 | Template.body.helpers({
5 | Griddle: function () {
6 | return Griddle;
7 | },
8 | columns: function () {
9 | return displayColumns;
10 | },
11 | formColumns: function () {
12 | return formColumns;
13 | },
14 | players: function () {
15 | return Players.find().fetch();
16 | }
17 | });
18 |
19 | Template.body.events({
20 | 'submit #add-new-player-form': function (e) {
21 | e.preventDefault();
22 | var formData = new FormData(e.target);
23 | var player = {};
24 | formColumns.forEach(function (col) {
25 | player[col] = formData.get(col);
26 | });
27 | player.id = Players.find().count();
28 | Players.insert(player);
29 | e.target.reset();
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/client/lib/app.browserify.js:
--------------------------------------------------------------------------------
1 | Griddle = require('griddle-react');
2 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/client/lib/app.browserify.options.json:
--------------------------------------------------------------------------------
1 | {
2 | "transforms": {
3 | "externalify": {
4 | "global": true,
5 | "external": {
6 | "react": "React.require"
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/lib/collection.js:
--------------------------------------------------------------------------------
1 | Players = new Mongo.Collection('players');
2 |
3 | if (Meteor.isServer) {
4 | Meteor.startup(function () {
5 | if (Players.find().count() === 0) {
6 | [
7 | {
8 | "id": 0,
9 | "name": "Mayer Leonard",
10 | "city": "Kapowsin",
11 | "state": "Hawaii",
12 | "country": "United Kingdom"
13 | },
14 | {
15 | "id": 1,
16 | "name": "Koch Becker",
17 | "city": "Johnsonburg",
18 | "state": "New Jersey",
19 | "country": "Madagascar"
20 | },
21 | {
22 | "id": 2,
23 | "name": "Lowery Hopkins",
24 | "city": "Blanco",
25 | "state": "Arizona",
26 | "country": "Ukraine"
27 | },
28 | {
29 | "id": 3,
30 | "name": "Walters Mays",
31 | "city": "Glendale",
32 | "state": "Illinois",
33 | "country": "New Zealand"
34 | },
35 | {
36 | "id": 4,
37 | "name": "Shaw Lowe",
38 | "city": "Coultervillle",
39 | "state": "Wyoming",
40 | "country": "Ecuador"
41 | }
42 | ].forEach(function (player) {
43 | Players.insert(player);
44 | });
45 | }
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/packages.json:
--------------------------------------------------------------------------------
1 | {
2 | "griddle-react": "0.2.13",
3 | "externalify": "0.1.0"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/packages/npm-container/.npm/package/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/packages/npm-container/.npm/package/README:
--------------------------------------------------------------------------------
1 | This directory and the files immediately inside it are automatically generated
2 | when you change this package's NPM dependencies. Commit the files in this
3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
4 | so that others run the same versions of sub-dependencies.
5 |
6 | You should NOT check in the node_modules directory that Meteor automatically
7 | creates; if you are using git, the .gitignore file tells git to ignore it.
8 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/packages/npm-container/.npm/package/npm-shrinkwrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "externalify": {
4 | "version": "0.1.0",
5 | "dependencies": {
6 | "globo": {
7 | "version": "1.0.2",
8 | "dependencies": {
9 | "accessory": {
10 | "version": "1.0.1",
11 | "dependencies": {
12 | "dot-parts": {
13 | "version": "1.0.1"
14 | }
15 | }
16 | },
17 | "is-defined": {
18 | "version": "1.0.0"
19 | },
20 | "ternary": {
21 | "version": "1.0.0"
22 | }
23 | }
24 | },
25 | "map-obj": {
26 | "version": "1.0.1"
27 | },
28 | "replace-require-functions": {
29 | "version": "1.0.0",
30 | "dependencies": {
31 | "detective": {
32 | "version": "4.1.1",
33 | "dependencies": {
34 | "acorn": {
35 | "version": "1.2.2"
36 | },
37 | "defined": {
38 | "version": "1.0.0"
39 | },
40 | "escodegen": {
41 | "version": "1.7.0",
42 | "dependencies": {
43 | "estraverse": {
44 | "version": "1.9.3"
45 | },
46 | "esutils": {
47 | "version": "2.0.2"
48 | },
49 | "esprima": {
50 | "version": "1.2.5"
51 | },
52 | "optionator": {
53 | "version": "0.5.0",
54 | "dependencies": {
55 | "prelude-ls": {
56 | "version": "1.1.2"
57 | },
58 | "deep-is": {
59 | "version": "0.1.3"
60 | },
61 | "wordwrap": {
62 | "version": "0.0.3"
63 | },
64 | "type-check": {
65 | "version": "0.3.1"
66 | },
67 | "levn": {
68 | "version": "0.2.5"
69 | },
70 | "fast-levenshtein": {
71 | "version": "1.0.7"
72 | }
73 | }
74 | },
75 | "source-map": {
76 | "version": "0.2.0",
77 | "dependencies": {
78 | "amdefine": {
79 | "version": "1.0.0"
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | },
87 | "has-require": {
88 | "version": "1.2.1",
89 | "dependencies": {
90 | "escape-string-regexp": {
91 | "version": "1.0.3"
92 | }
93 | }
94 | },
95 | "patch-text": {
96 | "version": "1.0.2"
97 | },
98 | "xtend": {
99 | "version": "4.0.0"
100 | }
101 | }
102 | },
103 | "through2": {
104 | "version": "0.4.2",
105 | "dependencies": {
106 | "readable-stream": {
107 | "version": "1.0.33",
108 | "dependencies": {
109 | "core-util-is": {
110 | "version": "1.0.1"
111 | },
112 | "isarray": {
113 | "version": "0.0.1"
114 | },
115 | "string_decoder": {
116 | "version": "0.10.31"
117 | },
118 | "inherits": {
119 | "version": "2.0.1"
120 | }
121 | }
122 | },
123 | "xtend": {
124 | "version": "2.1.2",
125 | "dependencies": {
126 | "object-keys": {
127 | "version": "0.4.0"
128 | }
129 | }
130 | }
131 | }
132 | },
133 | "transformify": {
134 | "version": "0.1.2",
135 | "dependencies": {
136 | "readable-stream": {
137 | "version": "1.1.13",
138 | "dependencies": {
139 | "core-util-is": {
140 | "version": "1.0.1"
141 | },
142 | "isarray": {
143 | "version": "0.0.1"
144 | },
145 | "string_decoder": {
146 | "version": "0.10.31"
147 | },
148 | "inherits": {
149 | "version": "2.0.1"
150 | }
151 | }
152 | }
153 | }
154 | }
155 | }
156 | },
157 | "griddle-react": {
158 | "version": "0.2.13"
159 | },
160 | "react": {
161 | "version": "0.14.3",
162 | "dependencies": {
163 | "envify": {
164 | "version": "3.4.0",
165 | "dependencies": {
166 | "through": {
167 | "version": "2.3.8"
168 | },
169 | "jstransform": {
170 | "version": "10.1.0",
171 | "dependencies": {
172 | "base62": {
173 | "version": "0.1.1"
174 | },
175 | "esprima-fb": {
176 | "version": "13001.1001.0-dev-harmony-fb"
177 | },
178 | "source-map": {
179 | "version": "0.1.31",
180 | "dependencies": {
181 | "amdefine": {
182 | "version": "1.0.0"
183 | }
184 | }
185 | }
186 | }
187 | }
188 | }
189 | },
190 | "fbjs": {
191 | "version": "0.3.2",
192 | "dependencies": {
193 | "core-js": {
194 | "version": "1.2.6"
195 | },
196 | "loose-envify": {
197 | "version": "1.1.0",
198 | "dependencies": {
199 | "js-tokens": {
200 | "version": "1.0.2"
201 | }
202 | }
203 | },
204 | "promise": {
205 | "version": "7.0.4",
206 | "dependencies": {
207 | "asap": {
208 | "version": "2.0.3"
209 | }
210 | }
211 | },
212 | "ua-parser-js": {
213 | "version": "0.7.9"
214 | },
215 | "whatwg-fetch": {
216 | "version": "0.9.0"
217 | }
218 | }
219 | }
220 | }
221 | },
222 | "underscore": {
223 | "version": "1.8.3"
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/examples/react-in-blaze/packages/npm-container/index.js:
--------------------------------------------------------------------------------
1 | Meteor.npmRequire = function(moduleName) {
2 | var module = Npm.require(moduleName);
3 | return module;
4 | };
5 |
6 | Meteor.require = function(moduleName) {
7 | console.warn('Meteor.require is deprecated. Please use Meteor.npmRequire instead!');
8 | return Meteor.npmRequire(moduleName);
9 | };
--------------------------------------------------------------------------------
/examples/react-in-blaze/packages/npm-container/package.js:
--------------------------------------------------------------------------------
1 | var path = Npm.require('path');
2 | var fs = Npm.require('fs');
3 |
4 | Package.describe({
5 | summary: 'Contains all your npm dependencies',
6 | version: '1.2.0',
7 | name: 'npm-container'
8 | });
9 |
10 | var packagesJsonFile = path.resolve('./packages.json');
11 | try {
12 | var fileContent = fs.readFileSync(packagesJsonFile);
13 | var packages = JSON.parse(fileContent.toString());
14 | Npm.depends(packages);
15 | } catch (ex) {
16 | console.error('ERROR: packages.json parsing error [ ' + ex.message + ' ]');
17 | }
18 |
19 | // Adding the app's packages.json as a used file for this package will get
20 | // Meteor to watch it and reload this package when it changes
21 | Package.onUse(function(api) {
22 | api.addFiles('index.js', 'server');
23 | if (api.addAssets) {
24 | api.addAssets('../../packages.json', 'server');
25 | } else {
26 | api.addFiles('../../packages.json', 'server', {
27 | isAsset: true
28 | });
29 | }
30 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-packages",
3 | "description": "Meteor packages for a great React developer experience",
4 | "scripts": {
5 | "lint": "eslint . || true"
6 | },
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/meteor/react-packages.git"
10 | },
11 | "keywords": [
12 | "meteor",
13 | "react"
14 | ],
15 | "author": "Tom Colemna",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/meteor/react-packages/issues"
19 | },
20 | "homepage": "https://github.com/meteor/react-packages#readme",
21 | "dependencies": {
22 | "eslint-config-airbnb": "^18.2.1",
23 | "eslint-plugin-jsx-a11y": "^6.4.1"
24 | },
25 | "eslintConfig": {
26 | "extends": "airbnb"
27 | },
28 | "devDependencies": {
29 | "@typescript-eslint/eslint-plugin": "^5.54.1",
30 | "eslint": "^8.35.0",
31 | "eslint-config-standard-with-typescript": "^34.0.0",
32 | "eslint-plugin-import": "^2.27.5",
33 | "eslint-plugin-n": "^15.6.1",
34 | "eslint-plugin-promise": "^6.1.1",
35 | "eslint-plugin-react": "^7.32.2",
36 | "typescript": "^4.9.5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/.meteorignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package.json
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/.versions:
--------------------------------------------------------------------------------
1 | accounts-base@3.0.0
2 | accounts-password@3.0.0
3 | allow-deny@2.0.0
4 | babel-compiler@7.11.0
5 | babel-runtime@1.5.2
6 | base64@1.0.13
7 | binary-heap@1.0.12
8 | boilerplate-generator@2.0.0
9 | callback-hook@1.6.0
10 | check@1.4.2
11 | core-runtime@1.0.0
12 | ddp@1.4.2
13 | ddp-client@3.0.0
14 | ddp-common@1.4.3
15 | ddp-rate-limiter@1.2.2
16 | ddp-server@3.0.0
17 | diff-sequence@1.1.3
18 | dynamic-import@0.7.4
19 | ecmascript@0.16.9
20 | ecmascript-runtime@0.8.2
21 | ecmascript-runtime-client@0.12.2
22 | ecmascript-runtime-server@0.11.1
23 | ejson@1.1.4
24 | email@3.0.0
25 | facts-base@1.0.2
26 | fetch@0.1.5
27 | geojson-utils@1.0.12
28 | id-map@1.2.0
29 | inter-process-messaging@0.1.2
30 | local-test:react-meteor-accounts@1.0.3
31 | localstorage@1.2.1
32 | logging@1.3.5
33 | meteor@2.0.0
34 | minimongo@2.0.0
35 | modern-browsers@0.1.11
36 | modules@0.20.1
37 | modules-runtime@0.13.2
38 | mongo@2.0.0
39 | mongo-decimal@0.1.4-beta300.7
40 | mongo-dev-server@1.1.1
41 | mongo-id@1.0.9
42 | npm-mongo@4.17.3
43 | ordered-dict@1.2.0
44 | promise@1.0.0
45 | random@1.2.2
46 | rate-limit@1.1.2
47 | react-fast-refresh@0.2.9
48 | react-meteor-accounts@1.0.3
49 | reactive-var@1.0.13
50 | reload@1.3.2
51 | retry@1.1.1
52 | routepolicy@1.1.2
53 | sha@1.0.10
54 | socket-stream-client@0.5.3
55 | tinytest@1.3.0
56 | tracker@1.3.4
57 | typescript@5.4.3
58 | underscore@1.6.4
59 | url@1.3.3
60 | webapp@2.0.0
61 | webapp-hashing@1.1.2
62 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Release versions follow [Semantic Versioning 2.0.0 guidelines](https://semver.org/).
4 |
5 | ## v1.0.3
6 |
7 | Update api.versionsFrom() adding `3.0` to support Meteor 3.0 official.
8 |
9 | ## v1.0.2
10 |
11 | Update api.versionsFrom() adding `3.0-alpha.19` to support Meteor 3.0-alpha.
12 |
13 | ## v1.0.1
14 |
15 | Bump `.versions` dependencies to match `react-meteor-data` dependencies.
16 |
17 | ## v1.0.0
18 |
19 | Published as a core package named `react-meteor-accounts`.
20 |
21 | ## v1.0.0-rc.1
22 |
23 | - `useLoggingIn`: Added implementation
24 | - `useLoggingOut`: Added implementation
25 | - `withLoggingIn`: Added implementation
26 | - `withLoggingOut`: Added implementation
27 | - improved tests and readme
28 |
29 | ## v1.0.0-beta.1
30 |
31 | 2021-10-20 (date of last commit)
32 |
33 | ### Features
34 |
35 | - `useUserId`: initial implementation.
36 | - `useUser`: initial implementation.
37 | - `withUserId`: initial implementation.
38 | - `withUser`: initial implementation.
39 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/README.md:
--------------------------------------------------------------------------------
1 | # react-meteor-accounts
2 |
3 | Simple hooks and higher-order components (HOCs) for getting reactive, stateful values of Meteor's Accounts data sources.
4 |
5 | ## Table of Contents
6 |
7 | - [Installation](#installation)
8 | - [Peer npm dependencies](#peer-npm-dependencies)
9 | - [Changelog](#changelog)
10 | - [Usage](#usage)
11 | - [`useUser`](#useuser)
12 | - [`useUserId`](#useuserid)
13 | - [`useLoggingIn`](#useloggingin)
14 | - [`useLoggingOut`](#useloggingout)
15 | - [`withUser`](#withuser)
16 | - [`withUserId`](#withuserid)
17 | - [`withLoggingIn`](#withloggingin)
18 | - [`withLoggingOut`](#withloggingout)
19 |
20 | ## Installation
21 |
22 | Install the package from Atmosphere:
23 |
24 | ```shell
25 | meteor add react-meteor-accounts
26 | ```
27 |
28 | ### Peer npm dependencies
29 |
30 | Install React if you have not already:
31 |
32 | ```shell
33 | meteor npm install react
34 | ```
35 |
36 | _Note:_ The minimum supported version of React is v16.8 ("the one with hooks").
37 |
38 | ### Changelog
39 |
40 | For recent changes, check the [changelog](./CHANGELOG.md).
41 |
42 | ## Usage
43 |
44 | Utilities for each data source are available for the two ways of writing React components: hooks and higher-order components (HOCs). Hooks can only be used in functional components. HOCs can be used for both functional and class components, but are primarily for the latter.
45 |
46 | _Note:_ All HOCs forward refs.
47 |
48 | ### useUser()
49 |
50 | Get a stateful value of the current user record. A hook. Uses [`Meteor.user`](https://docs.meteor.com/api/accounts.html#Meteor-user), a reactive data source.
51 |
52 | - Arguments: *none*.
53 | - Returns: `object | null`.
54 |
55 | Example:
56 |
57 | ```tsx
58 | import React from 'react';
59 | import { useUser } from 'meteor/react-meteor-accounts';
60 |
61 | function Foo() {
62 | const user = useUser();
63 |
64 | if (user === null) {
65 | return `) is currently in progress. A hook. Uses [`Meteor.loggingIn`](https://docs.meteor.com/api/accounts.html#Meteor-loggingIn), a reactive data source.
116 |
117 | - Arguments: *none*.
118 | - Returns: `boolean`.
119 |
120 | Example:
121 |
122 | ```tsx
123 | import React from 'react';
124 | import { useLoggingIn } from 'meteor/react-meteor-accounts';
125 |
126 | function Foo() {
127 | const loggingIn = useLoggingIn();
128 |
129 | if (!loggingIn) {
130 | return null;
131 | }
132 |
133 | return (
134 | Logging in, please wait a moment.
135 | );
136 | }
137 | ```
138 |
139 | TypeScript signature:
140 |
141 | ```ts
142 | function useLoggingIn(): boolean;
143 | ```
144 |
145 | ### useLoggingOut()
146 |
147 | Get a stateful value of whether the logout method is currently in progress. A hook. Uses `Meteor.loggingOut` (no online documentation), a reactive data source.
148 |
149 | - Arguments: *none*.
150 | - Returns: `boolean`.
151 |
152 | Example:
153 |
154 | ```tsx
155 | import React from 'react';
156 | import { useLoggingOut } from 'meteor/react-meteor-accounts';
157 |
158 | function Foo() {
159 | const loggingOut = useLoggingOut();
160 |
161 | if (!loggingOut) {
162 | return null;
163 | }
164 |
165 | return (
166 | Logging out, please wait a moment.
167 | );
168 | }
169 | ```
170 |
171 | TypeScript signature:
172 |
173 | ```ts
174 | function useLoggingOut(): boolean;
175 | ```
176 |
177 | ### withUser(...)
178 |
179 | Return a wrapped version of the given component, where the component receives a stateful prop of the current user record, `user`. A higher-order component. Uses [`Meteor.user`](https://docs.meteor.com/api/accounts.html#Meteor-user), a reactive data source.
180 |
181 | - Arguments:
182 |
183 | | Argument | Type | Required | Description |
184 | | --- | --- | --- | --- |
185 | | Component | `React.ComponentType` | yes | A React component. |
186 |
187 | - Returns: `React.ForwardRefExoticComponent`.
188 |
189 | Examples:
190 |
191 | ```tsx
192 | import React from 'react';
193 | import { withUser } from 'meteor/react-meteor-accounts';
194 |
195 | class Foo extends React.Component {
196 | render() {
197 | if (this.props.user === null) {
198 | return Log in
;
199 | }
200 |
201 | return Hello {this.props.user.username}
;
202 | }
203 | }
204 |
205 | const FooWithUser = withUser(Foo);
206 | ```
207 |
208 | TypeScript signature:
209 |
210 | ```ts
211 | function withUser(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
212 | ```
213 |
214 | ### withUserId(...)
215 |
216 | Return a wrapped version of the given component, where the component receives a stateful prop of the current user id. A higher-order component. Uses [`Meteor.userId`](https://docs.meteor.com/api/accounts.html#Meteor-userId), a reactive data source.
217 |
218 | - Arguments:
219 |
220 | | Argument | Type | Required | Description |
221 | | --- | --- | --- | --- |
222 | | Component | `React.ComponentType` | yes | A React component. |
223 |
224 | - Returns: `React.ForwardRefExoticComponent`.
225 |
226 | Example:
227 |
228 | ```tsx
229 | import React from 'react';
230 | import { withUserId } from 'meteor/react-meteor-accounts';
231 |
232 | class Foo extends React.Component {
233 | render() {
234 | return (
235 |
236 |
Account Details
237 | {this.props.userId ? (
238 |
Your unique account id is {this.props.userId}.
239 | ) : (
240 |
Log-in to view your account details.
241 | )}
242 |
243 | );
244 | }
245 | }
246 |
247 | const FooWithUserId = withUserId(Foo);
248 | ```
249 |
250 | TypeScript signature:
251 |
252 | ```ts
253 | function withUserId(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
254 | ```
255 |
256 | ### withLoggingIn(...)
257 |
258 | Return a wrapped version of the given component, where the component receives a stateful prop of whether a login method (e.g. `loginWith`) is currently in progress. A higher-order component. Uses [`Meteor.loggingIn`](https://docs.meteor.com/api/accounts.html#Meteor-loggingIn), a reactive data source.
259 |
260 | - Arguments:
261 |
262 | | Argument | Type | Required | Description |
263 | | --- | --- | --- | --- |
264 | | Component | `React.ComponentType` | yes | A React component. |
265 |
266 | - Returns: `React.ForwardRefExoticComponent`.
267 |
268 | Example:
269 |
270 | ```tsx
271 | import React from 'react';
272 | import { withLoggingIn } from 'meteor/react-meteor-accounts';
273 |
274 | class Foo extends React.Component {
275 | render() {
276 | if (!this.props.loggingIn) {
277 | return null;
278 | }
279 |
280 | return (
281 | Logging in, please wait a moment.
282 | );
283 | }
284 | }
285 |
286 | const FooWithLoggingIn = withLoggingIn(Foo);
287 | ```
288 |
289 | TypeScript signatures:
290 |
291 | ```ts
292 | function withLoggingIn(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
293 | ```
294 |
295 | ### withLoggingOut(...)
296 |
297 | Return a wrapped version of the given component, where the component receives a stateful prop of whether the logout method is currently in progress. A higher-order component. Uses [`Meteor.loggingOut`](https://docs.meteor.com/api/accounts.html#Meteor-loggingOut), a reactive data source.
298 |
299 | - Arguments:
300 |
301 | | Argument | Type | Required | Description |
302 | | --- | --- | --- | --- |
303 | | Component | `React.ComponentType` | yes | A React component. |
304 |
305 | - Returns: `React.ForwardRefExoticComponent`.
306 |
307 | Example:
308 |
309 | ```tsx
310 | import React from 'react';
311 | import { withLoggingOut } from 'meteor/react-meteor-accounts';
312 |
313 | class Foo extends React.Component {
314 | render() {
315 | if (!this.props.loggingOut) {
316 | return null;
317 | }
318 |
319 | return (
320 | Logging out, please wait a moment.
321 | );
322 | }
323 | }
324 |
325 | const FooWithLoggingOut = withLoggingOut(Foo);
326 | ```
327 |
328 | TypeScript signature:
329 |
330 | ```ts
331 | function withLoggingOut(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
332 | ```
333 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/index.tests.ts:
--------------------------------------------------------------------------------
1 | import './react-accounts.tests.tsx';
2 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/index.ts:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 |
4 | if (Meteor.isDevelopment) {
5 | // Custom check instead of `checkNpmVersions` to reduce prod bundle size (~8kb).
6 | const v = React.version.split('.').map(val => parseInt(val));
7 | if (v[0] < 16 || (v[0] === 16 && v[1] < 8)) {
8 | console.warn('react-accounts requires React version >= 16.8.');
9 | }
10 | }
11 |
12 | export {
13 | useUser,
14 | useUserId,
15 | useLoggingIn,
16 | useLoggingOut,
17 | withUser,
18 | withUserId,
19 | withLoggingIn,
20 | withLoggingOut
21 | } from './react-accounts';
22 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/package.js:
--------------------------------------------------------------------------------
1 | /* global Package */
2 |
3 | Package.describe({
4 | name: 'react-meteor-accounts',
5 | summary: 'React hooks and HOCs for reactively tracking Meteor Accounts data',
6 | version: '1.0.3',
7 | documentation: 'README.md',
8 | git: 'https://github.com/meteor/react-packages',
9 | });
10 |
11 | Package.onUse((api) => {
12 | api.versionsFrom(['1.10', '2.3', '3.0']);
13 |
14 | api.use(['accounts-base', 'tracker', 'typescript']);
15 |
16 | api.mainModule('index.ts', ['client', 'server'], { lazy: true });
17 | });
18 |
19 | Package.onTest((api) => {
20 | api.use([
21 | 'accounts-base',
22 | 'accounts-password',
23 | 'tinytest',
24 | 'tracker',
25 | 'typescript',
26 | ]);
27 |
28 | api.mainModule('index.tests.ts');
29 | });
30 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meteor-react-accounts",
3 | "scripts": {
4 | "make-types": "npx typescript react-accounts.tsx --jsx preserve --declaration --emitDeclarationOnly --esModuleInterop --outDir types --strict"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/react-accounts.tests.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react-hooks/dom';
2 | import { cleanup, render, waitFor } from "@testing-library/react";
3 | import { Accounts } from "meteor/accounts-base";
4 | import { Meteor } from "meteor/meteor";
5 | import { Tinytest } from "meteor/tinytest";
6 | import React from "react";
7 | import {
8 | useLoggingIn,
9 | useLoggingOut,
10 | useUser,
11 | useUserId,
12 | withLoggingIn,
13 | WithLoggingInProps,
14 | withLoggingOut,
15 | WithLoggingOutProps,
16 | withUser,
17 | withUserId,
18 | WithUserIdProps,
19 | WithUserProps,
20 | } from "./react-accounts";
21 |
22 | // Prepare method for clearing DB (doesn't need to be isomorphic).
23 | if (Meteor.isServer) {
24 | Meteor.methods({
25 | async reset() {
26 | await Meteor.users.removeAsync({});
27 | },
28 | });
29 | }
30 |
31 | if (Meteor.isClient) {
32 | // fixture data
33 | const username = "username";
34 | const password = "password";
35 |
36 | // common test actions
37 | async function login() {
38 | await new Promise((resolve, reject) => {
39 | Meteor.loginWithPassword(username, password, (error) => {
40 | if (error) reject(error);
41 | else resolve();
42 | });
43 | });
44 | }
45 | async function logout() {
46 | await new Promise((resolve, reject) => {
47 | Meteor.logout((error) => {
48 | if (error) reject(error);
49 | else resolve();
50 | });
51 | });
52 | }
53 |
54 | // common test arrangements
55 | async function beforeEach() {
56 | // reset DB; must complete before creation to avoid potential overlap
57 | await Meteor.callAsync("reset");
58 | // prepare sample user
59 | await Accounts.createUserAsync({ username, password });
60 | // logout since `createUser` auto-logs-in
61 | await logout();
62 | }
63 |
64 | // NOTE: each test body has three blocks: Arrange, Act, Assert.
65 |
66 | Tinytest.addAsync(
67 | "Hooks - useUserId - has initial value of `null`",
68 | async function (test, onComplete) {
69 | await beforeEach();
70 |
71 | const { result } = renderHook(() => useUserId());
72 |
73 | test.isNull(result.current);
74 | onComplete();
75 | }
76 | );
77 |
78 | Tinytest.addAsync(
79 | "Hooks - useUserId - is reactive to login",
80 | async function (test, onComplete) {
81 | await beforeEach();
82 |
83 | const { result, waitForNextUpdate } = renderHook(() => useUserId());
84 | // use `waitFor*` instead of `await`; mimics consumer usage
85 | login();
86 | await waitForNextUpdate();
87 |
88 | test.isNotNull(result.current);
89 | onComplete();
90 | }
91 | );
92 |
93 | Tinytest.addAsync(
94 | "Hooks - useUserId - is reactive to logout",
95 | async function (test, onComplete) {
96 | await beforeEach();
97 | await login();
98 |
99 | const { result, waitForNextUpdate } = renderHook(() => useUserId());
100 | // use `waitFor*` instead of `await`; mimics consumer usage
101 | logout();
102 | await waitForNextUpdate();
103 |
104 | test.isNull(result.current);
105 | onComplete();
106 | }
107 | );
108 |
109 | Tinytest.addAsync(
110 | "Hooks - useLoggingIn - has initial value of `false`",
111 | async function (test, onComplete) {
112 | await beforeEach();
113 |
114 | const { result } = renderHook(() => useLoggingIn());
115 |
116 | test.isFalse(result.current);
117 | onComplete();
118 | }
119 | );
120 |
121 | Tinytest.addAsync(
122 | "Hooks - useLoggingIn - is reactive to login starting",
123 | async function (test, onComplete) {
124 | await beforeEach();
125 |
126 | const { result, waitForNextUpdate } = renderHook(() => useLoggingIn());
127 | login();
128 | // first update will be while login strategy is in progress
129 | await waitForNextUpdate();
130 |
131 | test.isTrue(result.current);
132 | onComplete();
133 | }
134 | );
135 |
136 | Tinytest.addAsync(
137 | "Hooks - useLoggingIn - is reactive to login finishing",
138 | async function (test, onComplete) {
139 | await beforeEach();
140 |
141 | const { result, waitForNextUpdate } = renderHook(() => useLoggingIn());
142 | login();
143 | await waitForNextUpdate();
144 | // second update will be after login strategy finishes
145 | await waitForNextUpdate();
146 |
147 | test.isFalse(result.current);
148 | onComplete();
149 | }
150 | );
151 |
152 | Tinytest.addAsync(
153 | "Hooks - useLoggingOut - has initial value of `false`",
154 | async function (test, onComplete) {
155 | await beforeEach();
156 |
157 | const { result } = renderHook(() => useLoggingOut());
158 |
159 | test.isFalse(result.current);
160 | onComplete();
161 | }
162 | );
163 |
164 | Tinytest.addAsync(
165 | "Hooks - useLoggingOut - is reactive to logout starting",
166 | async function (test, onComplete) {
167 | await beforeEach();
168 |
169 | const { result, waitForNextUpdate } = renderHook(() => useLoggingOut());
170 | logout();
171 | // first update will be while logout is in progress
172 | await waitForNextUpdate();
173 |
174 | test.isTrue(result.current);
175 | onComplete();
176 | }
177 | );
178 |
179 | Tinytest.addAsync(
180 | "Hooks - useLoggingOut - is reactive to logout finishing",
181 | async function (test, onComplete) {
182 | await beforeEach();
183 |
184 | const { result, waitForNextUpdate } = renderHook(() => useLoggingOut());
185 | logout();
186 | await waitForNextUpdate();
187 | // second update will be after logout finishes
188 | await waitForNextUpdate();
189 |
190 | test.isFalse(result.current);
191 | onComplete();
192 | }
193 | );
194 |
195 | Tinytest.addAsync(
196 | "Hooks - useUser - has initial value of `null`",
197 | async function (test, onComplete) {
198 | await beforeEach();
199 |
200 | const { result } = renderHook(() => useUser());
201 |
202 | test.isNull(result.current);
203 | onComplete();
204 | }
205 | );
206 |
207 | Tinytest.addAsync(
208 | "Hooks - useUser - is reactive to login",
209 | async function (test, onComplete) {
210 | await beforeEach();
211 |
212 | const { result, waitForNextUpdate } = renderHook(() => useUser());
213 | // use `waitFor*` instead of `await`; mimics consumer usage
214 | login();
215 | await waitForNextUpdate();
216 |
217 | test.isNotNull(result.current);
218 | test.equal(
219 | result.current.username,
220 | username,
221 | "Expected username to match"
222 | );
223 | onComplete();
224 | }
225 | );
226 |
227 | Tinytest.addAsync(
228 | "Hooks - useUser - is reactive to logout",
229 | async function (test, onComplete) {
230 | await beforeEach();
231 | await login();
232 |
233 | const { result, waitForNextUpdate } = renderHook(() => useUser());
234 | // use `waitFor*` instead of `await`; mimics consumer usage
235 | logout();
236 | await waitForNextUpdate();
237 |
238 | test.isNull(result.current);
239 | onComplete();
240 | }
241 | );
242 |
243 | // Since the HOCs wrap with hooks, the logic is already tested in 'Hooks' tests, and we only really need to test for prop forwarding. However, doing so for the "non-initial" case of all these values seems more prudent than just checking the default of `null` or `false`.
244 |
245 | // :NOTE: these tests can be flaky (like 1 in 5 runs).
246 |
247 | Tinytest.addAsync(
248 | "HOCs - withUserId - forwards reactive value",
249 | async function (test, onComplete) {
250 | await beforeEach();
251 | function Foo({ userId }: WithUserIdProps) {
252 | // need something we can easily find; we don't know the id
253 | return {Boolean(userId).toString()};
254 | }
255 | const FooWithUserId = withUserId(Foo);
256 | const { findByText } = render();
257 |
258 | login();
259 |
260 | await waitFor(() => findByText("true"));
261 | cleanup();
262 | onComplete();
263 | }
264 | );
265 |
266 | // :TODO: this is flaky, fails ~1 in 10
267 | Tinytest.addAsync(
268 | "HOCs - withUser - forwards reactive value",
269 | async function (test, onComplete) {
270 | await beforeEach();
271 | function Foo({ user }: WithUserProps) {
272 | return {user?.username || String(user)};
273 | }
274 | const FooWithUser = withUser(Foo);
275 | const { findByText } = render();
276 |
277 | login();
278 |
279 | await waitFor(() => findByText(username));
280 | cleanup();
281 | onComplete();
282 | }
283 | );
284 |
285 | Tinytest.addAsync(
286 | "HOCs - withLoggingIn - forwards reactive value",
287 | async function (test, onComplete) {
288 | await beforeEach();
289 | function Foo({ loggingIn }: WithLoggingInProps) {
290 | return {loggingIn.toString()};
291 | }
292 | const FooWithLoggingIn = withLoggingIn(Foo);
293 | const { findByText } = render();
294 |
295 | login();
296 |
297 | await waitFor(() => findByText("true"));
298 | cleanup();
299 | onComplete();
300 | }
301 | );
302 |
303 | // :TODO: this is flaky, fails ~1 in 5
304 | Tinytest.addAsync(
305 | "HOCs - withLoggingOut - forwards reactive value",
306 | async function (test, onComplete) {
307 | await beforeEach();
308 | function Foo({ loggingOut }: WithLoggingOutProps) {
309 | return {loggingOut.toString()};
310 | }
311 | const FooWithLoggingOut = withLoggingOut(Foo);
312 | const { findByText } = render();
313 | await login();
314 |
315 | logout();
316 |
317 | await waitFor(() => findByText("true"));
318 | cleanup();
319 | onComplete();
320 | }
321 | );
322 | }
323 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/react-accounts.tsx:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import { Tracker } from 'meteor/tracker'
3 | import React, { useState, useEffect, forwardRef } from 'react'
4 |
5 | /**
6 | * Hook to get a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
7 | * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
8 | */
9 | export function useUserId() {
10 | const [userId, setUserId] = useState(Meteor.userId())
11 | useEffect(() => {
12 | const computation = Tracker.autorun(() => {
13 | setUserId(Meteor.userId())
14 | })
15 | return () => {
16 | computation.stop()
17 | }
18 | }, [])
19 | return userId
20 | }
21 |
22 | export interface WithUserIdProps {
23 | userId: string | null;
24 | }
25 |
26 | /**
27 | * HOC to forward a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
28 | * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
29 | */
30 | export function withUserId(Component: React.ComponentType
) {
31 | return forwardRef(
32 | // Use `Omit` so instantiation doesn't require the prop. Union with `Partial` because prop should be optionally overridable / the wrapped component will be prepared for it anyways.
33 | (props: Omit
& Partial, ref) => {
34 | const userId = useUserId();
35 | return (
36 |
42 | );
43 | }
44 | );
45 | }
46 |
47 | /**
48 | * Hook to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
49 | * @see https://docs.meteor.com/api/accounts.html#Meteor-user
50 | */
51 | export function useUser() {
52 | const [user, setUser] = useState(Meteor.user());
53 | useEffect(() => {
54 | const computation = Tracker.autorun(() => {
55 | let user = Meteor.user();
56 | // `Meteor.user` returns `undefined` after logout, but that ruins type signature and test parity. So, cast until that's fixed.
57 | if (user === undefined) {
58 | user = null;
59 | }
60 | setUser(user);
61 | });
62 | return () => {
63 | computation.stop();
64 | };
65 | }, []);
66 | return user;
67 | }
68 |
69 | export interface WithUserProps {
70 | user: Meteor.User | null;
71 | }
72 |
73 | /**
74 | * HOC to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
75 | * @see https://docs.meteor.com/api/accounts.html#Meteor-user
76 | */
77 | export function withUser(Component: React.ComponentType
) {
78 | return forwardRef(
79 | (props: Omit
& Partial, ref) => {
80 | const user = useUser();
81 | return ;
82 | }
83 | );
84 | }
85 |
86 | /**
87 | * Hook to get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
88 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
89 | */
90 | export function useLoggingIn(): boolean {
91 | const [loggingIn, setLoggingIn] = useState(Meteor.loggingIn());
92 | useEffect(() => {
93 | const computation = Tracker.autorun(() => {
94 | setLoggingIn(Meteor.loggingIn());
95 | });
96 | return () => {
97 | computation.stop();
98 | };
99 | }, []);
100 | return loggingIn;
101 | }
102 |
103 | export interface WithLoggingInProps {
104 | loggingIn: boolean;
105 | }
106 |
107 | /**
108 | * HOC to forward a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
109 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
110 | */
111 | export function withLoggingIn(Component: React.ComponentType
) {
112 | return forwardRef(
113 | (
114 | props: Omit
& Partial,
115 | ref
116 | ) => {
117 | const loggingIn = useLoggingIn();
118 | return (
119 |
120 | );
121 | }
122 | );
123 | }
124 |
125 | /**
126 | * Hook to get a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
127 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut
128 | */
129 | export function useLoggingOut(): boolean {
130 | const [loggingOut, setLoggingOut] = useState(Meteor.loggingOut());
131 | useEffect(() => {
132 | const computation = Tracker.autorun(() => {
133 | setLoggingOut(Meteor.loggingOut());
134 | });
135 | return () => {
136 | computation.stop();
137 | };
138 | }, []);
139 | return loggingOut;
140 | }
141 |
142 | export interface WithLoggingOutProps {
143 | loggingOut: boolean;
144 | }
145 |
146 | /**
147 | * HOC to forward a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
148 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut
149 | */
150 | export function withLoggingOut(Component: React.ComponentType
) {
151 | return forwardRef(
152 | (
153 | props: Omit
& Partial,
154 | ref
155 | ) => {
156 | const loggingOut = useLoggingOut();
157 | return (
158 |
159 | );
160 | }
161 | );
162 | }
163 |
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/recommended/tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "preserve"
5 | }
6 | }
--------------------------------------------------------------------------------
/packages/react-meteor-accounts/types/react-accounts.d.ts:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | declare module 'meteor/meteor' {
4 | module Meteor {
5 | function loggingOut(): boolean;
6 | }
7 | }
8 | /**
9 | * Hook to get a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
10 | * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
11 | */
12 | export declare function useUserId(): string | null;
13 | export interface WithUserIdProps {
14 | userId: string | null;
15 | }
16 | /**
17 | * HOC to forward a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
18 | * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
19 | */
20 | export declare function withUserId(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
21 | /**
22 | * Hook to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
23 | * @see https://docs.meteor.com/api/accounts.html#Meteor-user
24 | */
25 | export declare function useUser(): Meteor.User | null;
26 | export interface WithUserProps {
27 | user: Meteor.User | null;
28 | }
29 | /**
30 | * HOC to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
31 | * @see https://docs.meteor.com/api/accounts.html#Meteor-user
32 | */
33 | export declare function withUser(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
34 | /**
35 | * Hook to get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
36 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
37 | */
38 | export declare function useLoggingIn(): boolean;
39 | export interface WithLoggingInProps {
40 | loggingIn: boolean;
41 | }
42 | /**
43 | * HOC to forward a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
44 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
45 | */
46 | export declare function withLoggingIn(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
47 | /**
48 | * Hook to get a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
49 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut
50 | */
51 | export declare function useLoggingOut(): boolean;
52 | export interface WithLoggingOutProps {
53 | loggingOut: boolean;
54 | }
55 | /**
56 | * HOC to forward a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
57 | * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingOut
58 | */
59 | export declare function withLoggingOut(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
60 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/.meteorignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package.json
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/.npm/package/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/.npm/package/README:
--------------------------------------------------------------------------------
1 | This directory and the files immediately inside it are automatically generated
2 | when you change this package's NPM dependencies. Commit the files in this
3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
4 | so that others run the same versions of sub-dependencies.
5 |
6 | You should NOT check in the node_modules directory that Meteor automatically
7 | creates; if you are using git, the .gitignore file tells git to ignore it.
8 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/.npm/package/npm-shrinkwrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 4,
3 | "dependencies": {
4 | "lodash.isequal": {
5 | "version": "4.5.0",
6 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
7 | "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
8 | },
9 | "lodash.remove": {
10 | "version": "4.7.0",
11 | "resolved": "https://registry.npmjs.org/lodash.remove/-/lodash.remove-4.7.0.tgz",
12 | "integrity": "sha512-GnwkSsEXGXirSxh3YI+jc/qvptE2DV8ZjA4liK0NT1MJ3mNDMFhX3bY+4Wr8onlNItYuPp7/4u19Fi55mvzkTw=="
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/.versions:
--------------------------------------------------------------------------------
1 | allow-deny@2.1.0
2 | babel-compiler@7.11.3
3 | babel-runtime@1.5.2
4 | base64@1.0.13
5 | binary-heap@1.0.12
6 | blaze@3.0.0
7 | boilerplate-generator@2.0.0
8 | callback-hook@1.6.0
9 | check@1.4.4
10 | core-runtime@1.0.0
11 | ddp@1.4.2
12 | ddp-client@3.1.0
13 | ddp-common@1.4.4
14 | ddp-server@3.1.0
15 | diff-sequence@1.1.3
16 | dynamic-import@0.7.4
17 | ecmascript@0.16.10
18 | ecmascript-runtime@0.8.3
19 | ecmascript-runtime-client@0.12.2
20 | ecmascript-runtime-server@0.11.1
21 | ejson@1.1.4
22 | facts-base@1.0.2
23 | fetch@0.1.5
24 | geojson-utils@1.0.12
25 | htmljs@2.0.1
26 | id-map@1.2.0
27 | inter-process-messaging@0.1.2
28 | jquery@3.0.2
29 | local-test:react-meteor-data@3.0.4-beta.0
30 | logging@1.3.5
31 | meteor@2.1.0
32 | minimongo@2.0.2
33 | modern-browsers@0.2.0
34 | modules@0.20.3
35 | modules-runtime@0.13.2
36 | mongo@2.1.0
37 | mongo-decimal@0.2.0
38 | mongo-dev-server@1.1.1
39 | mongo-id@1.0.9
40 | npm-mongo@6.10.2
41 | observe-sequence@2.0.0
42 | ordered-dict@1.2.0
43 | promise@1.0.0
44 | random@1.2.2
45 | react-fast-refresh@0.2.9
46 | react-meteor-data@3.0.4-beta.0
47 | reactive-dict@1.3.2
48 | reactive-var@1.0.13
49 | reload@1.3.2
50 | retry@1.1.1
51 | routepolicy@1.1.2
52 | socket-stream-client@0.6.0
53 | test-helpers@2.0.2
54 | tinytest@1.3.1
55 | tracker@1.3.4
56 | typescript@5.6.3
57 | underscore@1.6.4
58 | webapp@2.0.5
59 | webapp-hashing@1.1.2
60 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## v3.0.3, 2024-12-30
4 | * Add `useSubscribeSuspenseServer` hook to be used in SSR.
5 |
6 | ## v3.0.1, 2024-07-18
7 | * Replace Meteor dependency version from 3.0-rc.0 to 3.0
8 |
9 | ## v3.0.0, 2024-07-12
10 | * Official bumped package version to be compatible with meteor v3
11 |
12 | ## v3.0.0-alpha.6, 2023-05-11
13 | * Bumped package version to be compatible with meteor v3
14 |
15 | ## v2.7.2, 2023-04-20
16 | * Updated the `suspense/useFind` hook to be isomorphic.
17 | * Updated the `suspense/useFind` types to match its implementation. [PR](https://github.com/meteor/react-packages/pull/390).
18 |
19 | ## v2.7.1, 2023-03-16
20 | * Added missing dependencies for the suspense hooks.
21 |
22 | ## v2.7.0, 2023-03-16
23 | * Added suspendable hooks:
24 | - `suspense/useFind`: A Suspense version of `useFind`. Is intended to be used along with SSR.
25 | - `suspense/useSubscribe`: A Suspense version of `useSubscribe`, moving the isLoading checking state to Suspense
26 | - `suspense/useTracker`: A Suspense version of `useTracker`, that accepts async function and maintains the reactivity of the data.
27 | more can be seen in the docs.
28 |
29 |
30 | ## v2.6.3, 2023-02-10
31 | * Removed assets so that zodern:types can work properly. More on how to use core types can be seen [here](https://docs.meteor.com/using-core-types.html). [PR](https://github.com/meteor/react-packages/pull/377).
32 |
33 | ## v2.6.2, 2023-02-02
34 |
35 | * Stop the computation immediately to avoid creating side effects in render. refers to this issues:
36 | [#382](https://github.com/meteor/react-packages/issues/382)
37 | [#381](https://github.com/meteor/react-packages/issues/381)
38 |
39 | ## v2.6.1, 2023-01-04
40 | * Added types to the package via zodern:types. More on how to use core types can be seen [here](https://docs.meteor.com/using-core-types.html). [PR](https://github.com/meteor/react-packages/pull/377).
41 |
42 |
43 | ## v2.6.0, 2022-11-28
44 | * fix useFind can accept () => null as argument. Previously it returned null in this scenario, so changed the return statement because it was returning an empty array. [PR](https://github.com/meteor/react-packages/pull/374).
45 | * fix: named exports for useTracker and withTracker. Now it is has is standardized. [PR](https://github.com/meteor/react-packages/pull/376).
46 |
47 |
48 | ## v2.5.3, 2022-11-08
49 | * useFind Data returned by useFind wasn't updated if the cursor was changed. It happened because the initial fetch was called only once. [PR](https://github.com/meteor/react-packages/pull/370).
50 |
51 | ## v2.5.2, 2022-10-27
52 | * useFind now works with async code by removes the fetch function call and adds a data initializer made with cursor observing.[PR](https://github.com/meteor/react-packages/pull/366)
53 |
54 | ## v2.5.1, 2022-05-18
55 | * Fix useFind in SSR: check for server Cursor. [PR](https://github.com/meteor/react-packages/pull/350).
56 |
57 | ## v2.5.0, 2022-05-02
58 | * Fix useTrackerNoDeps for React 18. [PR](https://github.com/meteor/react-packages/pull/359).
59 |
60 | ## v2.4.0, 2021-12-02
61 | * Added `useSubscribe` and `useFind` hooks
62 |
63 | ## v2.3.3, 2021-07-14
64 | * Fixes a publication issue in v2.3.2
65 |
66 | ## v2.3.2, 2021-07-12
67 | * Updated dev dependencies
68 | * Add version constraint to take package versions from Meteor 2.3+
69 |
70 | ## v2.3.1, 2021-05-10
71 | * Adds a skipUpdate comparator option to both useTracker (with and without deps) and withTracker.
72 | * Fixes a bug which would sometimes cause the value to get lost (specifically, when a re-render is invoked by an immediate, in-render state change).
73 |
74 | ## v2.2.2, 2021-01-28
75 | * Fix lost reactivity when using deps. https://github.com/meteor/react-packages/pull/314
76 |
77 | ## v2.2.1, 2021-01-20
78 | * Fix warning that was produced when useTracker was used without any deps. https://github.com/meteor/react-packages/pull/312
79 |
80 | ## v2.2.0, 2021-01-20
81 | * Fix issue with useTracker and Subscriptions when using deps https://github.com/meteor/react-packages/pull/306
82 | * Remove version constraint on core TypeScript package https://github.com/meteor/react-packages/pull/308
83 |
84 | ## v2.1.1, 2020-05-21
85 | * Make pure default to true like it used to https://github.com/meteor/react-packages/issues/287
86 |
87 | ## v2.1.0, 2020-04-22
88 | * Update and fix tests.
89 | * Convert to TypeScript.
90 | * Separate deps and no-deps implementation for easier to read implementation.
91 | * Fix a problem in StrictMode when using no-deps (and withTracker) where
92 | updates get lost after first render.
93 | https://github.com/meteor/react-packages/issues/278
94 |
95 | ## v2.0.1, 2019-12-13
96 |
97 | * Makes main module lazy. Fixes [Issue #264](https://github.com/meteor/react-packages/issues/264). Thanks [@moberegger](https://github.com/moberegger)
98 |
99 | ## v2.0.0, 2019-11-19
100 |
101 | * Adds React Hooks support (`useTracker`)
102 |
103 | ## v1.0.0, 2019-12-13
104 |
105 | * Renames deprecated lifecycle to support React 16.9
106 | * Publishes branch v1.
107 | - This branch is not synced with devel as it requires at least version 16.8 of react.
108 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/README.md:
--------------------------------------------------------------------------------
1 | # react-meteor-data
2 |
3 | This package provides an integration between React and [`Tracker`](https://atmospherejs.com/meteor/tracker), Meteor's reactive data system.
4 |
5 | ## Table of Contents
6 |
7 | - [Install](#install)
8 | - [Changelog](#changelog)
9 | - [Usage](#usage)
10 | - [`useTracker`](#usetrackerreactivefn-basic-hook)
11 | - [With dependencies](#usetrackerreactivefn-deps-hook-with-deps)
12 | - [Skipping updates](#usetrackerreactivefn-deps-skipUpdate-or-usetrackerreactivefn-skipUpdate)
13 | - [`withTracker`](#withtrackerreactivefn-higher-order-component)
14 | - [Advanced container config](#withtracker-reactivefn-pure-skipupdate--advanced-container-config)
15 | - [`useSubscribe`](#usesubscribesubname-args-a-convenient-wrapper-for-subscriptions)
16 | - [`useFind`](#usefindcursorfactory-deps-accelerate-your-lists)
17 | - [Concurrent Mode, Suspense, and Error Boundaries](#concurrent-mode-suspense-and-error-boundaries)
18 | - [Suspendable version of hooks](#suspendable-hooks)
19 | - [useTracker](#suspense/useTracker)
20 | - [useSubscribe](#suspense/useSubscribe)
21 | - [useFind](#suspense/useFind)
22 | - [Version compatibility notes](#version-compatibility-notes)
23 |
24 | ## Install
25 |
26 | To install the package, use `meteor add`:
27 |
28 | ```bash
29 | meteor add react-meteor-data
30 | ```
31 |
32 | You'll also need to install `react` if you have not already:
33 |
34 | ```bash
35 | meteor npm install react
36 | ```
37 |
38 | ### Changelog
39 |
40 | [check recent changes here](./CHANGELOG.md)
41 |
42 | ## Usage
43 |
44 | This package provides two ways to use Tracker reactive data in your React components:
45 |
46 | - a hook: `useTracker` (v2 only, requires React `^16.8`)
47 | - a higher-order component (HOC): `withTracker` (v1 and v2).
48 |
49 | The `useTracker` hook, introduced in version 2.0.0, embraces the [benefits of hooks](https://reactjs.org/docs/hooks-faq.html). Like all React hooks, it can only be used in function components, not in class components.
50 |
51 | The `withTracker` HOC can be used with all components, function or class based.
52 |
53 | It is not necessary to rewrite existing applications to use the `useTracker` hook instead of the existing `withTracker` HOC.
54 |
55 | ### `useTracker(reactiveFn)` basic hook
56 |
57 | You can use the `useTracker` hook to get the value of a Tracker reactive function in your React "function components." The reactive function will get re-run whenever its reactive inputs change, and the component will re-render with the new value.
58 |
59 | `useTracker` manages its own state, and causes re-renders when necessary. There is no need to call React state setters from inside your `reactiveFn`. Instead, return the values from your `reactiveFn` and assign those to variables directly. When the `reactiveFn` updates, the variables will be updated, and the React component will re-render.
60 |
61 | Arguments:
62 |
63 | - `reactiveFn`: A Tracker reactive function (receives the current computation).
64 |
65 | The basic way to use `useTracker` is to simply pass it a reactive function, with no further fuss. This is the preferred configuration in many cases.
66 |
67 | #### `useTracker(reactiveFn, deps)` hook with deps
68 |
69 | You can pass an optional deps array as a second value. When provided, the computation will be retained, and reactive updates after the first run will run asynchronously from the react render execution frame. This array typically includes all variables from the outer scope "captured" in the closure passed as the 1st argument. For example, the value of a prop used in a subscription or a minimongo query; see example below.
70 |
71 | This should be considered a low level optimization step for cases where your computations are somewhat long running - like a complex minimongo query. In many cases it's safe and even preferred to omit deps and allow the computation to run synchronously with render.
72 |
73 | Arguments:
74 |
75 | - `reactiveFn`
76 | - `deps`: An optional array of "dependencies" of the reactive function. This is very similar to how the `deps` argument for [React's built-in `useEffect`, `useCallback` or `useMemo` hooks](https://reactjs.org/docs/hooks-reference.html) work.
77 |
78 | ```js
79 | import { useTracker } from 'meteor/react-meteor-data';
80 |
81 | // React function component.
82 | function Foo({ listId }) {
83 | // This computation uses no value from the outer scope,
84 | // and thus does not needs to pass a 'deps' argument.
85 | // However, we can optimize the use of the computation
86 | // by providing an empty deps array. With it, the
87 | // computation will be retained instead of torn down and
88 | // rebuilt on every render. useTracker will produce the
89 | // same results either way.
90 | const currentUser = useTracker(() => Meteor.user(), []);
91 |
92 | // The following two computations both depend on the
93 | // listId prop. When deps are specified, the computation
94 | // will be retained.
95 | const listLoading = useTracker(() => {
96 | // Note that this subscription will get cleaned up
97 | // when your component is unmounted or deps change.
98 | const handle = Meteor.subscribe('todoList', listId);
99 | return !handle.ready();
100 | }, [listId]);
101 | const tasks = useTracker(() => Tasks.find({ listId }).fetch(), [listId]);
102 |
103 | return (
104 | Hello {currentUser.username}
105 | {listLoading ? (
106 | Loading
107 | ) : (
108 |
109 | Here is the Todo list {listId}:
110 |
111 | {tasks.map(task => (
112 | - {task.label}
113 | ))}
114 |
115 |
116 | )}
117 | );
118 | }
119 | ```
120 |
121 | **Note:** the [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) package provides ESLint hints to help detect missing values in the `deps` argument of React built-in hooks. It can be configured to also validate the `deps` argument of the `useTracker` hook or some other hooks, with the following `eslintrc` config:
122 |
123 | ```json
124 | "react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useTracker|useSomeOtherHook|..." }]
125 | ```
126 |
127 | #### `useTracker(reactiveFn, deps, skipUpdate)` or `useTracker(reactiveFn, skipUpdate)`
128 |
129 | You may optionally pass a function as a second or third argument. The `skipUpdate` function can evaluate the return value of `reactiveFn` for changes, and control re-renders in sensitive cases. *Note:* This is not meant to be used with a deep compare (even fast-deep-equals), as in many cases that may actually lead to worse performance than allowing React to do it's thing. But as an example, you could use this to compare an `updatedAt` field between updates, or a subset of specific fields, if you aren't using the entire document in a subscription. As always with any optimization, measure first, then optimize second. Make sure you really need this before implementing it.
130 |
131 | Arguments:
132 |
133 | - `reactiveFn`
134 | - `deps?` - optional - you may omit this, or pass a "falsy" value.
135 | - `skipUpdate` - A function which receives two arguments: `(prev, next) => (prev === next)`. `prev` and `next` will match the type or data shape as that returned by `reactiveFn`. Note: A return value of `true` means the update will be "skipped". `false` means re-render will occur as normal. So the function should be looking for equivalence.
136 |
137 | ```jsx
138 | import { useTracker } from 'meteor/react-meteor-data';
139 |
140 | // React function component.
141 | function Foo({ listId }) {
142 | const tasks = useTracker(
143 | () => Tasks.find({ listId }).fetch(), [listId],
144 | (prev, next) => {
145 | // prev and next will match the type returned by the reactiveFn
146 | return prev.every((doc, i) => (
147 | doc._id === next[i] && doc.updatedAt === next[i]
148 | )) && prev.length === next.length;
149 | }
150 | );
151 |
152 | return (
153 | Hello {currentUser.username}
154 |
155 | Here is the Todo list {listId}:
156 |
157 | {tasks.map(task => (
158 | - {task.label}
159 | ))}
160 |
161 |
162 | );
163 | }
164 | ```
165 |
166 | ### `withTracker(reactiveFn)` higher-order component
167 |
168 | You can use the `withTracker` HOC to wrap your components and pass them additional props values from a Tracker reactive function. The reactive function will get re-run whenever its reactive inputs change, and the wrapped component will re-render with the new values for the additional props.
169 |
170 | Arguments:
171 |
172 | - `reactiveFn`: a Tracker reactive function, getting the props as a parameter, and returning an object of additional props to pass to the wrapped component.
173 |
174 | ```js
175 | import { withTracker } from 'meteor/react-meteor-data';
176 |
177 | // React component (function or class).
178 | function Foo({ listId, currentUser, listLoading, tasks }) {
179 | return (
180 | Hello {currentUser.username}
181 | {listLoading ?
182 | Loading
:
183 |
184 | Here is the Todo list {listId}:
185 |
{tasks.map(task => - {task.label}
)}
186 |
{
191 | // Do all your reactive data access in this function.
192 | // Note that this subscription will get cleaned up when your component is unmounted
193 | const handle = Meteor.subscribe('todoList', listId);
194 |
195 | return {
196 | currentUser: Meteor.user(),
197 | listLoading: !handle.ready(),
198 | tasks: Tasks.find({ listId }).fetch(),
199 | };
200 | })(Foo);
201 | ```
202 |
203 | The returned component will, when rendered, render `Foo` (the "lower-order" component) with its provided props in addition to the result of the reactive function. So `Foo` will receive `{ listId }` (provided by its parent) as well as `{ currentUser, listLoading, tasks }` (added by the `withTracker` HOC).
204 |
205 | For more information, see the [React article](http://guide.meteor.com/react.html) in the Meteor Guide.
206 |
207 | #### `withTracker({ reactiveFn, pure, skipUpdate })` advanced container config
208 |
209 | The `withTracker` HOC can receive a config object instead of a simple reactive function.
210 |
211 | - `getMeteorData` - The `reactiveFn`.
212 | - `pure` - `true` by default. Causes the resulting Container to be wrapped with React's `memo()`.
213 | - `skipUpdate` - A function which receives two arguments: `(prev, next) => (prev === next)`. `prev` and `next` will match the type or data shape as that returned by `reactiveFn`. Note: A return value of `true` means the update will be "skipped". `false` means re-render will occur as normal. So the function should be looking for equivalence.
214 |
215 | ```js
216 | import { withTracker } from 'meteor/react-meteor-data';
217 |
218 | // React component (function or class).
219 | function Foo({ listId, currentUser, listLoading, tasks }) {
220 | return (
221 | Hello {currentUser.username}
222 | {listLoading ?
223 | Loading
:
224 |
225 | Here is the Todo list {listId}:
226 |
{tasks.map(task => - {task.label}
)}
227 |
(
252 | doc._id === next[i] && doc.updatedAt === next[i]
253 | ))
254 | && prev.tasks.length === next.tasks.length
255 | );
256 | }
257 | })(Foo);
258 | ```
259 |
260 | ### `useSubscribe(subName, ...args)` A convenient wrapper for subscriptions
261 |
262 | `useSubscribe` is a convenient short hand for setting up a subscription. It is particularly useful when working with `useFind`, which should NOT be used for setting up subscriptions. At its core, it is a very simple wrapper around `useTracker` (with no deps) to create the subscription in a safe way, and allows you to avoid some of the ceremony around defining a factory and defining deps. Just pass the name of your subscription, and your arguments.
263 |
264 | `useSubscribe` returns an `isLoading` function. You can call `isLoading()` to react to changes in the subscription's loading state. The `isLoading` function will both return the loading state of the subscription, and set up a reactivity for the loading state change. If you don't call this function, no re-render will occur when the loading state changes.
265 |
266 | ```jsx
267 | // Note: isLoading is a function!
268 | const isLoading = useSubscribe('posts', groupId);
269 | const posts = useFind(() => Posts.find({ groupId }), [groupId]);
270 |
271 | if (isLoading()) {
272 | return
273 | } else {
274 | return
275 | {posts.map(post => - {post.title}
)}
276 |
277 | }
278 | ```
279 |
280 | If you want to conditionally subscribe, you can set the `name` field (the first argument) to a falsy value to bypass the subscription.
281 |
282 | ```jsx
283 | const needsData = false;
284 | const isLoading = useSubscribe(needsData ? "my-pub" : null);
285 |
286 | // When a subscription is not used, isLoading() will always return false
287 | ```
288 |
289 | ### `useFind(cursorFactory, deps)` Accelerate your lists
290 |
291 | The `useFind` hook can substantially speed up the rendering (and rerendering) of lists coming from mongo queries (subscriptions). It does this by controlling document object references. By providing a highly tailored cursor management within the hook, using the `Cursor.observe` API, `useFind` carefully updates only the object references changed during a DDP update. This approach allows a tighter use of core React tools and philosophies to turbo charge your list renders. It is a very different approach from the more general purpose `useTracker`, and it requires a bit more set up. A notable difference is that you should NOT call `.fetch()`. `useFind` requires its factory to return a `Mongo.Cursor` object. You may also return `null`, if you want to conditionally set up the Cursor.
292 |
293 | Here is an example in code:
294 |
295 | ```jsx
296 | import React, { memo } from 'react'
297 | import { useFind } from 'meteor/react-meteor-data'
298 | import TestDocs from '/imports/api/collections/TestDocs'
299 |
300 | // Memoize the list item
301 | const ListItem = memo(({doc}) => {
302 | return (
303 | {doc.id},{doc.updated}
304 | )
305 | })
306 |
307 | const Test = () => {
308 | const docs = useFind(() => TestDocs.find(), [])
309 | return (
310 |
311 | {docs.map(doc =>
312 |
313 | )}
314 |
315 | )
316 | }
317 |
318 | // Later on, update a single document - notice only that single component is updated in the DOM
319 | TestDocs.update({ id: 2 }, { $inc: { someProp: 1 } })
320 | ```
321 |
322 | If you want to conditionally call the find method based on some props configuration or anything else, return `null` from the factory.
323 |
324 | ```jsx
325 | const docs = useFind(() => {
326 | if (props.skip) {
327 | return null
328 | }
329 | return TestDocs.find()
330 | }, [])
331 | ```
332 |
333 | ### Concurrent Mode, Suspense and Error Boundaries
334 |
335 | There are some additional considerations to keep in mind when using Concurrent Mode, Suspense and Error Boundaries, as
336 | each of these can cause React to cancel and discard (toss) a render, including the result of the first run of your
337 | reactive function. One of the things React developers often stress is that we should not create "side-effects" directly
338 | in the render method or in functional components. There are a number of good reasons for this, including allowing the
339 | React runtime to cancel renders. Limiting the use of side-effects allows features such as concurrent mode, suspense and
340 | error boundaries to work deterministically, without leaking memory or creating rogue processes. Care should be taken to
341 | avoid side effects in your reactive function for these reasons. (Note: this caution does not apply to Meteor specific
342 | side-effects like subscriptions, since those will be automatically cleaned up when `useTracker`'s computation is
343 | disposed.)
344 |
345 | Ideally, side-effects such as creating a Meteor computation would be done in `useEffect`. However, this is problematic
346 | for Meteor, which mixes an initial data query with setting up the computation to watch those data sources all in one
347 | initial run. If we wait to do that in `useEffect`, we'll end up rendering a minimum of 2 times (and using hacks for the
348 | first one) for every component which uses `useTracker` or `withTracker`, or not running at all in the initial render and
349 | still requiring a minimum of 2 renders, and complicating the API.
350 |
351 | To work around this and keep things running fast, we are creating the computation in the render method directly, and
352 | doing a number of checks later in `useEffect` to make sure we keep that computation fresh and everything up to date,
353 | while also making sure to clean things up if we detect the render has been tossed. For the most part, this should all be
354 | transparent.
355 |
356 | The important thing to understand is that your reactive function can be initially called more than once for a single
357 | render, because sometimes the work will be tossed. Additionally, `useTracker` will not call your reactive function
358 | reactively until the render is committed (until `useEffect` runs). If you have a particularly fast changing data source,
359 | this is worth understanding. With this very short possible suspension, there are checks in place to make sure the
360 | eventual result is always up to date with the current state of the reactive function. Once the render is "committed",
361 | and the component mounted, the computation is kept running, and everything will run as expected.
362 |
363 |
364 | ## Suspendable version of hooks
365 |
366 | ### `suspense/useTracker`
367 |
368 | This is a version of `useTracker` that can be used with React Suspense.
369 |
370 | For its first argument, a key is necessary, witch is used to identify the computation and to avoid recreating it when the
371 | component is re-rendered.
372 |
373 | Its second argument is a function that can be async and reactive,
374 | this argument works similar to the original `useTracker` that does not suspend.
375 |
376 | For its _optional_ third argument, the dependency array, works similar to the `useTracker` that does not suspend,
377 | you pass in an array of variables that this tracking function depends upon.
378 |
379 | For its _optional_ fourth argument, the options object, works similar to the `useTracker` that does not suspend,
380 | you pass in a function for when should skip the update.
381 |
382 |
383 | ```jsx
384 | import { useTracker } from 'meteor/react-meteor-data/suspense'
385 | import { useSubscribe } from 'meteor/react-meteor-data/suspense'
386 |
387 | function Tasks() { // this component will suspend
388 | useSubscribe("tasks");
389 | const { username } = useTracker("user", () => Meteor.user()) // Meteor.user() is async meteor 3.0
390 | const tasksByUser = useTracker("tasksByUser", () =>
391 | TasksCollection.find({username}, { sort: { createdAt: -1 } }).fetchAsync() // async call
392 | );
393 |
394 |
395 | // render the tasks
396 | }
397 | ```
398 |
399 |
400 | ### Maintaining the reactive context
401 |
402 | To maintain a reactive context using the new Meteor Async methods, we are using the new `Tracker.withComputation` API to maintain the reactive context of an
403 | async call, this is needed because otherwise it would be only called once, and the computation would never run again,
404 | this way, every time we have a new Link being added, this useTracker is ran.
405 |
406 | ```jsx
407 |
408 | // needs Tracker.withComputation because otherwise it would be only called once, and the computation would never run again
409 | const docs = useTracker('name', async (c) => {
410 | const placeholders = await fetch('https://jsonplaceholder.typicode.com/todos').then(x => x.json());
411 | console.log(placeholders);
412 | return await Tracker.withComputation(c, () => LinksCollection.find().fetchAsync());
413 | });
414 |
415 | ```
416 |
417 | A rule of thumb is that if you are using a reactive function for example `find` + `fetchAsync`, it is nice to wrap it
418 | inside `Tracker.withComputation` to make sure that the computation is kept alive, if you are just calling that function
419 | that is not necessary, like the one bellow, will be always reactive.
420 |
421 | ```jsx
422 |
423 | const docs = useTracker('name', () => LinksCollection.find().fetchAsync());
424 |
425 | ```
426 |
427 | ### `suspense/useSubscribe`
428 |
429 | This is a version of `useSubscribe` that can be used with React Suspense.
430 | It is similar to `useSubscribe`, it throws a promise and suspends the rendering until the promise is resolved.
431 | It does not return a Meteor Handle to control the subscription
432 |
433 | ```jsx
434 |
435 | import { useTracker } from 'meteor/react-meteor-data/suspense'
436 | import { useSubscribe } from 'meteor/react-meteor-data/suspense'
437 |
438 | function Tasks() { // this component will suspend
439 | useSubscribe("tasks");
440 | const { username } = useTracker("user", () => Meteor.user()) // Meteor.user() is async meteor 3.0
441 | const tasksByUser = useTracker("tasksByUser", () =>
442 | TasksCollection.find({username}, { sort: { createdAt: -1 } }).fetchAsync() // async call
443 | );
444 |
445 |
446 | // render the tasks
447 | }
448 |
449 | ```
450 |
451 | ### `suspense/useFind`
452 |
453 | This is a version of `useFind` that can be used with React Suspense.
454 | It has a few differences from the `useFind` without suspense, it throws a promise and suspends the rendering until the promise is resolved.
455 | It returns the result and it is reactive.
456 | You should pass as the first parameter the collection where is being searched upon and as the second parameter an array with the arguments,
457 | the same arguments that you would pass to the `find` method of the collection, third parameter is optional, and it is dependency array object.
458 | It's meant for the SSR, you don't have to use it if you're not interested in SSR.
459 |
460 | ```jsx
461 |
462 | import { useFind } from 'meteor/react-meteor-data/suspense'
463 | import { useSubscribe } from 'meteor/react-meteor-data/suspense'
464 |
465 | function Tasks() { // this component will suspend
466 | useSubscribe("tasks");
467 | const tasksByUser = useFind(
468 | TasksCollection,
469 | [{}, { sort: { createdAt: -1 } }]
470 | );
471 |
472 | // render the tasks
473 | }
474 |
475 | ```
476 |
477 |
478 | ### Version compatibility notes
479 |
480 | - `react-meteor-data` v2.x :
481 | - `useTracker` hook + `withTracker` HOC
482 | - Requires React `^16.8`.
483 | - Implementation is compatible with "React Suspense", concurrent mode and error boundaries.
484 | - The `withTracker` HOC is strictly backwards-compatible with the one provided in v1.x, the major version number is only motivated by the bump of React version requirement. Provided a compatible React version, existing Meteor apps leveraging the `withTracker` HOC can freely upgrade from v1.x to v2.x, and gain compatibility with future React versions.
485 | - The previously deprecated `createContainer` has been removed.
486 |
487 | - `react-meteor-data` v0.x :
488 | - `withTracker` HOC (+ `createContainer`, kept for backwards compatibility with early v0.x releases)
489 | - Requires React `^15.3` or `^16.0`.
490 | - Implementation relies on React lifecycle methods (`componentWillMount` / `componentWillUpdate`) that are [marked for deprecation in future React versions](https://reactjs.org/blog/2018/03/29/react-v-16-3.html#component-lifecycle-changes) ("React Suspense").
491 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/index.ts:
--------------------------------------------------------------------------------
1 | /* global Meteor*/
2 | import React from 'react';
3 |
4 | if (Meteor.isDevelopment) {
5 | const v = React.version.split('.');
6 | if (v[0] < 16 || (v[0] == 16 && v[1] < 8)) {
7 | console.warn('react-meteor-data 2.x requires React version >= 16.8.');
8 | }
9 | }
10 |
11 | export { useTracker } from './useTracker';
12 | export { withTracker } from './withTracker';
13 | export { useFind } from './useFind';
14 | export { useSubscribe } from './useSubscribe';
--------------------------------------------------------------------------------
/packages/react-meteor-data/package-types.json:
--------------------------------------------------------------------------------
1 | {
2 | "typesEntry": "index.ts"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/package.js:
--------------------------------------------------------------------------------
1 | /* global Package */
2 |
3 | Package.describe({
4 | name: 'react-meteor-data',
5 | summary: 'React hook for reactively tracking Meteor data',
6 | version: '3.0.5-beta.0',
7 | documentation: 'README.md',
8 | git: 'https://github.com/meteor/react-packages'
9 | })
10 |
11 | Npm.depends({
12 | 'lodash.isequal': '4.5.0'
13 | })
14 |
15 | Package.onUse((api) => {
16 | api.versionsFrom(['1.8.2', '1.12', '2.0', '2.3', '3.0'])
17 | api.use('tracker')
18 | api.use('ecmascript')
19 | api.use('typescript')
20 | api.use('zodern:types', 'server')
21 |
22 | api.mainModule('index.ts', ['client', 'server'], { lazy: true })
23 | })
24 |
25 | Package.onTest((api) => {
26 | api.use(['ecmascript', 'typescript', 'reactive-dict', 'reactive-var', 'tracker', 'tinytest', 'underscore', 'mongo'])
27 | api.use('test-helpers')
28 | api.use('react-meteor-data')
29 | api.use('jquery@3.0.0', 'client');
30 |
31 | api.mainModule('tests.js');
32 | })
33 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-meteor-data"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/react-meteor-data.d.ts:
--------------------------------------------------------------------------------
1 | import type * as React from 'react'
2 | import { type Mongo } from 'meteor/mongo'
3 |
4 | export function useTracker(
5 | reactiveFn: () => TDataProps
6 | ): TDataProps
7 | export function useTracker(
8 | reactiveFn: () => TDataProps,
9 | deps: React.DependencyList
10 | ): TDataProps
11 | export function useTracker(
12 | getMeteorData: () => TDataProps,
13 | deps: React.DependencyList,
14 | skipUpdate?: (prev: TDataProps, next: TDataProps) => boolean
15 | ): TDataProps
16 | export function useTracker(
17 | getMeteorData: () => TDataProps,
18 | skipUpdate: (prev: TDataProps, next: TDataProps) => boolean
19 | ): TDataProps
20 |
21 | export function withTracker(
22 | reactiveFn: (props: TOwnProps) => TDataProps
23 | ): (
24 | reactComponent: React.ComponentType
25 | ) => React.ComponentClass
26 | export function withTracker(options: {
27 | getMeteorData: (props: TOwnProps) => TDataProps
28 | pure?: boolean | undefined
29 | skipUpdate?: (prev: TDataProps, next: TDataProps) => boolean
30 | }): (
31 | reactComponent: React.ComponentType
32 | ) => React.ComponentClass
33 |
34 | export function useSubscribe(name?: string, ...args: any[]): () => boolean
35 |
36 | export function useFind(
37 | factory: () => Mongo.Cursor,
38 | deps?: React.DependencyList
39 | ): T[]
40 | export function useFind(
41 | factory: () => Mongo.Cursor | undefined | null,
42 | deps?: React.DependencyList
43 | ): T[] | null
44 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | if (Meteor.isDevelopment) {
4 | const v = React.version.split('.')
5 | if (v[0] < 16 || (v[0] == 16 && v[1] < 8)) {
6 | console.warn('react-meteor-data 2.x requires React version >= 16.8.')
7 | }
8 | }
9 |
10 | export { useFind } from './useFind'
11 | export { useSubscribe } from './useSubscribe'
12 | export { useTracker } from './useTracker'
13 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/react-meteor-data.d.ts:
--------------------------------------------------------------------------------
1 | // Suspense
2 | import { type Mongo } from 'meteor/mongo'
3 | import { type EJSON } from 'meteor/ejson'
4 | import { type Meteor } from 'meteor/meteor'
5 | import type * as React from 'react'
6 |
7 | export function useTracker(
8 | key: string,
9 | reactiveFn: () => Promise
10 | ): TDataProps
11 | export function useTracker(
12 | key: string,
13 | reactiveFn: () => Promise,
14 | deps: React.DependencyList
15 | ): TDataProps
16 | export function useTracker(
17 | key: string,
18 | getMeteorData: () => Promise,
19 | deps: React.DependencyList,
20 | skipUpdate?: (prev: Promise, next: Promise) => boolean
21 | ): TDataProps
22 | export function useTracker(
23 | key: string,
24 | getMeteorData: () => Promise,
25 | skipUpdate: (prev: Promise, next: Promise) => boolean
26 | ): TDataProps
27 |
28 | export function useFind(collection: Mongo.Collection, findArgs: Parameters['find']> | null, deps?: React.DependencyList): T[] | null
29 |
30 | export function useSubscribe(name: string, ...params: EJSON[]): Meteor.SubscriptionHandle
31 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/useFind.tests.js:
--------------------------------------------------------------------------------
1 | /* global Meteor, Tinytest */
2 | import React, { Suspense } from 'react';
3 | import { renderToString } from 'react-dom/server';
4 | import { Mongo } from 'meteor/mongo';
5 | import { renderHook } from '@testing-library/react';
6 | import { useFindSuspenseClient, useFindSuspenseServer } from './useFind';
7 |
8 | /**
9 | * Test for useFindSuspenseClient
10 | */
11 | if (Meteor.isClient) {
12 | Tinytest.addAsync(
13 | 'suspense/useFindSuspenseClient - Verify reference stability between rerenders',
14 | async (test) => {
15 | const TestDocs = new Mongo.Collection(null);
16 |
17 | TestDocs.insert({ id: 0, updated: 0 });
18 | TestDocs.insert({ id: 1, updated: 0 });
19 |
20 | const { result, rerender } = renderHook(() =>
21 | useFindSuspenseClient(TestDocs, [{}])
22 | );
23 |
24 | test.equal(
25 | result.current.length,
26 | 2,
27 | '2 items should have rendered, only 2, no more.'
28 | );
29 |
30 | await TestDocs.updateAsync({ id: 1 }, { $inc: { updated: 1 } });
31 |
32 | rerender();
33 |
34 | test.equal(
35 | result.current.length,
36 | 2,
37 | '2 items should have rendered - only 1 of the items should have been matched by the reconciler after a single change.'
38 | );
39 | }
40 | );
41 |
42 | Tinytest.addAsync(
43 | 'suspense/useFindSuspenseClient - null return is allowed',
44 | async (test) => {
45 | const TestDocs = new Mongo.Collection(null);
46 |
47 | TestDocs.insertAsync({ id: 0, updated: 0 });
48 |
49 | const { result } = renderHook(() =>
50 | useFindSuspenseClient(TestDocs, null)
51 | );
52 |
53 | test.isNull(
54 | result.current,
55 | 'Return value should be null when the factory returns null'
56 | );
57 | }
58 | );
59 | }
60 |
61 | /**
62 | * Test for useFindSuspenseServer
63 | */
64 | if (Meteor.isServer) {
65 | Tinytest.addAsync(
66 | 'suspense/useFindSuspenseServer - Data query validation',
67 | async function (test) {
68 | const TestDocs = new Mongo.Collection(null);
69 |
70 | TestDocs.insertAsync({ id: 0, updated: 0 });
71 |
72 | let returnValue;
73 |
74 | const Test = () => {
75 | returnValue = useFindSuspenseServer(TestDocs, [{}]);
76 |
77 | return null;
78 | };
79 | const TestSuspense = () => {
80 | return (
81 | Loading...}>
82 |
83 |
84 | );
85 | };
86 |
87 | // first return promise
88 | renderToString();
89 | test.isUndefined(
90 | returnValue,
91 | 'Return value should be undefined as find promise unresolved'
92 | );
93 | // wait promise
94 | await new Promise((resolve) => setTimeout(resolve, 100));
95 | // return data
96 | renderToString();
97 |
98 | test.equal(
99 | returnValue.length,
100 | 1,
101 | 'Return value should be an array with one document'
102 | );
103 | }
104 | );
105 |
106 | Tinytest.addAsync(
107 | 'suspense/useFindSuspenseServer - Test proper cache invalidation',
108 | async function (test) {
109 | const TestDocs = new Mongo.Collection(null);
110 |
111 | TestDocs.insertAsync({ id: 0, updated: 0 });
112 |
113 | let returnValue;
114 |
115 | const Test = () => {
116 | returnValue = useFindSuspenseServer(TestDocs, [{}]);
117 |
118 | return null;
119 | };
120 | const TestSuspense = () => {
121 | return (
122 | Loading...}>
123 |
124 |
125 | );
126 | };
127 |
128 | // first return promise
129 | renderToString();
130 |
131 | test.isUndefined(
132 | returnValue,
133 | 'Return value should be undefined as find promise unresolved'
134 | );
135 | // wait promise
136 | await new Promise((resolve) => setTimeout(resolve, 100));
137 | // return data
138 | renderToString();
139 |
140 | test.equal(
141 | returnValue[0].updated,
142 | 0,
143 | 'Return value should be an array with initial value as find promise resolved'
144 | );
145 |
146 | TestDocs.updateAsync({ id: 0 }, { $inc: { updated: 1 } });
147 | await new Promise((resolve) => setTimeout(resolve, 100));
148 |
149 | // second return promise
150 | renderToString();
151 |
152 | test.equal(
153 | returnValue[0].updated,
154 | 0,
155 | 'Return value should still not updated as second find promise unresolved'
156 | );
157 |
158 | // wait promise
159 | await new Promise((resolve) => setTimeout(resolve, 100));
160 | // return data
161 | renderToString();
162 |
163 | test.equal(
164 | returnValue[0].updated,
165 | 1,
166 | 'Return value should be an array with one document with value updated'
167 | );
168 | }
169 | );
170 |
171 | Tinytest.addAsync(
172 | 'suspense/useFindSuspenseServer - null return is allowed',
173 | async function (test) {
174 | const TestDocs = new Mongo.Collection(null);
175 |
176 | TestDocs.insertAsync({ id: 0, updated: 0 });
177 |
178 | let returnValue;
179 |
180 | const Test = () => {
181 | returnValue = useFindSuspenseServer(TestDocs, null);
182 |
183 | return null;
184 | };
185 | const TestSuspense = () => {
186 | return (
187 | Loading...}>
188 |
189 |
190 | );
191 | };
192 |
193 | renderToString();
194 |
195 | test.isNull(
196 | returnValue,
197 | 'Return value should be null when the factory returns null'
198 | );
199 | }
200 | );
201 | }
202 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/useFind.ts:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import { EJSON } from 'meteor/ejson'
3 | import { Mongo } from 'meteor/mongo'
4 | import type React from 'react'
5 | import { useReducer, useMemo, useEffect, type Reducer, type DependencyList, useRef } from 'react'
6 | import { Tracker } from 'meteor/tracker'
7 |
8 | type useFindActions =
9 | | { type: 'refresh', data: T[] }
10 | | { type: 'addedAt', document: T, atIndex: number }
11 | | { type: 'changedAt', document: T, atIndex: number }
12 | | { type: 'removedAt', atIndex: number }
13 | | { type: 'movedTo', fromIndex: number, toIndex: number }
14 |
15 | const useFindReducer = (data: T[], action: useFindActions): T[] => {
16 | switch (action.type) {
17 | case 'refresh':
18 | return action.data
19 | case 'addedAt':
20 | return [
21 | ...data.slice(0, action.atIndex),
22 | action.document,
23 | ...data.slice(action.atIndex)
24 | ]
25 | case 'changedAt':
26 | return [
27 | ...data.slice(0, action.atIndex),
28 | action.document,
29 | ...data.slice(action.atIndex + 1)
30 | ]
31 | case 'removedAt':
32 | return [
33 | ...data.slice(0, action.atIndex),
34 | ...data.slice(action.atIndex + 1)
35 | ]
36 | case 'movedTo':
37 | const doc = data[action.fromIndex]
38 | const copy = [
39 | ...data.slice(0, action.fromIndex),
40 | ...data.slice(action.fromIndex + 1)
41 | ]
42 | copy.splice(action.toIndex, 0, doc)
43 | return copy
44 | }
45 | }
46 |
47 | // Check for valid Cursor or null.
48 | // On client, we should have a Mongo.Cursor (defined in
49 | // https://github.com/meteor/meteor/blob/devel/packages/minimongo/cursor.js and
50 | // https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js).
51 | // On server, however, we instead get a private Cursor type from
52 | // https://github.com/meteor/meteor/blob/devel/packages/mongo/mongo_driver.js
53 | // which has fields _mongo and _cursorDescription.
54 | const checkCursor = (
55 | cursor:
56 | | Mongo.Cursor
57 | | Partial<{ _mongo: any, _cursorDescription: any }>
58 | | undefined
59 | | null
60 | ) => {
61 | if (
62 | cursor !== null &&
63 | cursor !== undefined &&
64 | !(cursor instanceof Mongo.Cursor) &&
65 | !(cursor._mongo && cursor._cursorDescription)
66 | ) {
67 | console.warn(
68 | 'Warning: useFind requires an instance of Mongo.Cursor. ' +
69 | 'Make sure you do NOT call .fetch() on your cursor.'
70 | )
71 | }
72 | }
73 |
74 | // Synchronous data fetch. It uses cursor observing instead of cursor.fetch() because synchronous fetch will be deprecated.
75 | const fetchData = (cursor: Mongo.Cursor) => {
76 | const data: T[] = []
77 | const observer = cursor.observe({
78 | addedAt(document, atIndex, before) {
79 | data.splice(atIndex, 0, document)
80 | }
81 | })
82 | observer.stop()
83 | return data
84 | }
85 |
86 | export const useFindSuspenseClient = (
87 | collection: Mongo.Collection,
88 | findArgs: Parameters['find']> | null,
89 | deps: DependencyList = []
90 | ) => {
91 | const findArgsKey = EJSON.stringify(findArgs)
92 |
93 | const cursor = useMemo(() => {
94 | // To avoid creating side effects in render, opt out
95 | // of Tracker integration altogether.
96 | const cursor = Tracker.nonreactive(() => findArgs && collection.find(...findArgs))
97 | if (Meteor.isDevelopment) {
98 | checkCursor(cursor)
99 | }
100 | return cursor
101 | }, [findArgsKey, ...deps])
102 |
103 | const [data, dispatch] = useReducer>, null>(
104 | useFindReducer,
105 | null,
106 | () => {
107 | if (!(cursor instanceof Mongo.Cursor)) {
108 | return []
109 | }
110 |
111 | return fetchData(cursor)
112 | }
113 | )
114 |
115 | // Store information about mounting the component.
116 | // It will be used to run code only if the component is updated.
117 | const didMount = useRef(false)
118 |
119 | useEffect(() => {
120 | // Fetch intitial data if cursor was changed.
121 | if (didMount.current) {
122 | if (!(cursor instanceof Mongo.Cursor)) {
123 | return
124 | }
125 |
126 | const data = fetchData(cursor)
127 | dispatch({ type: 'refresh', data })
128 | } else {
129 | didMount.current = true
130 | }
131 |
132 | if (!(cursor instanceof Mongo.Cursor)) {
133 | return
134 | }
135 |
136 | const observer = cursor.observe({
137 | addedAt(document, atIndex, before) {
138 | dispatch({ type: 'addedAt', document, atIndex })
139 | },
140 | changedAt(newDocument, oldDocument, atIndex) {
141 | dispatch({ type: 'changedAt', document: newDocument, atIndex })
142 | },
143 | removedAt(oldDocument, atIndex) {
144 | dispatch({ type: 'removedAt', atIndex })
145 | },
146 | movedTo(document, fromIndex, toIndex, before) {
147 | dispatch({ type: 'movedTo', fromIndex, toIndex })
148 | },
149 | // @ts-expect-error
150 | _suppress_initial: true
151 | })
152 |
153 | return () => {
154 | observer.stop()
155 | }
156 | }, [cursor])
157 |
158 | return cursor ? data : cursor
159 | }
160 |
161 | interface Entry {
162 | findArgs: Parameters['find']>
163 | promise: Promise
164 | result?: unknown
165 | error?: unknown
166 | }
167 |
168 | const cacheMap = new Map, Map>()
169 |
170 | export const useFindSuspenseServer = (
171 | collection: Mongo.Collection,
172 | findArgs: Parameters['find']> | null,
173 | deps: React.DependencyList = []
174 | ) => {
175 | if (findArgs === null) return null
176 |
177 | const cachedEntries = cacheMap.get(collection)
178 | const findArgsKey = EJSON.stringify(findArgs)
179 | const cachedEntry = cachedEntries?.get(findArgsKey)
180 |
181 | if (cachedEntry != null) {
182 | if ('error' in cachedEntry || 'result' in cachedEntry) {
183 | setTimeout(() => {
184 | if (cachedEntries) cachedEntries.delete(findArgsKey)
185 | else cacheMap.delete(collection)
186 | }, 0)
187 | }
188 | if ('error' in cachedEntry) throw cachedEntry.error
189 | if ('result' in cachedEntry) return cachedEntry.result as T[]
190 | throw cachedEntry.promise
191 | }
192 |
193 | const entry: Entry = {
194 | findArgs,
195 | promise: collection
196 | .find(...findArgs)
197 | .fetchAsync()
198 | .then(
199 | result => {
200 | entry.result = result
201 | },
202 | error => {
203 | entry.error = error
204 | }
205 | )
206 | }
207 |
208 | if (!cachedEntries) cacheMap.set(collection, new Map([[findArgsKey, entry]]))
209 | else cachedEntries.set(findArgsKey, entry)
210 |
211 | throw entry.promise
212 | }
213 |
214 | export const useFind = Meteor.isServer
215 | ? useFindSuspenseServer
216 | : useFindSuspenseClient
217 |
218 | function useFindDev(
219 | collection: Mongo.Collection,
220 | findArgs: Parameters['find']> | null,
221 | deps: React.DependencyList = []
222 | ) {
223 | function warn(expects: string, pos: string, arg: string, type: string) {
224 | console.warn(
225 | `Warning: useFind expected a ${expects} in it\'s ${pos} argument ` +
226 | `(${arg}), but got type of \`${type}\`.`
227 | )
228 | }
229 |
230 | if (typeof collection !== 'object') {
231 | warn('Mongo Collection', '1st', 'reactiveFn', collection)
232 | }
233 |
234 | return useFind(collection, findArgs, deps)
235 | }
236 |
237 | export default Meteor.isDevelopment
238 | ? useFindDev
239 | : useFind
240 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/useSubscribe.tests.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { waitFor, render, renderHook } from '@testing-library/react';
3 |
4 | import { useSubscribeSuspense } from './useSubscribe';
5 |
6 | if (Meteor.isServer) {
7 | Meteor.publish('testUseSubscribe', function () {
8 | this.added('testCollection', 0, { name: 'nameA' });
9 | this.ready();
10 | });
11 | } else {
12 | Tinytest.addAsync(
13 | 'suspense/useSubscribe - Verified data returned successfully',
14 | async (test) => {
15 | const TestCollection = new Mongo.Collection('testCollection');
16 |
17 | const Test = () => {
18 | useSubscribeSuspense('testUseSubscribe');
19 |
20 | const doc = TestCollection.findOne();
21 |
22 | return {doc.name}
;
23 | };
24 | const TestSuspense = () => {
25 | return (
26 | Loading...}>
27 |
28 |
29 | );
30 | };
31 |
32 | const { queryByText, findByText, unmount } = render(, {
33 | container: document.createElement('container'),
34 | });
35 |
36 | test.isNotNull(
37 | queryByText('Loading...'),
38 | 'Throw Promise as needed to trigger the fallback.'
39 | );
40 |
41 | test.isTrue(await findByText('nameA'), 'Need to return data');
42 |
43 | unmount();
44 | }
45 | );
46 |
47 | Tinytest.addAsync(
48 | 'suspense/useSubscribe - Simulate running in strict mode',
49 | async (test) => {
50 | // Repeated runs of this block should consistently pass without failures.
51 | for (let i = 0; i < 10; i++) {
52 | const { result, rerender, unmount } = renderHook(
53 | () => {
54 | try {
55 | return useSubscribeSuspense('testUseSubscribe');
56 | } catch (promise) {
57 | return promise;
58 | }
59 | },
60 | {
61 | reactStrictMode: true,
62 | }
63 | );
64 |
65 | await result.current;
66 |
67 | rerender();
68 |
69 | test.isNull(
70 | result.current,
71 | 'Should be null after rerender (this indicates the data has been read)'
72 | );
73 |
74 | unmount();
75 | }
76 | }
77 | );
78 |
79 | Tinytest.addAsync(
80 | 'suspense/useSubscribe - when multiple subscriptions are active, cleaning one preserves others',
81 | async (test) => {
82 | // Run in both normal mode and strict mode respectively.
83 | for (const reactStrictMode of [false, true]) {
84 | const {
85 | result: resultA,
86 | rerender: rerenderA,
87 | unmount: unmountA,
88 | } = renderHook(
89 | () => {
90 | try {
91 | return useSubscribeSuspense('testUseSubscribe');
92 | } catch (promise) {
93 | return promise;
94 | }
95 | },
96 | { reactStrictMode }
97 | );
98 |
99 | await resultA.current;
100 | rerenderA();
101 |
102 | const { result: resultB, unmount: unmountB } = renderHook(
103 | () => {
104 | try {
105 | return useSubscribeSuspense('testUseSubscribe');
106 | } catch (promise) {
107 | return promise;
108 | }
109 | },
110 | { reactStrictMode }
111 | );
112 |
113 | test.isNull(
114 | resultB.current,
115 | 'Should be null after subscribeA (this indicates the data has been cached)'
116 | );
117 |
118 | unmountB();
119 | await waitFor(() => {});
120 |
121 | rerenderA();
122 |
123 | test.isNull(
124 | resultA.current,
125 | 'Should be null (this indicates the data has been cached)'
126 | );
127 |
128 | unmountA();
129 | await waitFor(() => {});
130 |
131 | const { result: resultA2, unmount: unmountA2 } = renderHook(
132 | () => {
133 | try {
134 | return useSubscribeSuspense('testUseSubscribe');
135 | } catch (promise) {
136 | return promise;
137 | }
138 | },
139 | { reactStrictMode }
140 | );
141 |
142 | test.instanceOf(
143 | resultA2.current,
144 | Promise,
145 | 'Should be a promise (this indicates the data has been cleaned)'
146 | );
147 |
148 | await resultA2.current;
149 | unmountA2();
150 | }
151 | }
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/useSubscribe.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { EJSON } from 'meteor/ejson'
3 | import { Meteor } from 'meteor/meteor'
4 |
5 | const cachedSubscriptions = new Map()
6 |
7 | interface Entry {
8 | params: EJSON[]
9 | name: string
10 | handle?: Meteor.SubscriptionHandle
11 | promise: Promise
12 | result?: null
13 | error?: unknown
14 | counter: number
15 | }
16 |
17 | const useSubscribeSuspenseClient = (name: string, ...params: EJSON[]) => {
18 | const subscribeKey = EJSON.stringify([name, params])
19 | const cachedSubscription = cachedSubscriptions.get(subscribeKey)
20 | const cleanupTimoutIdRefs = useRef(new Map())
21 |
22 | useEffect(() => {
23 | /**
24 | * In strict mode (development only), `useEffect` may run 1-2 times.
25 | * Throwing a promise outside can cause premature cleanup of subscriptions and cachedSubscription before unmount.
26 | * To avoid this, check the `timeout` to ensure cleanup only occurs after unmount.
27 | */
28 | if (cleanupTimoutIdRefs.current.has(subscribeKey)) {
29 | clearTimeout(cleanupTimoutIdRefs.current.get(subscribeKey))
30 | cleanupTimoutIdRefs.current.delete(subscribeKey)
31 | } else {
32 | const cachedSubscription = cachedSubscriptions.get(subscribeKey)
33 | if (cachedSubscription) cachedSubscription.counter++
34 | }
35 |
36 | return () => {
37 | cleanupTimoutIdRefs.current.set(
38 | subscribeKey,
39 | setTimeout(() => {
40 | const cachedSubscription = cachedSubscriptions.get(subscribeKey)
41 |
42 | if (cachedSubscription) {
43 | cachedSubscription.counter--
44 |
45 | if (cachedSubscription.counter === 0) {
46 | cachedSubscription.handle?.stop()
47 | cachedSubscriptions.delete(subscribeKey)
48 | cleanupTimoutIdRefs.current.delete(subscribeKey)
49 | }
50 | }
51 | }, 0)
52 | )
53 | }
54 | }, [subscribeKey])
55 |
56 | if (cachedSubscription != null) {
57 | if ('error' in cachedSubscription) throw cachedSubscription.error
58 | if ('result' in cachedSubscription) return cachedSubscription.result
59 | throw cachedSubscription.promise
60 | }
61 |
62 | const subscription: Entry = {
63 | name,
64 | params,
65 | promise: new Promise((resolve, reject) => {
66 | const h = Meteor.subscribe(name, ...params, {
67 | onReady() {
68 | subscription.result = null
69 | subscription.handle = h
70 | resolve(h)
71 | },
72 | onStop(error: unknown) {
73 | subscription.error = error
74 | subscription.handle = h
75 | reject(error)
76 | }
77 | })
78 | }),
79 | counter: 0
80 | }
81 |
82 | cachedSubscriptions.set(subscribeKey, subscription)
83 |
84 | throw subscription.promise
85 | }
86 |
87 | const useSubscribeSuspenseServer = (name?: string, ...args: any[]) => undefined
88 |
89 | export const useSubscribeSuspense = Meteor.isServer
90 | ? useSubscribeSuspenseServer
91 | : useSubscribeSuspenseClient
92 |
93 | export const useSubscribe = useSubscribeSuspense
94 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/suspense/useTracker.ts:
--------------------------------------------------------------------------------
1 | import isEqual from 'lodash.isequal'
2 | import { Tracker } from 'meteor/tracker'
3 | import { type EJSON } from 'meteor/ejson'
4 | import { type DependencyList, useEffect, useMemo, useReducer, useRef } from 'react'
5 | import { Meteor } from 'meteor/meteor'
6 |
7 | function checkCursor(data: any): void {
8 | let shouldWarn = false
9 | if (Package.mongo && Package.mongo.Mongo && data && typeof data === 'object') {
10 | if (data instanceof Package.mongo.Mongo.Cursor) {
11 | shouldWarn = true
12 | } else if (Object.getPrototypeOf(data) === Object.prototype) {
13 | Object.keys(data).forEach((key) => {
14 | if (data[key] instanceof Package.mongo.Mongo.Cursor) {
15 | shouldWarn = true
16 | }
17 | })
18 | }
19 | }
20 | if (shouldWarn) {
21 | console.warn(
22 | 'Warning: your reactive function is returning a Mongo cursor. ' +
23 | 'This value will not be reactive. You probably want to call ' +
24 | '`.fetch()` on the cursor before returning it.'
25 | )
26 | }
27 | }
28 |
29 | export const cacheMap = new Map()
30 |
31 | interface Entry {
32 | deps: EJSON[]
33 | promise: Promise
34 | result?: unknown
35 | error?: unknown
36 | }
37 |
38 | // Used to create a forceUpdate from useReducer. Forces update by
39 | // incrementing a number whenever the dispatch method is invoked.
40 | const fur = (x: number): number => x + 1
41 | const useForceUpdate = () => useReducer(fur, 0)[1]
42 |
43 | export type IReactiveFn = (c?: Tracker.Computation) => Promise
44 |
45 | export type ISkipUpdate = (prev: T, next: T) => boolean
46 |
47 | interface TrackerRefs {
48 | computation?: Tracker.Computation
49 | isMounted: boolean
50 | trackerData: any
51 | }
52 |
53 | function resolveAsync(key: string, promise: Promise | null, deps: DependencyList = []): typeof promise extends null ? null : T {
54 | const cached = cacheMap.get(key)
55 | useEffect(() =>
56 | () => {
57 | setTimeout(() => {
58 | if (cached !== undefined && isEqual(cached.deps, deps)) cacheMap.delete(key)
59 | }, 0)
60 | }, [cached, key, ...deps])
61 |
62 | if (promise === null) return null
63 |
64 | if (cached !== undefined) {
65 | if ('error' in cached) throw cached.error
66 | if ('result' in cached) {
67 | const result = cached.result as T
68 | setTimeout(() => {
69 | cacheMap.delete(key)
70 | }, 0)
71 | return result
72 | }
73 | throw cached.promise
74 | }
75 |
76 | const entry: Entry = {
77 | deps,
78 | promise: new Promise((resolve, reject) => {
79 | promise
80 | .then((result: any) => {
81 | entry.result = result
82 | resolve(result)
83 | })
84 | .catch((error: any) => {
85 | entry.error = error
86 | reject(error)
87 | })
88 | })
89 | }
90 | cacheMap.set(key, entry)
91 | throw entry.promise
92 | }
93 |
94 | export function useTrackerNoDeps(key: string, reactiveFn: IReactiveFn, skipUpdate: ISkipUpdate = null): T {
95 | const { current: refs } = useRef({
96 | isMounted: false,
97 | trackerData: null
98 | })
99 | const forceUpdate = useForceUpdate()
100 |
101 | // Without deps, always dispose and recreate the computation with every render.
102 | if (refs.computation != null) {
103 | refs.computation.stop()
104 | // @ts-expect-error This makes TS think ref.computation is "never" set
105 | delete refs.computation
106 | }
107 |
108 | // Use Tracker.nonreactive in case we are inside a Tracker Computation.
109 | // This can happen if someone calls `ReactDOM.render` inside a Computation.
110 | // In that case, we want to opt out of the normal behavior of nested
111 | // Computations, where if the outer one is invalidated or stopped,
112 | // it stops the inner one.
113 | Tracker.nonreactive(() =>
114 | Tracker.autorun(async (c: Tracker.Computation) => {
115 | refs.computation = c
116 |
117 | const data: Promise = Tracker.withComputation(c, async () => reactiveFn(c))
118 | if (c.firstRun) {
119 | // Always run the reactiveFn on firstRun
120 | refs.trackerData = data
121 | } else if (!skipUpdate || !skipUpdate(await refs.trackerData, await data)) {
122 | // For any reactive change, forceUpdate and let the next render rebuild the computation.
123 | forceUpdate()
124 | }
125 | }))
126 |
127 | // To clean up side effects in render, stop the computation immediately
128 | if (!refs.isMounted) {
129 | Meteor.defer(() => {
130 | if (!refs.isMounted && (refs.computation != null)) {
131 | refs.computation.stop()
132 | delete refs.computation
133 | }
134 | })
135 | }
136 |
137 | useEffect(() => {
138 | // Let subsequent renders know we are mounted (render is committed).
139 | refs.isMounted = true
140 |
141 | // In some cases, the useEffect hook will run before Meteor.defer, such as
142 | // when React.lazy is used. In those cases, we might as well leave the
143 | // computation alone!
144 | if (refs.computation == null) {
145 | // Render is committed, but we no longer have a computation. Invoke
146 | // forceUpdate and let the next render recreate the computation.
147 | if (!skipUpdate) {
148 | forceUpdate()
149 | } else {
150 | Tracker.nonreactive(() =>
151 | Tracker.autorun(async (c: Tracker.Computation) => {
152 | const data = Tracker.withComputation(c, async () => reactiveFn(c))
153 |
154 | refs.computation = c
155 | if (!skipUpdate(await refs.trackerData, await data)) {
156 | // For any reactive change, forceUpdate and let the next render rebuild the computation.
157 | forceUpdate()
158 | }
159 | }))
160 | }
161 | }
162 |
163 | // stop the computation on unmount
164 | return () => {
165 | refs.computation?.stop()
166 | delete refs.computation
167 | refs.isMounted = false
168 | }
169 | }, [])
170 |
171 | return resolveAsync(key, refs.trackerData)
172 | }
173 |
174 | export const useTrackerWithDeps =
175 | (key: string, reactiveFn: IReactiveFn, deps: DependencyList, skipUpdate: ISkipUpdate = null): T => {
176 | const forceUpdate = useForceUpdate()
177 |
178 | const { current: refs } = useRef<{
179 | reactiveFn: IReactiveFn
180 | data?: Promise
181 | comp?: Tracker.Computation
182 | isMounted?: boolean
183 | }>({ reactiveFn })
184 |
185 | // keep reactiveFn ref fresh
186 | refs.reactiveFn = reactiveFn
187 |
188 | useMemo(() => {
189 | // To jive with the lifecycle interplay between Tracker/Subscribe, run the
190 | // reactive function in a computation, then stop it, to force flush cycle.
191 | const comp = Tracker.nonreactive(
192 | () => Tracker.autorun(async (c: Tracker.Computation) => {
193 | const data = Tracker.withComputation(c, async () => refs.reactiveFn(c))
194 | if (c.firstRun) {
195 | refs.data = data
196 | } else if (!skipUpdate || !skipUpdate(await refs.data, await data)) {
197 | refs.data = data
198 | forceUpdate()
199 | }
200 | })
201 | )
202 |
203 | // Stop the computation immediately to avoid creating side effects in render.
204 | // refers to this issues:
205 | // https://github.com/meteor/react-packages/issues/382
206 | // https://github.com/meteor/react-packages/issues/381
207 | if (refs.comp != null) refs.comp.stop()
208 |
209 | // In some cases, the useEffect hook will run before Meteor.defer, such as
210 | // when React.lazy is used. This will allow it to be stopped earlier in
211 | // useEffect if needed.
212 | refs.comp = comp
213 | // To avoid creating side effects in render, stop the computation immediately
214 | Meteor.defer(() => {
215 | if (!refs.isMounted && (refs.comp != null)) {
216 | refs.comp.stop()
217 | delete refs.comp
218 | }
219 | })
220 | }, deps)
221 |
222 | useEffect(() => {
223 | // Let subsequent renders know we are mounted (render is committed).
224 | refs.isMounted = true
225 |
226 | if (refs.comp == null) {
227 | refs.comp = Tracker.nonreactive(
228 | () => Tracker.autorun(async (c) => {
229 | const data: Promise = Tracker.withComputation(c, async () => refs.reactiveFn())
230 | if (!skipUpdate || !skipUpdate(await refs.data, await data)) {
231 | refs.data = data
232 | forceUpdate()
233 | }
234 | })
235 | )
236 | }
237 |
238 | return () => {
239 | // @ts-expect-error
240 | refs.comp.stop()
241 | delete refs.comp
242 | refs.isMounted = false
243 | }
244 | }, deps)
245 |
246 | return resolveAsync(key, refs.data as Promise, deps)
247 | }
248 |
249 | function useTrackerClient(key: string, reactiveFn: IReactiveFn, skipUpdate?: ISkipUpdate): T
250 | function useTrackerClient(key: string, reactiveFn: IReactiveFn, deps?: DependencyList, skipUpdate?: ISkipUpdate): T
251 | function useTrackerClient(key: string, reactiveFn: IReactiveFn, deps: DependencyList | ISkipUpdate = null, skipUpdate: ISkipUpdate = null): T {
252 | if (deps === null || deps === undefined || !Array.isArray(deps)) {
253 | if (typeof deps === 'function') {
254 | skipUpdate = deps
255 | }
256 | return useTrackerNoDeps(key, reactiveFn, skipUpdate)
257 | } else {
258 | return useTrackerWithDeps(key, reactiveFn, deps, skipUpdate)
259 | }
260 | }
261 |
262 | const useTrackerServer: typeof useTrackerClient = (key, reactiveFn) => {
263 | return resolveAsync(key, Tracker.nonreactive(reactiveFn))
264 | }
265 |
266 | // When rendering on the server, we don't want to use the Tracker.
267 | // We only do the first rendering on the server so we can get the data right away
268 | const _useTracker = Meteor.isServer
269 | ? useTrackerServer
270 | : useTrackerClient
271 |
272 | function useTrackerDev(key: string, reactiveFn, deps: DependencyList | null = null, skipUpdate = null) {
273 | function warn(expects: string, pos: string, arg: string, type: string) {
274 | console.warn(
275 | `Warning: useTracker expected a ${expects} in it\'s ${pos} argument ` +
276 | `(${arg}), but got type of \`${type}\`.`
277 | )
278 | }
279 |
280 | if (typeof reactiveFn !== 'function') {
281 | warn('function', '1st', 'reactiveFn', reactiveFn)
282 | }
283 |
284 | if ((deps != null) && skipUpdate && !Array.isArray(deps) && typeof skipUpdate === 'function') {
285 | warn('array & function', '2nd and 3rd', 'deps, skipUpdate',
286 | `${typeof deps} & ${typeof skipUpdate}`)
287 | } else {
288 | if ((deps != null) && !Array.isArray(deps) && typeof deps !== 'function') {
289 | warn('array or function', '2nd', 'deps or skipUpdate', typeof deps)
290 | }
291 | if (skipUpdate && typeof skipUpdate !== 'function') {
292 | warn('function', '3rd', 'skipUpdate', typeof skipUpdate)
293 | }
294 | }
295 |
296 | const data = _useTracker(key, reactiveFn, deps, skipUpdate)
297 | checkCursor(data)
298 | return data
299 | }
300 |
301 | export const useTracker = Meteor.isDevelopment
302 | ? useTrackerDev as typeof useTrackerClient
303 | : _useTracker
304 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/tests.js:
--------------------------------------------------------------------------------
1 | import './useTracker.tests.js'
2 | import './withTracker.tests.js'
3 | import './useFind.tests.js'
4 | import './suspense/useSubscribe.tests.js'
5 | import './suspense/useFind.tests.js'
6 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/useFind.tests.js:
--------------------------------------------------------------------------------
1 | /* global Meteor, Tinytest */
2 | import React, { memo, useState } from 'react'
3 | import ReactDOM from 'react-dom'
4 | import { waitFor } from '@testing-library/react'
5 | import { Mongo } from 'meteor/mongo'
6 |
7 | import { useFind } from './useFind'
8 |
9 | if (Meteor.isClient) {
10 | Tinytest.addAsync('useFind - Verify reference stability between rerenders', async function (test, completed) {
11 | const container = document.createElement("DIV")
12 |
13 | const TestDocs = new Mongo.Collection(null)
14 |
15 | TestDocs.insert({
16 | id: 0,
17 | updated: 0
18 | })
19 | TestDocs.insert({
20 | id: 1,
21 | updated: 0
22 | })
23 | TestDocs.insert({
24 | id: 2,
25 | updated: 0
26 | })
27 | TestDocs.insert({
28 | id: 3,
29 | updated: 0
30 | })
31 | TestDocs.insert({
32 | id: 4,
33 | updated: 0
34 | })
35 |
36 | let renders = 0
37 | const MemoizedItem = memo(({doc}) => {
38 | renders++
39 | return (
40 | {doc.id},{doc.updated}
41 | )
42 | })
43 |
44 | const Test = () => {
45 | const docs = useFind(() => TestDocs.find(), [])
46 | return (
47 |
48 | {docs.map(doc =>
49 |
50 | )}
51 |
52 | )
53 | }
54 |
55 | ReactDOM.render(, container)
56 | test.equal(renders, 5, '5 items should have rendered, only 5, no more.')
57 |
58 | await waitFor(() => {}, { container, timeout: 250 })
59 |
60 | await waitFor(() => {
61 | TestDocs.update({ id: 2 }, { $inc: { updated: 1 } })
62 | }, { container, timeout: 250 })
63 |
64 | test.equal(renders, 6, '6 items should have rendered - only 1 of the items should have been matched by the reconciler after a single change.')
65 |
66 | completed()
67 | })
68 |
69 | Tinytest.addAsync('useFind - null return is allowed', async function (test, completed) {
70 | const container = document.createElement("DIV")
71 |
72 | const TestDocs = new Mongo.Collection(null)
73 |
74 | TestDocs.insert({
75 | id: 0,
76 | updated: 0
77 | })
78 |
79 | let setReturnNull, returnValue;
80 |
81 | const Test = () => {
82 | const [returnNull, _setReturnNull] = useState(true)
83 | setReturnNull = _setReturnNull
84 | const docs = useFind(() => returnNull ? null : TestDocs.find(), [returnNull])
85 | returnValue = docs;
86 | if (!docs) {
87 | return null
88 | } else {
89 | return (
90 |
91 | {docs.map(doc =>
92 |
93 | )}
94 |
95 | )
96 | }
97 | }
98 |
99 | ReactDOM.render(, container)
100 | test.isNull(returnValue, 'Return value should be null when the factory returns null')
101 |
102 | setReturnNull(false)
103 |
104 | await waitFor(() => {}, { container, timeout: 250 })
105 | test.equal(returnValue.length, 1, 'Return value should be an array with one document')
106 |
107 | completed()
108 | })
109 | // Test that catches the issue reported on https://github.com/meteor/react-packages/issues/418
110 | Tinytest.addAsync(
111 | 'useFind - Immediate update before effect registration (race condition test)',
112 | async function (test, completed) {
113 | const container = document.createElement('div');
114 | document.body.appendChild(container);
115 |
116 | const TestDocs = new Mongo.Collection(null);
117 | // Insert a single document.
118 | TestDocs.insert({ id: 1, val: 'initial' });
119 |
120 | const Test = () => {
121 | const docs = useFind(() => TestDocs.find(), []);
122 | return (
123 |
124 | {docs && docs[0] && docs[0].val}
125 |
126 | );
127 | };
128 |
129 | // Render the component.
130 | ReactDOM.render(, container);
131 |
132 | // Immediately update the document (this should occur
133 | // after the synchronous fetch in the old code but before the effect attaches).
134 | TestDocs.update({ id: 1 }, { $set: { val: 'updated' } });
135 |
136 | // Wait until the rendered output reflects the update.
137 | await waitFor(() => {
138 | const node = container.querySelector('[data-testid="doc-value"]');
139 | if (!node || !node.textContent.includes('updated')) {
140 | throw new Error('Updated value not rendered yet');
141 | }
142 | }, { container, timeout: 500 });
143 |
144 | test.ok(
145 | container.innerHTML.includes('updated'),
146 | 'Document should display updated value; the old code would fail to capture this update.'
147 | );
148 |
149 | document.body.removeChild(container);
150 | completed();
151 | }
152 | );
153 | } else {
154 |
155 | }
156 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/useFind.ts:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import { Mongo } from 'meteor/mongo'
3 | import { useReducer, useMemo, useEffect, Reducer, DependencyList, useRef } from 'react'
4 | import { Tracker } from 'meteor/tracker'
5 |
6 | type useFindActions =
7 | | { type: 'refresh', data: T[] }
8 | | { type: 'addedAt', document: T, atIndex: number }
9 | | { type: 'changedAt', document: T, atIndex: number }
10 | | { type: 'removedAt', atIndex: number }
11 | | { type: 'movedTo', fromIndex: number, toIndex: number }
12 |
13 | const useFindReducer = (data: T[], action: useFindActions): T[] => {
14 | switch (action.type) {
15 | case 'refresh':
16 | return action.data
17 | case 'addedAt':
18 | return [
19 | ...data.slice(0, action.atIndex),
20 | action.document,
21 | ...data.slice(action.atIndex)
22 | ]
23 | case 'changedAt':
24 | return [
25 | ...data.slice(0, action.atIndex),
26 | action.document,
27 | ...data.slice(action.atIndex + 1)
28 | ]
29 | case 'removedAt':
30 | return [
31 | ...data.slice(0, action.atIndex),
32 | ...data.slice(action.atIndex + 1)
33 | ]
34 | case 'movedTo':
35 | const doc = data[action.fromIndex]
36 | const copy = [
37 | ...data.slice(0, action.fromIndex),
38 | ...data.slice(action.fromIndex + 1)
39 | ]
40 | copy.splice(action.toIndex, 0, doc)
41 | return copy
42 | }
43 | }
44 |
45 | // Check for valid Cursor or null.
46 | // On client, we should have a Mongo.Cursor (defined in
47 | // https://github.com/meteor/meteor/blob/devel/packages/minimongo/cursor.js and
48 | // https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js).
49 | // On server, however, we instead get a private Cursor type from
50 | // https://github.com/meteor/meteor/blob/devel/packages/mongo/mongo_driver.js
51 | // which has fields _mongo and _cursorDescription.
52 | const checkCursor = (cursor: Mongo.Cursor | Partial<{ _mongo: any, _cursorDescription: any }> | undefined | null) => {
53 | if (cursor !== null && cursor !== undefined && !(cursor instanceof Mongo.Cursor) &&
54 | !(cursor._mongo && cursor._cursorDescription)) {
55 | console.warn(
56 | 'Warning: useFind requires an instance of Mongo.Cursor. '
57 | + 'Make sure you do NOT call .fetch() on your cursor.'
58 | );
59 | }
60 | }
61 |
62 | // Synchronous data fetch. It uses cursor observing instead of cursor.fetch() because synchronous fetch will be deprecated.
63 | const fetchData = (cursor: Mongo.Cursor) => {
64 | const data: T[] = []
65 | const observer = cursor.observe({
66 | addedAt (document, atIndex, before) {
67 | data.splice(atIndex, 0, document)
68 | },
69 | })
70 | observer.stop()
71 | return data
72 | }
73 |
74 | const useSyncEffect = (effect, deps) => {
75 | const [cleanup, timeoutId] = useMemo(
76 | () => {
77 | const cleanup = effect();
78 | const timeoutId = setTimeout(cleanup, 1000);
79 | return [cleanup, timeoutId];
80 | },
81 | deps
82 | );
83 |
84 | useEffect(() => {
85 | clearTimeout(timeoutId);
86 |
87 | return cleanup;
88 | }, [cleanup]);
89 | };
90 |
91 |
92 | const useFindClient = (factory: () => (Mongo.Cursor | undefined | null), deps: DependencyList) => {
93 | const cursor = useMemo(() => {
94 | // To avoid creating side effects in render, opt out
95 | // of Tracker integration altogether.
96 | const cursor = Tracker.nonreactive(factory);
97 | if (Meteor.isDevelopment) {
98 | checkCursor(cursor)
99 | }
100 | return cursor
101 | }, deps)
102 |
103 | const [data, dispatch] = useReducer>, null>(
104 | useFindReducer,
105 | null,
106 | () => {
107 | if (!(cursor instanceof Mongo.Cursor)) {
108 | return []
109 | }
110 |
111 | return fetchData(cursor)
112 | }
113 | )
114 |
115 | useSyncEffect(() => {
116 | if (!(cursor instanceof Mongo.Cursor)) {
117 | return
118 | }
119 |
120 | const initialData = fetchData(cursor);
121 | dispatch({ type: 'refresh', data: initialData });
122 |
123 | const observer = cursor.observe({
124 | addedAt(document, atIndex, before) {
125 | dispatch({ type: 'addedAt', document, atIndex })
126 | },
127 | changedAt(newDocument, oldDocument, atIndex) {
128 | dispatch({ type: 'changedAt', document: newDocument, atIndex })
129 | },
130 | removedAt(oldDocument, atIndex) {
131 | dispatch({ type: 'removedAt', atIndex })
132 | },
133 | movedTo(document, fromIndex, toIndex, before) {
134 | dispatch({ type: 'movedTo', fromIndex, toIndex })
135 | },
136 | // @ts-ignore
137 | _suppress_initial: true
138 | })
139 |
140 | return () => {
141 | observer.stop()
142 | }
143 | }, [cursor]);
144 |
145 | return cursor ? data : cursor
146 | }
147 |
148 | const useFindServer = (factory: () => Mongo.Cursor | undefined | null, deps: DependencyList) => (
149 | Tracker.nonreactive(() => {
150 | const cursor = factory()
151 | if (Meteor.isDevelopment) checkCursor(cursor)
152 | return cursor?.fetch?.() ?? null
153 | })
154 | )
155 |
156 | export const useFind = Meteor.isServer
157 | ? useFindServer
158 | : useFindClient
159 |
160 | function useFindDev (factory: () => (Mongo.Cursor | undefined | null), deps: DependencyList = []) {
161 | function warn (expects: string, pos: string, arg: string, type: string) {
162 | console.warn(
163 | `Warning: useFind expected a ${expects} in it\'s ${pos} argument `
164 | + `(${arg}), but got type of \`${type}\`.`
165 | );
166 | }
167 |
168 | if (typeof factory !== 'function') {
169 | warn("function", "1st", "reactiveFn", factory);
170 | }
171 |
172 | if (!deps || !Array.isArray(deps)) {
173 | warn("array", "2nd", "deps", typeof deps);
174 | }
175 |
176 | return useFind(factory, deps);
177 | }
178 |
179 | export default Meteor.isDevelopment
180 | ? useFindDev
181 | : useFind;
182 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/useSubscribe.ts:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import { useTracker } from './useTracker'
3 |
4 | const useSubscribeClient = (name?: string, ...args: any[]): () => boolean => {
5 | let updateOnReady = false
6 | let subscription: Meteor.SubscriptionHandle
7 |
8 | const isReady = useTracker(() => {
9 | if (!name) return true
10 |
11 | subscription = Meteor.subscribe(name, ...args)
12 |
13 | return subscription.ready()
14 | }, () => (!updateOnReady))
15 |
16 | return () => {
17 | updateOnReady = true
18 | return !isReady
19 | }
20 | }
21 |
22 | const useSubscribeServer = (name?: string, ...args: any[]): () => boolean => (
23 | () => false
24 | )
25 |
26 | export const useSubscribe = Meteor.isServer
27 | ? useSubscribeServer
28 | : useSubscribeClient
29 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/useTracker.ts:
--------------------------------------------------------------------------------
1 | declare var Package: any
2 | import { Meteor } from 'meteor/meteor';
3 | import { Tracker } from 'meteor/tracker';
4 | import { useReducer, useEffect, useRef, useMemo, DependencyList } from 'react';
5 |
6 | // Warns if data is a Mongo.Cursor or a POJO containing a Mongo.Cursor.
7 | function checkCursor (data: any): void {
8 | let shouldWarn = false;
9 | if (Package.mongo && Package.mongo.Mongo && data && typeof data === 'object') {
10 | if (data instanceof Package.mongo.Mongo.Cursor) {
11 | shouldWarn = true;
12 | } else if (Object.getPrototypeOf(data) === Object.prototype) {
13 | Object.keys(data).forEach((key) => {
14 | if (data[key] instanceof Package.mongo.Mongo.Cursor) {
15 | shouldWarn = true;
16 | }
17 | });
18 | }
19 | }
20 | if (shouldWarn) {
21 | console.warn(
22 | 'Warning: your reactive function is returning a Mongo cursor. '
23 | + 'This value will not be reactive. You probably want to call '
24 | + '`.fetch()` on the cursor before returning it.'
25 | );
26 | }
27 | }
28 |
29 | // Used to create a forceUpdate from useReducer. Forces update by
30 | // incrementing a number whenever the dispatch method is invoked.
31 | const fur = (x: number): number => x + 1;
32 | const useForceUpdate = () => useReducer(fur, 0)[1];
33 |
34 | export interface IReactiveFn {
35 | (c?: Tracker.Computation): T
36 | }
37 |
38 | export interface ISkipUpdate {
39 | (prev: T, next: T): boolean
40 | }
41 |
42 | type TrackerRefs = {
43 | computation?: Tracker.Computation;
44 | isMounted: boolean;
45 | trackerData: any;
46 | }
47 |
48 | const useTrackerNoDeps = (reactiveFn: IReactiveFn, skipUpdate: ISkipUpdate = null) => {
49 | const { current: refs } = useRef({
50 | isMounted: false,
51 | trackerData: null
52 | });
53 | const forceUpdate = useForceUpdate();
54 |
55 | // Without deps, always dispose and recreate the computation with every render.
56 | if (refs.computation) {
57 | refs.computation.stop();
58 | // @ts-ignore This makes TS think ref.computation is "never" set
59 | delete refs.computation;
60 | }
61 |
62 | // Use Tracker.nonreactive in case we are inside a Tracker Computation.
63 | // This can happen if someone calls `ReactDOM.render` inside a Computation.
64 | // In that case, we want to opt out of the normal behavior of nested
65 | // Computations, where if the outer one is invalidated or stopped,
66 | // it stops the inner one.
67 | Tracker.nonreactive(() => Tracker.autorun((c: Tracker.Computation) => {
68 | refs.computation = c;
69 | const data = reactiveFn(c);
70 | if (c.firstRun) {
71 | // Always run the reactiveFn on firstRun
72 | refs.trackerData = data;
73 | } else if (!skipUpdate || !skipUpdate(refs.trackerData, data)) {
74 | // For any reactive change, forceUpdate and let the next render rebuild the computation.
75 | forceUpdate();
76 | }
77 | }));
78 |
79 | // To clean up side effects in render, stop the computation immediately
80 | if (!refs.isMounted) {
81 | Meteor.defer(() => {
82 | if (!refs.isMounted && refs.computation) {
83 | refs.computation.stop();
84 | delete refs.computation;
85 | }
86 | });
87 | }
88 |
89 | useEffect(() => {
90 | // Let subsequent renders know we are mounted (render is committed).
91 | refs.isMounted = true;
92 |
93 | // In some cases, the useEffect hook will run before Meteor.defer, such as
94 | // when React.lazy is used. In those cases, we might as well leave the
95 | // computation alone!
96 | if (!refs.computation) {
97 | // Render is committed, but we no longer have a computation. Invoke
98 | // forceUpdate and let the next render recreate the computation.
99 | if (!skipUpdate) {
100 | forceUpdate();
101 | } else {
102 | Tracker.nonreactive(() => Tracker.autorun((c: Tracker.Computation) => {
103 | const data = reactiveFn(c);
104 | refs.computation = c;
105 | if (!skipUpdate(refs.trackerData, data)) {
106 | // For any reactive change, forceUpdate and let the next render rebuild the computation.
107 | forceUpdate();
108 | }
109 | }));
110 | }
111 | }
112 |
113 | // stop the computation on unmount
114 | return () =>{
115 | refs.computation?.stop();
116 | delete refs.computation;
117 | refs.isMounted = false;
118 | }
119 | }, []);
120 |
121 | return refs.trackerData;
122 | }
123 |
124 | const useTrackerWithDeps = (reactiveFn: IReactiveFn, deps: DependencyList, skipUpdate: ISkipUpdate = null): T => {
125 | const forceUpdate = useForceUpdate();
126 |
127 | const { current: refs } = useRef<{
128 | reactiveFn: IReactiveFn;
129 | data?: T;
130 | comp?: Tracker.Computation;
131 | isMounted?: boolean;
132 | }>({ reactiveFn });
133 |
134 | // keep reactiveFn ref fresh
135 | refs.reactiveFn = reactiveFn;
136 |
137 | useMemo(() => {
138 | // To jive with the lifecycle interplay between Tracker/Subscribe, run the
139 | // reactive function in a computation, then stop it, to force flush cycle.
140 | const comp = Tracker.nonreactive(
141 | () => Tracker.autorun((c: Tracker.Computation) => {
142 | const data = refs.reactiveFn();
143 | if (c.firstRun) {
144 | refs.data = data;
145 | } else if (!skipUpdate || !skipUpdate(refs.data, data)) {
146 | refs.data = data;
147 | forceUpdate();
148 | }
149 | })
150 | );
151 |
152 | // Stop the computation immediately to avoid creating side effects in render.
153 | // refers to this issues:
154 | // https://github.com/meteor/react-packages/issues/382
155 | // https://github.com/meteor/react-packages/issues/381
156 | if (refs.comp) refs.comp.stop();
157 |
158 | // In some cases, the useEffect hook will run before Meteor.defer, such as
159 | // when React.lazy is used. This will allow it to be stopped earlier in
160 | // useEffect if needed.
161 | refs.comp = comp;
162 | // To avoid creating side effects in render, stop the computation immediately
163 | Meteor.defer(() => {
164 | if (!refs.isMounted && refs.comp) {
165 | refs.comp.stop();
166 | delete refs.comp;
167 | }
168 | });
169 | }, deps);
170 |
171 | useEffect(() => {
172 | // Let subsequent renders know we are mounted (render is committed).
173 | refs.isMounted = true;
174 |
175 | if (!refs.comp) {
176 | refs.comp = Tracker.nonreactive(
177 | () => Tracker.autorun((c) => {
178 | const data: T = refs.reactiveFn(c);
179 | if (!skipUpdate || !skipUpdate(refs.data, data)) {
180 | refs.data = data;
181 | forceUpdate();
182 | }
183 | })
184 | );
185 | }
186 |
187 | return () => {
188 | refs.comp.stop();
189 | delete refs.comp;
190 | refs.isMounted = false;
191 | };
192 | }, deps);
193 |
194 | return refs.data as T;
195 | };
196 |
197 | function useTrackerClient (reactiveFn: IReactiveFn, skipUpdate?: ISkipUpdate): T;
198 | function useTrackerClient (reactiveFn: IReactiveFn, deps?: DependencyList, skipUpdate?: ISkipUpdate): T;
199 | function useTrackerClient (reactiveFn: IReactiveFn, deps: DependencyList | ISkipUpdate = null, skipUpdate: ISkipUpdate = null): T {
200 | if (deps === null || deps === undefined || !Array.isArray(deps)) {
201 | if (typeof deps === "function") {
202 | skipUpdate = deps;
203 | }
204 | return useTrackerNoDeps(reactiveFn, skipUpdate);
205 | } else {
206 | return useTrackerWithDeps(reactiveFn, deps, skipUpdate);
207 | }
208 | }
209 |
210 | const useTrackerServer: typeof useTrackerClient = (reactiveFn) => {
211 | return Tracker.nonreactive(reactiveFn);
212 | }
213 |
214 | // When rendering on the server, we don't want to use the Tracker.
215 | // We only do the first rendering on the server so we can get the data right away
216 | const _useTracker = Meteor.isServer
217 | ? useTrackerServer
218 | : useTrackerClient;
219 |
220 | function useTrackerDev (reactiveFn, deps = null, skipUpdate = null) {
221 | function warn (expects: string, pos: string, arg: string, type: string) {
222 | console.warn(
223 | `Warning: useTracker expected a ${expects} in it\'s ${pos} argument `
224 | + `(${arg}), but got type of \`${type}\`.`
225 | );
226 | }
227 |
228 | if (typeof reactiveFn !== 'function') {
229 | warn("function", "1st", "reactiveFn", reactiveFn);
230 | }
231 |
232 | if (deps && skipUpdate && !Array.isArray(deps) && typeof skipUpdate === "function") {
233 | warn("array & function", "2nd and 3rd", "deps, skipUpdate",
234 | `${typeof deps} & ${typeof skipUpdate}`);
235 | } else {
236 | if (deps && !Array.isArray(deps) && typeof deps !== "function") {
237 | warn("array or function", "2nd", "deps or skipUpdate", typeof deps);
238 | }
239 | if (skipUpdate && typeof skipUpdate !== "function") {
240 | warn("function", "3rd", "skipUpdate", typeof skipUpdate);
241 | }
242 | }
243 |
244 | const data = _useTracker(reactiveFn, deps, skipUpdate);
245 | checkCursor(data);
246 | return data;
247 | }
248 |
249 | export const useTracker = Meteor.isDevelopment
250 | ? useTrackerDev as typeof useTrackerClient
251 | : _useTracker;
252 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/withTracker.tests.js:
--------------------------------------------------------------------------------
1 | /* global Tinytest */
2 | import React, { useState } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { waitFor } from '@testing-library/react';
5 | import { ReactiveVar } from 'meteor/reactive-var';
6 |
7 | import { withTracker } from './withTracker';
8 |
9 | const getInnerHtml = function (elem) {
10 | // clean up elem.innerHTML and strip data-reactid attributes too
11 | return canonicalizeHtml(elem.innerHTML).replace(/ data-reactroot=".*?"/g, '');
12 | };
13 |
14 | if (Meteor.isClient) {
15 | Tinytest.addAsync('withTracker - skipUpdate prevents rerenders', async function (test, completed) {
16 | /**
17 | * In cases where a state change causes rerender before the render is
18 | * committed, useMemo will only run on the first render. This can cause the
19 | * value to get lost (unexpected undefined), if we aren't careful.
20 | */
21 | const container = document.createElement("DIV");
22 | const reactiveDict = new ReactiveDict();
23 | let value;
24 | let renders = 0;
25 | const skipUpdate = (prev, next) => {
26 | // only update when second changes, not first
27 | return prev.value.second === next.value.second;
28 | };
29 | const Test = withTracker({
30 | pure: true,
31 | getMeteorData: () => {
32 | reactiveDict.setDefault('key', { first: 0, second: 0 });
33 | return {
34 | value: reactiveDict.get('key')
35 | };
36 | },
37 | skipUpdate: skipUpdate,
38 | })((props) => {
39 | renders++;
40 | return {JSON.stringify(props.value)};
41 | });
42 |
43 | ReactDOM.render(, container);
44 | test.equal(renders, 1, 'Should have rendered only once');
45 |
46 | // wait for useEffect
47 | await waitFor(() => {}, { container, timeout: 250 });
48 | test.equal(renders, 1, 'Should have rendered only once after mount');
49 |
50 | reactiveDict.set('key', { first: 1, second: 0 });
51 | await waitFor(() => {}, { container, timeout: 250 });
52 |
53 | test.equal(renders, 1, "Should still have rendered only once");
54 |
55 | reactiveDict.set('key', { first: 1, second: 1 });
56 | await waitFor(() => {}, { container, timeout: 250 });
57 |
58 | test.equal(renders, 2, "Should have rendered a second time");
59 |
60 | completed();
61 | });
62 |
63 | Tinytest.addAsync('withTracker - basic track', async function (test, completed) {
64 | var container = document.createElement("DIV");
65 |
66 | var x = new ReactiveVar('aaa');
67 |
68 | var Foo = withTracker(() => {
69 | return {
70 | x: x.get()
71 | };
72 | })((props) => {
73 | return {props.x};
74 | });
75 |
76 | ReactDOM.render(, container);
77 | test.equal(getInnerHtml(container), 'aaa');
78 |
79 | x.set('bbb');
80 | await waitFor(() => {
81 | Tracker.flush({_throwFirstError: true});
82 | }, { container, timeout: 250 });
83 | test.equal(getInnerHtml(container), 'bbb');
84 |
85 | test.equal(x._numListeners(), 1);
86 |
87 | await waitFor(() => {
88 | ReactDOM.unmountComponentAtNode(container);
89 | }, { container, timeout: 250 });
90 |
91 | test.equal(x._numListeners(), 0);
92 |
93 | completed();
94 | });
95 |
96 | // Make sure that calling ReactDOM.render() from an autorun doesn't
97 | // associate that autorun with the mixin's autorun. When autoruns are
98 | // nested, invalidating the outer one stops the inner one, unless
99 | // Tracker.nonreactive is used. This test tests for the use of
100 | // Tracker.nonreactive around the mixin's autorun.
101 | Tinytest.addAsync('withTracker - render in autorun', async function (test, completed) {
102 | var container = document.createElement("DIV");
103 |
104 | var x = new ReactiveVar('aaa');
105 |
106 | var Foo = withTracker(() => {
107 | return {
108 | x: x.get()
109 | };
110 | })((props) => {
111 | return {props.x};
112 | });
113 |
114 | Tracker.autorun(function (c) {
115 | ReactDOM.render(, container);
116 | // Stopping this autorun should not affect the mixin's autorun.
117 | c.stop();
118 | });
119 | test.equal(getInnerHtml(container), 'aaa');
120 |
121 | x.set('bbb');
122 | await waitFor(() => {
123 | Tracker.flush({_throwFirstError: true});
124 | }, { container, timeout: 250 });
125 | test.equal(getInnerHtml(container), 'bbb');
126 |
127 | ReactDOM.unmountComponentAtNode(container);
128 |
129 | completed();
130 | });
131 |
132 | Tinytest.addAsync('withTracker - track based on props and state', async function (test, completed) {
133 | var container = document.createElement("DIV");
134 |
135 | var xs = [new ReactiveVar('aaa'),
136 | new ReactiveVar('bbb'),
137 | new ReactiveVar('ccc')];
138 |
139 | let setState;
140 | var Foo = (props) => {
141 | const [state, _setState] = useState({ m: 0 });
142 | setState = _setState;
143 | const Component = withTracker((props) => {
144 | return {
145 | x: xs[state.m + props.n].get()
146 | };
147 | })((props) => {
148 | return {props.x};
149 | });
150 | return
151 | };
152 |
153 | var comp = ReactDOM.render(, container);
154 |
155 | test.equal(getInnerHtml(container), 'aaa');
156 | xs[0].set('AAA');
157 | test.equal(getInnerHtml(container), 'aaa');
158 | await waitFor(() => {
159 | Tracker.flush({_throwFirstError: true});
160 | }, { container, timeout: 250 });
161 | test.equal(getInnerHtml(container), 'AAA');
162 |
163 | {
164 | let comp2 = ReactDOM.render(, container);
165 | test.isTrue(comp === comp2);
166 | }
167 |
168 | test.equal(getInnerHtml(container), 'bbb');
169 | xs[1].set('BBB');
170 | await waitFor(() => {
171 | Tracker.flush({_throwFirstError: true});
172 | }, { container, timeout: 250 });
173 | test.equal(getInnerHtml(container), 'BBB');
174 |
175 | setState({m: 1});
176 | test.equal(getInnerHtml(container), 'ccc');
177 | xs[2].set('CCC');
178 | await waitFor(() => {
179 | Tracker.flush({_throwFirstError: true});
180 | }, { container, timeout: 250 });
181 | test.equal(getInnerHtml(container), 'CCC');
182 |
183 | ReactDOM.render(, container);
184 | setState({m: 0});
185 | test.equal(getInnerHtml(container), 'AAA');
186 |
187 | ReactDOM.unmountComponentAtNode(container);
188 |
189 | completed();
190 | });
191 |
192 | Tinytest.addAsync('withTracker - track based on props and state (with deps)', async function (test, completed) {
193 | var container = document.createElement("DIV");
194 |
195 | var xs = [new ReactiveVar('aaa'),
196 | new ReactiveVar('bbb'),
197 | new ReactiveVar('ccc')];
198 |
199 | let setState;
200 | var Foo = (props) => {
201 | const [state, _setState] = useState({ m: 0 });
202 | setState = _setState;
203 | const Component = withTracker(() => {
204 | return {
205 | x: xs[state.m + props.n].get()
206 | };
207 | })((props) => {
208 | return {props.x};
209 | });
210 | return
211 | };
212 |
213 | ReactDOM.render(, container);
214 |
215 | test.equal(getInnerHtml(container), 'aaa');
216 |
217 | xs[0].set('AAA');
218 | await waitFor(() => {
219 | Tracker.flush({_throwFirstError: true});
220 | }, { container, timeout: 250 });
221 | test.equal(getInnerHtml(container), 'AAA');
222 |
223 | xs[1].set('BBB');
224 | setState({m: 1});
225 | await waitFor(() => {
226 | Tracker.flush({_throwFirstError: true});
227 | }, { container, timeout: 250 });
228 | test.equal(getInnerHtml(container), 'BBB');
229 |
230 | setState({m: 2});
231 | await waitFor(() => {
232 | Tracker.flush({_throwFirstError: true});
233 | }, { container, timeout: 250 });
234 | test.equal(getInnerHtml(container), 'ccc');
235 | xs[2].set('CCC');
236 | await waitFor(() => {
237 | Tracker.flush({_throwFirstError: true});
238 | }, { container, timeout: 250 });
239 | test.equal(getInnerHtml(container), 'CCC');
240 |
241 | ReactDOM.unmountComponentAtNode(container);
242 |
243 | ReactDOM.render(, container);
244 | setState({m: 0});
245 | test.equal(getInnerHtml(container), 'AAA');
246 |
247 | ReactDOM.unmountComponentAtNode(container);
248 |
249 | completed();
250 | });
251 |
252 | function waitForTracker(func, callback) {
253 | Tracker.autorun(function (c) {
254 | if (func()) {
255 | c.stop();
256 | callback();
257 | }
258 | });
259 | };
260 |
261 | testAsyncMulti('withTracker - resubscribe', [
262 | function (test, expect) {
263 | var self = this;
264 | self.div = document.createElement("DIV");
265 | self.collection = new Mongo.Collection("withTracker-mixin-coll");
266 | self.num = new ReactiveVar(1);
267 | self.someOtherVar = new ReactiveVar('foo');
268 | self.Foo = withTracker(() => {
269 | self.handle =
270 | Meteor.subscribe("withTracker-mixin-sub",
271 | self.num.get());
272 |
273 | return {
274 | v: self.someOtherVar.get(),
275 | docs: self.collection.find().fetch()
276 | };
277 | })((props) => {
278 | self.data = props;
279 | return {
280 | _.map(props.docs, (doc) => {doc._id})
281 | }
;
282 | });
283 |
284 | self.component = ReactDOM.render(, self.div);
285 | test.equal(getInnerHtml(self.div), '');
286 |
287 | var handle = self.handle;
288 | test.isFalse(handle.ready());
289 |
290 | waitForTracker(() => handle.ready(),
291 | expect());
292 | },
293 | function (test, expect) {
294 | var self = this;
295 | test.isTrue(self.handle.ready());
296 | test.equal(getInnerHtml(self.div), 'id1
');
297 |
298 | self.someOtherVar.set('bar');
299 | self.oldHandle1 = self.handle;
300 |
301 | // can't call Tracker.flush() here (we are in a Tracker.flush already)
302 | Tracker.afterFlush(expect());
303 | },
304 | function (test, expect) {
305 | var self = this;
306 | var oldHandle = self.oldHandle1;
307 | var newHandle = self.handle;
308 | test.notEqual(oldHandle, newHandle); // new handle
309 | test.equal(newHandle.subscriptionId, oldHandle.subscriptionId); // same sub
310 | test.isTrue(newHandle.ready()); // doesn't become unready
311 | // no change to the content
312 | test.equal(getInnerHtml(self.div), 'id1
');
313 |
314 | // ok, now change the `num` argument to the subscription
315 | self.num.set(2);
316 | self.oldHandle2 = newHandle;
317 | Tracker.afterFlush(expect());
318 | },
319 | function (test, expect) {
320 | var self = this;
321 | // data is still there
322 | test.equal(getInnerHtml(self.div), 'id1
');
323 | // handle is no longer ready
324 | var handle = self.handle;
325 | test.isFalse(handle.ready());
326 | // different sub ID
327 | test.isTrue(self.oldHandle2.subscriptionId);
328 | test.isTrue(handle.subscriptionId);
329 | test.notEqual(handle.subscriptionId, self.oldHandle2.subscriptionId);
330 |
331 | waitForTracker(() => handle.ready(),
332 | expect());
333 | },
334 | function (test, expect) {
335 | var self = this;
336 | // now we see the new data! (and maybe the old data, because
337 | // when a subscription goes away, its data doesn't disappear right
338 | // away; the server has to tell the client which documents or which
339 | // properties to remove, and this is not easy to wait for either; see
340 | // https://github.com/meteor/meteor/issues/2440)
341 | test.equal(getInnerHtml(self.div).replace('id1', ''),
342 | 'id2
');
343 |
344 | self.someOtherVar.set('baz');
345 | self.oldHandle3 = self.handle;
346 |
347 | Tracker.afterFlush(expect());
348 | },
349 | function (test, expect) {
350 | var self = this;
351 | test.equal(self.data.v, 'baz');
352 | test.notEqual(self.oldHandle3, self.handle);
353 | test.equal(self.oldHandle3.subscriptionId,
354 | self.handle.subscriptionId);
355 | test.isTrue(self.handle.ready());
356 | },
357 | function (test, expect) {
358 | ReactDOM.unmountComponentAtNode(this.div);
359 | // break out of flush time, so we don't call the test's
360 | // onComplete from within Tracker.flush
361 | Meteor.defer(expect());
362 | }
363 | ]);
364 |
365 | // Tinytest.add(
366 | // "withTracker - print warning if return cursor from withTracker",
367 | // function (test) {
368 | // var coll = new Mongo.Collection(null);
369 | // var ComponentWithCursor = () => {
370 | // withTracker(() => {
371 | // return {
372 | // theCursor: coll.find()
373 | // };
374 | // });
375 | // return ;
376 | // };
377 |
378 | // // Check if we print a warning to console about props
379 | // // You can be sure this test is correct because we have an identical one in
380 | // // react-runtime-dev
381 | // let warning;
382 | // try {
383 | // var oldWarn = console.warn;
384 | // console.warn = function specialWarn(message) {
385 | // warning = message;
386 | // };
387 |
388 | // var div = document.createElement("DIV");
389 | // ReactDOM.render(, div);
390 |
391 | // test.matches(warning, /cursor before returning it/);
392 | // } finally {
393 | // console.warn = oldWarn;
394 | // }
395 | // });
396 |
397 | } else {
398 | Meteor.publish("withTracker-mixin-sub", function (num) {
399 | Meteor.defer(() => { // because subs are blocking
400 | this.added("withTracker-mixin-coll", 'id'+num, {});
401 | this.ready();
402 | });
403 | });
404 | }
405 |
--------------------------------------------------------------------------------
/packages/react-meteor-data/withTracker.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, memo } from 'react';
2 | import { useTracker } from './useTracker';
3 |
4 | type ReactiveFn = (props: object) => any;
5 | type ReactiveOptions = {
6 | getMeteorData: ReactiveFn;
7 | pure?: boolean;
8 | skipUpdate?: (prev: any, next: any) => boolean;
9 | }
10 |
11 | export const withTracker = (options: ReactiveFn | ReactiveOptions) => {
12 | return (Component: React.ComponentType) => {
13 | const getMeteorData = typeof options === 'function'
14 | ? options
15 | : options.getMeteorData;
16 |
17 | const WithTracker = forwardRef((props, ref) => {
18 | const data = useTracker(
19 | () => getMeteorData(props) || {},
20 | (options as ReactiveOptions).skipUpdate
21 | );
22 | return (
23 |
24 | );
25 | });
26 |
27 | const { pure = true } = options as ReactiveOptions;
28 | return pure ? memo(WithTracker) : WithTracker;
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/packages/react-template-helper/.versions:
--------------------------------------------------------------------------------
1 | babel-compiler@7.11.0
2 | babel-runtime@1.5.2
3 | base64@1.0.13
4 | blaze@3.0.0
5 | blaze-tools@2.0.0
6 | caching-compiler@2.0.0
7 | caching-html-compiler@2.0.0
8 | check@1.4.2
9 | core-runtime@1.0.0
10 | diff-sequence@1.1.3
11 | dynamic-import@0.7.4
12 | ecmascript@0.16.9
13 | ecmascript-runtime@0.8.2
14 | ecmascript-runtime-client@0.12.2
15 | ecmascript-runtime-server@0.11.1
16 | ejson@1.1.4
17 | fetch@0.1.5
18 | html-tools@2.0.0
19 | htmljs@2.0.1
20 | inter-process-messaging@0.1.2
21 | meteor@2.0.1
22 | modern-browsers@0.1.11
23 | modules@0.20.1
24 | modules-runtime@0.13.2
25 | mongo-id@1.0.9
26 | observe-sequence@2.0.0
27 | ordered-dict@1.2.0
28 | promise@1.0.0
29 | random@1.2.2
30 | react-fast-refresh@0.2.9
31 | react-template-helper@0.3.0
32 | reactive-var@1.0.13
33 | spacebars@2.0.0
34 | spacebars-compiler@2.0.0
35 | templating@1.4.4
36 | templating-compiler@2.0.0
37 | templating-runtime@2.0.0
38 | templating-tools@2.0.0
39 | tmeasday:check-npm-versions@2.0.0
40 | tracker@1.3.4
41 | typescript@5.4.3
42 | underscore@1.6.4
43 | zodern:types@1.0.13
44 |
--------------------------------------------------------------------------------
/packages/react-template-helper/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## v0.2.17, 2023-05-31
4 | * Fixes React Components not being unmounted on Blaze Template destroy when using React 18+
5 | *
6 |
--------------------------------------------------------------------------------
/packages/react-template-helper/README.md:
--------------------------------------------------------------------------------
1 | Lets you easily include React components in Meteor templates. Pass the
2 | component class through the `component` argument.
3 |
4 | Examples:
5 |
6 | ```html
7 |
8 |
9 | {{> React component=dropdown options=options value=selected onChange=onChange}}
10 |
11 |
12 | ```
13 |
14 | ```js
15 | Template.Dropdown.onCreated(function () {
16 | this.state = new ReactiveDict;
17 | this.state.set("selected", null);
18 | });
19 |
20 | Template.Dropdown.helpers({
21 | dropdown: function () {
22 | // Assuming this is https://github.com/fraserxu/react-dropdown, loaded
23 | // elsewhere in the project.
24 | return Dropdown;
25 | },
26 | options: [
27 | { value: 'one', label: 'One' },
28 | { value: 'two', label: 'Two' },
29 | {
30 | type: 'group', name: 'group1', items: [
31 | { value: 'three', label: 'Three' },
32 | { value: 'four', label: 'Four' }
33 | ]
34 | }
35 | ],
36 | selected: function () {
37 | return Template.instance().state.get("selected");
38 | },
39 | onChange: function () {
40 | var tmpl = Template.instance();
41 | return function (option) {
42 | tmpl.state.set("selected", option);
43 | }
44 | }
45 | });
46 | ```
47 |
48 | Check out [the React article](http://guide.meteor.com/react.html) in the Meteor Guide to learn how to use Meteor and React together to build awesome apps.
49 |
50 | Check the [Changelog](./CHANGELOG.md).
51 |
--------------------------------------------------------------------------------
/packages/react-template-helper/package.js:
--------------------------------------------------------------------------------
1 | Package.describe({
2 | name: 'react-template-helper',
3 | version: '0.3.0',
4 | // Brief, one-line summary of the package.
5 | summary: 'Use React components in native Meteor templates',
6 | // URL to the Git repository containing the source code for this package.
7 | git: 'https://github.com/meteor/react-packages',
8 | // By default, Meteor will default to using README.md for documentation.
9 | // To avoid submitting documentation, set this field to null.
10 | documentation: 'README.md',
11 | });
12 |
13 | Package.onUse((api) => {
14 | api.versionsFrom(['1.3', '2.3', '3.0']);
15 |
16 | api.use([
17 | 'templating',
18 | 'underscore',
19 | 'ecmascript',
20 | 'tmeasday:check-npm-versions@2.0.0',
21 | ]);
22 |
23 | api.addFiles(['react-template-helper.js'], 'client');
24 | });
25 |
--------------------------------------------------------------------------------
/packages/react-template-helper/react-template-helper.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/packages/react-template-helper/react-template-helper.html
--------------------------------------------------------------------------------
/packages/react-template-helper/react-template-helper.js:
--------------------------------------------------------------------------------
1 | import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';
2 | checkNpmVersions({
3 | 'react': '15.3 - 18',
4 | 'react-dom': '15.3 - 18'
5 | }, 'react-template-helper');
6 |
7 | const React = require('react');
8 | const ReactDOM = require('react-dom');
9 | const shouldUseNewDOMRenderSyntax = React.version >= '18';
10 |
11 | // Empty template; logic in `onRendered` below
12 | Template.React = new Template("Template.React", function () { return []; });
13 |
14 | Template.React.onRendered(function () {
15 | var parentTemplate = parentTemplateName();
16 | var container = this.firstNode.parentNode;
17 | this.container = container;
18 |
19 | this.autorun(function (c) {
20 | var data = Blaze.getData();
21 |
22 | var comp = data && data.component;
23 | if (! comp) {
24 | throw new Error(
25 | "In template \"" + parentTemplate + "\", call to `{{> React ... }}` missing " +
26 | "`component` argument.");
27 | }
28 |
29 | var props = _.omit(data, 'component');
30 | var node = React.createElement(comp, props);
31 | if (shouldUseNewDOMRenderSyntax) {
32 | // pseudo-validation
33 | if (!this.root) {
34 | this.root = require('react-dom/client').createRoot(container);
35 | }
36 | this.root.render(node);
37 | return;
38 | }
39 |
40 | ReactDOM.render(node, container);
41 | });
42 | });
43 |
44 | Template.React.onDestroyed(function () {
45 | if (this.container) {
46 | if (shouldUseNewDOMRenderSyntax) {
47 | // React root is created inside the BlazeView, not TemplateInstance
48 | // Keeping the first this.root just in case somebody monkey-patched it
49 | // but it should be undefined here
50 | var reactRoot = this.root || (this.view && this.view.root);
51 |
52 | if (reactRoot) {
53 | reactRoot.unmount();
54 | }
55 | } else {
56 | ReactDOM.unmountComponentAtNode(this.container);
57 | }
58 | }
59 | });
60 |
61 | // Gets the name of the template inside of which this instance of `{{>
62 | // React ...}}` is being used. Used to print more explicit error messages
63 | function parentTemplateName () {
64 | var view = Blaze.getView();
65 | if (!view || view.name !== "Template.React")
66 | throw new Error("Unexpected: called outside of Template.React");
67 |
68 | // find the first parent view which is a template or body
69 | view = view.parentView;
70 | while (view) {
71 | var m;
72 | // check `view.name.match(/^Template\./)` because iron-router (and
73 | // maybe other packages) create a view named "yield" that has the
74 | // `template` property set
75 | if (view.template && view.name && (m = view.name.match(/^Template\.(.*)/))) {
76 | return m[1];
77 | } else if (view.name === "body") {
78 | return "";
79 | }
80 |
81 | view = view.parentView;
82 | }
83 |
84 | // not sure when this could happen
85 | return "";
86 | };
87 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.4.0-remove-old-dev-bundle-link
15 | 1.4.1-add-shell-server-package
16 | 1.4.3-split-account-service-packages
17 | 1.5-add-dynamic-import-package
18 | 1.7-split-underscore-from-meteor-base
19 | 1.8.3-split-jquery-from-blaze
20 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | xi4ob9zualf.vxwytungbgj
8 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base@1.5.2 # Packages every Meteor app needs to have
8 | mobile-experience@1.1.2 # Packages for a great mobile UX
9 | mongo@2.1.0 # The database Meteor supports right now
10 | static-html@1.4.0 # Define static page content in .html files
11 | reactive-var@1.0.13 # Reactive variable for tracker
12 | tracker@1.3.4 # Meteor's client-side reactive programming library
13 |
14 | standard-minifier-css@1.9.3 # CSS minifier run for production mode
15 | standard-minifier-js@3.0.0 # JS minifier run for production mode
16 | es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers
17 | ecmascript@0.16.10 # Enable ECMAScript2015+ syntax in app code
18 | typescript@5.6.3 # Enable TypeScript syntax in .ts and .tsx modules
19 | shell-server@0.6.1 # Server-side component of the `meteor shell` command
20 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@3.1.2
2 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/.meteor/versions:
--------------------------------------------------------------------------------
1 | allow-deny@2.1.0
2 | autoupdate@2.0.0
3 | babel-compiler@7.11.3
4 | babel-runtime@1.5.2
5 | base64@1.0.13
6 | binary-heap@1.0.12
7 | boilerplate-generator@2.0.0
8 | caching-compiler@2.0.1
9 | callback-hook@1.6.0
10 | check@1.4.4
11 | core-runtime@1.0.0
12 | ddp@1.4.2
13 | ddp-client@3.1.0
14 | ddp-common@1.4.4
15 | ddp-server@3.1.0
16 | diff-sequence@1.1.3
17 | dynamic-import@0.7.4
18 | ecmascript@0.16.10
19 | ecmascript-runtime@0.8.3
20 | ecmascript-runtime-client@0.12.2
21 | ecmascript-runtime-server@0.11.1
22 | ejson@1.1.4
23 | es5-shim@4.8.1
24 | facts-base@1.0.2
25 | fetch@0.1.5
26 | geojson-utils@1.0.12
27 | hot-code-push@1.0.5
28 | id-map@1.2.0
29 | inter-process-messaging@0.1.2
30 | launch-screen@2.0.1
31 | logging@1.3.5
32 | meteor@2.1.0
33 | meteor-base@1.5.2
34 | minifier-css@2.0.0
35 | minifier-js@3.0.1
36 | minimongo@2.0.2
37 | mobile-experience@1.1.2
38 | mobile-status-bar@1.1.1
39 | modern-browsers@0.2.0
40 | modules@0.20.3
41 | modules-runtime@0.13.2
42 | mongo@2.1.0
43 | mongo-decimal@0.2.0
44 | mongo-dev-server@1.1.1
45 | mongo-id@1.0.9
46 | npm-mongo@6.10.2
47 | ordered-dict@1.2.0
48 | promise@1.0.0
49 | random@1.2.2
50 | react-fast-refresh@0.2.9
51 | reactive-var@1.0.13
52 | reload@1.3.2
53 | retry@1.1.1
54 | routepolicy@1.1.2
55 | shell-server@0.6.1
56 | socket-stream-client@0.6.0
57 | standard-minifier-css@1.9.3
58 | standard-minifier-js@3.0.0
59 | static-html@1.4.0
60 | static-html-tools@1.0.0
61 | tracker@1.3.4
62 | typescript@5.6.3
63 | webapp@2.0.5
64 | webapp-hashing@1.1.2
65 |
--------------------------------------------------------------------------------
/tests/react-meteor-accounts-harness/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-meteor-accounts-harness",
3 | "private": true,
4 | "scripts": {
5 | "test": "METEOR_PACKAGE_DIRS=../../packages meteor test-packages react-meteor-accounts --driver-package test-in-browser",
6 | "test:ci": "METEOR_PACKAGE_DIRS=../../packages meteor npx mtest --package react-meteor-accounts --once"
7 | },
8 | "dependencies": {
9 | "@babel/runtime": "^7.26.10",
10 | "@testing-library/react": "16.2.0",
11 | "@testing-library/react-hooks": "^7.0.2",
12 | "meteor-node-stubs": "^1.2.13",
13 | "react": "18.3.1",
14 | "react-dom": "18.3.1"
15 | },
16 | "devDependencies": {
17 | "puppeteer": "^24.4.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.4.0-remove-old-dev-bundle-link
15 | 1.4.1-add-shell-server-package
16 | 1.4.3-split-account-service-packages
17 | 1.5-add-dynamic-import-package
18 | 1.7-split-underscore-from-meteor-base
19 | 1.8.3-split-jquery-from-blaze
20 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | xi4ob9zualf.vxwytungbgj
8 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base@1.5.2 # Packages every Meteor app needs to have
8 | mobile-experience@1.1.2 # Packages for a great mobile UX
9 | mongo@2.1.0 # The database Meteor supports right now
10 | static-html@1.4.0 # Define static page content in .html files
11 | reactive-var@1.0.13 # Reactive variable for tracker
12 | tracker@1.3.4 # Meteor's client-side reactive programming library
13 |
14 | standard-minifier-css@1.9.3 # CSS minifier run for production mode
15 | standard-minifier-js@3.0.0 # JS minifier run for production mode
16 | es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers
17 | ecmascript@0.16.10 # Enable ECMAScript2015+ syntax in app code
18 | typescript@5.6.3 # Enable TypeScript syntax in .ts and .tsx modules
19 | shell-server@0.6.1 # Server-side component of the `meteor shell` command
20 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@3.1.2
2 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/.meteor/versions:
--------------------------------------------------------------------------------
1 | allow-deny@2.1.0
2 | autoupdate@2.0.0
3 | babel-compiler@7.11.3
4 | babel-runtime@1.5.2
5 | base64@1.0.13
6 | binary-heap@1.0.12
7 | boilerplate-generator@2.0.0
8 | caching-compiler@2.0.1
9 | callback-hook@1.6.0
10 | check@1.4.4
11 | core-runtime@1.0.0
12 | ddp@1.4.2
13 | ddp-client@3.1.0
14 | ddp-common@1.4.4
15 | ddp-server@3.1.0
16 | diff-sequence@1.1.3
17 | dynamic-import@0.7.4
18 | ecmascript@0.16.10
19 | ecmascript-runtime@0.8.3
20 | ecmascript-runtime-client@0.12.2
21 | ecmascript-runtime-server@0.11.1
22 | ejson@1.1.4
23 | es5-shim@4.8.1
24 | facts-base@1.0.2
25 | fetch@0.1.5
26 | geojson-utils@1.0.12
27 | hot-code-push@1.0.5
28 | id-map@1.2.0
29 | inter-process-messaging@0.1.2
30 | launch-screen@2.0.1
31 | logging@1.3.5
32 | meteor@2.1.0
33 | meteor-base@1.5.2
34 | minifier-css@2.0.0
35 | minifier-js@3.0.1
36 | minimongo@2.0.2
37 | mobile-experience@1.1.2
38 | mobile-status-bar@1.1.1
39 | modern-browsers@0.2.0
40 | modules@0.20.3
41 | modules-runtime@0.13.2
42 | mongo@2.1.0
43 | mongo-decimal@0.2.0
44 | mongo-dev-server@1.1.1
45 | mongo-id@1.0.9
46 | npm-mongo@6.10.2
47 | ordered-dict@1.2.0
48 | promise@1.0.0
49 | random@1.2.2
50 | react-fast-refresh@0.2.9
51 | reactive-var@1.0.13
52 | reload@1.3.2
53 | retry@1.1.1
54 | routepolicy@1.1.2
55 | shell-server@0.6.1
56 | socket-stream-client@0.6.0
57 | standard-minifier-css@1.9.3
58 | standard-minifier-js@3.0.0
59 | static-html@1.4.0
60 | static-html-tools@1.0.0
61 | tracker@1.3.4
62 | typescript@5.6.3
63 | webapp@2.0.5
64 | webapp-hashing@1.1.2
65 |
--------------------------------------------------------------------------------
/tests/react-meteor-data-harness/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "harness",
3 | "private": true,
4 | "scripts": {
5 | "test": "METEOR_PACKAGE_DIRS=../../packages meteor test-packages react-meteor-data --driver-package test-in-browser",
6 | "test:ci": "METEOR_PACKAGE_DIRS=../../packages meteor npx mtest --package react-meteor-data --once"
7 | },
8 | "dependencies": {
9 | "@babel/runtime": "^7.26.10",
10 | "@testing-library/react": "16.2.0",
11 | "meteor-node-stubs": "^1.2.13",
12 | "react": "18.3.1",
13 | "react-dom": "18.3.1",
14 | "react-test-renderer": "18.3.1"
15 | },
16 | "devDependencies": {
17 | "puppeteer": "^24.4.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "commonjs", /* Specify what module code is generated. */
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "resolveJsonModule": true, /* Enable importing .json files. */
39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
40 |
41 | /* JavaScript Support */
42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45 |
46 | /* Emit */
47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52 | // "outDir": "./", /* Specify an output folder for all emitted files. */
53 | // "removeComments": true, /* Disable emitting comments. */
54 | // "noEmit": true, /* Disable emitting files from a compilation. */
55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 |
71 | /* Interop Constraints */
72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 |
78 | /* Type Checking */
79 | "strict": true, /* Enable all strict type-checking options. */
80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98 |
99 | /* Completeness */
100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 | }
103 | }
104 |
--------------------------------------------------------------------------------