├── .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 |
40 |
{this.data.selectedPlayer.name}
41 | 47 |
48 | ) 49 | :
Click a player to select
; 50 | }, 51 | render() { 52 | return ( 53 |
54 |
55 |

Leaderboard

56 |
Select a scientist to give them points
57 | 60 | {this.getBottomBar()} 61 |
62 | ) 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/client/components/leaderboard.jsx: -------------------------------------------------------------------------------- 1 | const { 2 | List, 3 | ListItem, 4 | ListDivider, 5 | Avatar 6 | } = mui; 7 | 8 | Leaderboard = React.createClass({ 9 | propTypes: { 10 | selectedPlayerId: React.PropTypes.string, 11 | players: React.PropTypes.array.isRequired, 12 | onPlayerSelected: React.PropTypes.func 13 | }, 14 | selectPlayer(playerId) { 15 | this.props.onPlayerSelected(playerId); 16 | }, 17 | render() { 18 | return ( 19 | 20 | {this.props.players.map((player) => { 21 | let style = {}; 22 | 23 | if (this.props.selectedPlayerId === player._id) { 24 | style['backgroundColor'] = '#eee'; 25 | } 26 | 27 | return [ 28 | } 32 | secondaryText={'Current score: ' + player.score} 33 | style={style}/>, 34 | 35 | ]; 36 | })} 37 | 38 | ); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | Leaderboard 3 | 4 | 5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/client/startup.jsx: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | ReactDOM.render(, document.getElementById("app")); 3 | }); 4 | -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/client/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,700); 2 | 3 | * { 4 | -moz-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; 10 | font-size: 16px; 11 | font-weight: normal; 12 | margin: 3em 0; 13 | padding: 0; 14 | } 15 | 16 | .outer { 17 | margin: 0 auto; 18 | max-width: 480px; 19 | } 20 | 21 | .logo { 22 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGcgaWQ9Imljb21vb24taWdub3JlIj4KCTxsaW5lIHN0cm9rZS13aWR0aD0iMSIgeDE9IiIgeTE9IiIgeDI9IiIgeTI9IiIgc3Ryb2tlPSIjNDQ5RkRCIiBvcGFjaXR5PSIiPjwvbGluZT4KPC9nPgoJPHBhdGggZD0iTTI2LjUyIDI0LjcwNWwtNi41Mi0xMS4yOTN2LTcuODc3aDAuNzVjMC42OSAwIDEuMjUtMC41NiAxLjI1LTEuMjVzLTAuNTYtMS4yNS0xLjI1LTEuMjVoLTkuNWMtMC42OSAwLTEuMjUgMC41Ni0xLjI1IDEuMjVzMC41NiAxLjI1IDEuMjUgMS4yNWgwLjc1djcuODc3bC02LjUyIDExLjI5M2MtMS4zNzUgMi4zODEtMC4yNSA0LjMzIDIuNSA0LjMzaDE2LjA0MGMyLjc1IDAgMy44NzUtMS45NDkgMi41LTQuMzN6TTE0LjUgNS41MzVoM2wtMC4wMDIgOC41NDMgMS45OTYgMy40NTdoLTYuOTljMS4xMjMtMS45NDYgMS45OTYtMy40NTkgMS45OTYtMy40NTl2LTguNTQxek0yNC4wMjAgMjYuNTM1aC0xNi4wNDBjLTAuMjU5IDAtMC40NDMtMC4wMzAtMC41NjEtMC4wNjMgMC4wMzEtMC4xMTkgMC4wOTctMC4yOTQgMC4yMjctMC41MTggMCAwIDEuNzA2LTIuOTU3IDMuNDE2LTUuOTJsOS44NzYtMCAzLjQxOCA1LjkyYzAuMTI5IDAuMjI0IDAuMTk1IDAuMzk5IDAuMjI3IDAuNTE4LTAuMTE4IDAuMDMyLTAuMzAzIDAuMDYzLTAuNTYyIDAuMDYzeiIgZmlsbD0iIzAwMDAwMCI+PC9wYXRoPgo8L3N2Zz4K'); 23 | background-position: center center; 24 | background-repeat: no-repeat; 25 | background-size: contain; 26 | height: 1.5em; 27 | margin: 0 auto .75em; 28 | width: 1.5em; 29 | } 30 | 31 | .title { 32 | font-size: 1.5em; 33 | font-weight: 700; 34 | letter-spacing: .3em; 35 | margin: 0 0 .25em; 36 | text-align: center; 37 | text-indent: .3em; 38 | text-transform: uppercase; 39 | } 40 | 41 | .subtitle { 42 | color: #999; 43 | font-size: .875em; 44 | margin-bottom: 2em; 45 | text-align: center; 46 | } 47 | 48 | .details { 49 | overflow: hidden; 50 | margin-top: 1em; 51 | } 52 | 53 | .details .name { 54 | display: inline-block; 55 | font-size: 1.5em; 56 | font-weight: 300; 57 | line-height: 2.25rem; 58 | padding-left: 1.25rem; 59 | vertical-align: middle; 60 | } 61 | 62 | .details .inc { 63 | border-radius: 3em; 64 | border: #eb5f3a 1px solid; 65 | background: transparent; 66 | color: #eb5f3a; 67 | cursor: pointer; 68 | float: right; 69 | font-family: 'Source Sans Pro' ,'Helvetica Neue', Helvetica, Arial, sans-serif; 70 | font-size: 1rem; 71 | line-height: 1; 72 | margin: 0; 73 | outline: none; 74 | padding: 10px 30px; 75 | transition: all 200ms ease-in; 76 | } 77 | 78 | .inc:hover { 79 | background: #eb5f3a; 80 | color: #fff; 81 | } 82 | 83 | .inc:active { 84 | box-shadow: rgba(0,0,0,.3) 0 1px 3px 0 inset; 85 | } 86 | 87 | .message { 88 | color: #aaa; 89 | line-height: 2.25rem; 90 | text-align: center; 91 | margin-top: 1em; 92 | } 93 | 94 | @media (max-width: 500px) { 95 | .details, .message { 96 | display: block; 97 | position: fixed; 98 | bottom: 0; 99 | background-color: #fafafa; 100 | width: 100%; 101 | padding: 12px 15px; 102 | border-top: 1px solid #ccc; 103 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 104 | } 105 | 106 | .details .name { 107 | font-size: 1.2em; 108 | padding-left: 0; 109 | } 110 | 111 | .details .inc { 112 | padding: 10px 20px; 113 | } 114 | 115 | body { 116 | margin: 2em 0 4em 0; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/lib/collection.js: -------------------------------------------------------------------------------- 1 | Players = new Mongo.Collection('players'); 2 | -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/public/Ada Lovelace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/public/Ada Lovelace.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/public/Carl Friedrich Gauss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/public/Carl Friedrich Gauss.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/public/Claude Shannon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/public/Claude Shannon.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/public/Grace Hopper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/public/Grace Hopper.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/public/Marie Curie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/public/Marie Curie.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/public/Nikola Tesla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/public/Nikola Tesla.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor/react-packages/c1a44cb4b2909aebd5e2026672dd5ec706f809dc/examples/material-ui-leaderboard/screenshot.png -------------------------------------------------------------------------------- /examples/material-ui-leaderboard/server/fixtures.js: -------------------------------------------------------------------------------- 1 | if (Players.find().count() === 0) { 2 | const names = [ 3 | 'Ada Lovelace', 4 | 'Grace Hopper', 5 | 'Marie Curie', 6 | 'Carl Friedrich Gauss', 7 | 'Nikola Tesla', 8 | 'Claude Shannon' 9 | ]; 10 | 11 | for (let i = 0; i < names.length; i++) { 12 | Players.insert({name: names[i], score: Math.floor(Math.random() * 10) * 5}); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/react-in-blaze/.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 | -------------------------------------------------------------------------------- /examples/react-in-blaze/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/react-in-blaze/.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 | 1a57o0q13jqx45zhs9x4 8 | -------------------------------------------------------------------------------- /examples/react-in-blaze/.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 | autopublish 8 | insecure 9 | react 10 | react-template-helper 11 | meteorhacks:npm 12 | cosmos:browserify 13 | twbs:bootstrap 14 | standard-minifiers 15 | meteor-base 16 | mobile-experience 17 | mongo 18 | blaze-html-templates 19 | session 20 | jquery 21 | tracker 22 | logging 23 | reload 24 | random 25 | ejson 26 | spacebars 27 | check 28 | 29 | 30 | npm-container -------------------------------------------------------------------------------- /examples/react-in-blaze/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/react-in-blaze/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/react-in-blaze/.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-html-templates@1.0.1 9 | blaze-tools@1.0.4 10 | boilerplate-generator@1.0.4 11 | caching-compiler@1.0.0 12 | caching-html-compiler@1.0.2 13 | callback-hook@1.0.4 14 | check@1.1.0 15 | coffeescript@1.0.11 16 | cosmos:browserify@0.9.2 17 | ddp@1.2.2 18 | ddp-client@1.2.1 19 | ddp-common@1.2.2 20 | ddp-server@1.2.2 21 | deps@1.0.9 22 | diff-sequence@1.0.1 23 | ecmascript@0.1.6 24 | ecmascript-runtime@0.2.6 25 | ejson@1.0.7 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 | meteor@1.1.10 40 | meteor-base@1.0.1 41 | meteorhacks:async@1.0.0 42 | meteorhacks:npm@1.5.0 43 | minifiers@1.1.7 44 | minimongo@1.0.10 45 | mobile-experience@1.0.1 46 | mobile-status-bar@1.0.6 47 | mongo@1.1.3 48 | mongo-id@1.0.1 49 | npm-container@1.2.0 50 | npm-mongo@1.4.39_1 51 | observe-sequence@1.0.7 52 | ordered-dict@1.0.4 53 | promise@0.5.1 54 | random@1.0.5 55 | react@0.14.3 56 | react-meteor-data@0.2.3 57 | react-runtime@0.14.3 58 | react-runtime-dev@0.14.3 59 | react-runtime-prod@0.14.3 60 | react-template-helper@0.2.3 61 | reactive-dict@1.1.3 62 | reactive-var@1.0.6 63 | reload@1.1.4 64 | retry@1.0.4 65 | routepolicy@1.0.6 66 | session@1.1.1 67 | spacebars@1.0.7 68 | spacebars-compiler@1.0.7 69 | standard-minifiers@1.0.2 70 | templating@1.1.5 71 | templating-tools@1.0.0 72 | tracker@1.0.9 73 | twbs:bootstrap@3.3.6 74 | ui@1.0.8 75 | underscore@1.0.4 76 | url@1.0.5 77 | webapp@1.2.3 78 | webapp-hashing@1.0.5 79 | -------------------------------------------------------------------------------- /examples/react-in-blaze/README.md: -------------------------------------------------------------------------------- 1 | # React in Blaze Example 2 | 3 | This example demonstrates using 3rd-party React components inside a Blaze app. 4 | 5 | ### Using Packages from NPM 6 | 7 | We are using [meteorhacks:npm](https://atmospherejs.com/meteorhacks/npm) and [cosmos:browserify](https://atmospherejs.com/cosmos/browserify) to build and use the `griddle-react` NPM package on the client side. NPM dependencies are declared in [`packages.json`](https://github.com/meteor/react-packages/blob/master/examples/react-in-blaze/packages.json) and required in [`client/lib/app.browserify.js`](https://github.com/meteor/react-packages/blob/master/examples/react-in-blaze/client/lib/app.browserify.js). 8 | 9 | For more details on using client-side NPM packages, see [this guide](http://react-in-meteor.readthedocs.org/en/latest/client-npm/). 10 | 11 | ### Using React Components inside Blaze 12 | 13 | To use React components inside Blaze, you need to first install the `react-template-helper` package. The component also needs to be made available in a helper for the Blaze template using it (see [client/app.js](https://github.com/meteor/react-packages/blob/master/examples/react-in-blaze/client/app.js#L5-L7)). Then you can use it inside your Blaze template using the `React` helper (see [client/app.html](https://github.com/meteor/react-packages/blob/master/examples/react-in-blaze/client/app.html#L15-L21)). 14 | 15 | For more details on using the React template helper, see [this guide](http://react-in-meteor.readthedocs.org/en/latest/react-template-helper/). 16 | -------------------------------------------------------------------------------- /examples/react-in-blaze/client/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | } 4 | 5 | #add-new-player-form { 6 | margin: 30px 0; 7 | } 8 | 9 | .griddle-previous, .griddle-page, .griddle-next{ 10 | float:left; 11 | width:33%; 12 | min-height:1px; 13 | margin-top:5px; 14 | } 15 | 16 | .griddle-page{ 17 | text-align:center; 18 | } 19 | 20 | .griddle-next{ 21 | text-align:right; 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-in-blaze/client/app.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Grid powered by 4 | Griddle 5 |

6 |
7 |
8 | {{#each formColumns}} 9 | 10 | {{/each}} 11 | 12 |
13 |
14 |
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

Log in

; 66 | } 67 | 68 | return

Hello {user.username}

; 69 | } 70 | ``` 71 | 72 | TypeScript signature: 73 | 74 | ```ts 75 | function useUser(): Meteor.User | null; 76 | ``` 77 | 78 | ### useUserId() 79 | 80 | Get a stateful value of the current user id. A hook. Uses [`Meteor.userId`](https://docs.meteor.com/api/accounts.html#Meteor-userId), a reactive data source. 81 | 82 | - Arguments: *none*. 83 | - Returns: `string | null`. 84 | 85 | Example: 86 | 87 | ```tsx 88 | import React from 'react'; 89 | import { useUserId } from 'meteor/react-meteor-accounts'; 90 | 91 | function Foo() { 92 | const userId = useUserId(); 93 | 94 | return ( 95 |
96 |

Account Details

97 | {userId ? ( 98 |

Your unique account id is {userId}.

99 | ) : ( 100 |

Log-in to view your account details.

101 | )} 102 |
103 | ); 104 | } 105 | ``` 106 | 107 | TypeScript signature: 108 | 109 | ```ts 110 | function useUserId(): string | null; 111 | ``` 112 | 113 | ### useLoggingIn() 114 | 115 | Get a stateful value of whether a login method (e.g. `loginWith`) 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 | 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 | --------------------------------------------------------------------------------