77 | )
78 | }
79 | }
80 |
81 | /* React
82 | Render into main.html, target the div with id="root". React can live next to other elements inside the main html
83 | document, i.e. legacy templates/engines.
84 | */
85 | Meteor.startup(function () {
86 | ReactDOM.render(, document.getElementById('root'));
87 | });
88 |
89 |
90 | /***********************************************************************************************************************
91 | *
92 | * [CollectionShowcase]
93 | * This component uses TrackerReact to provide real-time data from the "tasks" Meteor Collection.
94 | * Make sure collections are also instantiated on the server, published and subscribed on the client.
95 | *
96 | **********************************************************************************************************************/
97 |
98 | /* Meteor
99 | Install TrackerReact via "meteor add ultimatejs:tracker-react" and import the default composition helper
100 | "TrackerReact" via Meteors module system (meteor/author:package-name).
101 | */
102 | import TrackerReact from 'meteor/ultimatejs:tracker-react';
103 |
104 | /* Meteor
105 | Initialise the "tasks" collection on the client. The same is done on the server. See
106 | "/server/dataPublications.js".
107 | */
108 | Tasks = new Mongo.Collection("tasks");
109 |
110 | /* Meteor, React -> TrackerReact
111 | In order to have react re-render on data invalidation and update our component, we need to compose it with
112 | TrackerReact. If we would not do so, everything would still work fine but updates are only shown on page/component
113 | re-load. -> "TrackerReact(React.Component)"
114 |
115 | Profiler: Set {profiler: false} to turn profile logs off. If not used, this second argument can be omitted.
116 | */
117 | class CollectionShowcase extends TrackerReact(React.Component) {
118 |
119 | /* Meteor, React
120 | In this example, the Meteor data is only used inside this react component. Therefore, no active data subscription
121 | exists up on rendering this component. To make sure we have all the necessary data up on rendering, we subscribe
122 | to our data when the component is about to render.
123 |
124 | The "constructor()" method is called before rendering and therefore a good place to start our subscription. We
125 | could also subscribe outside of the react life cycle, i.e., simply at the beginning of the page.
126 | */
127 | constructor() {
128 | super();
129 | /* React
130 | Data subscription(s) define our state (availability of data), so it should also be assigned to it (return object).
131 | */
132 | this.state = {
133 | subscription: {
134 | tasks: Meteor.subscribe('tasks')
135 | }
136 | }
137 | }
138 |
139 | /* Meteor, React
140 | Since we are not planning to use this data anywhere else, we will stop the subscription when the component unmounts.
141 | */
142 | //noinspection JSUnusedGlobalSymbols
143 | componentWillUnmount() {
144 | this.state.subscription.tasks.stop();
145 | }
146 |
147 | /* Meteor, React
148 | As mentioned before, data subscriptions define state. Therefore we can toggle subscriptions and assign new ones.
149 | This method could also be made static'aly available and passed around for generic subscription management. However,
150 | within the same component tree, a state-handler should be passed around instead.
151 | */
152 | toggleSubscription(publication) {
153 | let subscription = this.state.subscription[publication];
154 |
155 | if (subscription.ready()) {
156 | subscription.stop()
157 | } else {
158 | this.setState({subscription: {tasks: Meteor.subscribe(publication)}})
159 | }
160 | }
161 |
162 | /* Meteor, React
163 | An object array is simply returned from our collection. With TrackerReact, any changes to the underlying will lead
164 | to an automatic component re-render with a new return value -> virtual dom diffing -> dom update.
165 | */
166 | //noinspection JSMethodCanBeStatic
167 | tasks() {
168 | return Tasks.find().fetch().reverse();
169 | }
170 |
171 | handleTasksInsert(e) {
172 | e.preventDefault();
173 |
174 | /* Meteor
175 | Client-side inserts - due to our allowed auth settings - will propagate from the client, to the server
176 | and to other clients automatically.
177 | */
178 | Tasks.insert({
179 | title: this.refs["todoTitle"].value || "No Title",
180 | text: this.refs["todoText"].value || "No Message Text"
181 | });
182 | }
183 |
184 | render() {
185 | return (
186 |
187 |
188 |
Reactive Data
189 |
Subscribe to Meteor Collections and
190 | see real-time updates. Open this
191 | page in another browser window and save a new todo message to the "todos" collection.
207 | )
208 | }
209 | }
210 |
211 | /* React
212 | A generic react component can be simply used within a TrackerReact component.
213 | */
214 | class Task extends React.Component {
215 | render() {
216 | return (
217 |
218 |
{this.props.task.title}
219 |
{this.props.task.text}
220 |
221 | )
222 | }
223 | }
224 |
225 |
226 | /***********************************************************************************************************************
227 | *
228 | * [MethodShowcase]
229 | * This component uses Meteor Methods to invoke collection changes on the server side. Changes are
230 | * optimistically applied on the client until the server either confirms or discards the change (i.e. due to missing
231 | * authentication). Here for, the method needs to be both defined on the server and the client (stub simulation).
232 | *
233 | **********************************************************************************************************************/
234 |
235 | /* Meteor
236 | The relevant Methods and the "temperature" collection is defined in this file. It is in an external file, so that it
237 | can be easily imported on the server as well. See "/server/dataPublications.js".
238 | */
239 | import "/imports/dataTemperature";
240 |
241 | /* React
242 | We use a simple react component from npm to illustrate reactivity and optimistic updates.
243 | Source: https://www.npmjs.com/package/react-thermometer
244 | */
245 | import Thermometer from "react-thermometer";
246 |
247 | /* Meteor, React -> TrackerReact
248 | Here the mixin method is used (scroll to the end of the file). Real-time data-invalidation due to optimistic updates.
249 | -> "ReactMixin(MethodShowcase.prototype, TrackerReactMixin);"
250 | */
251 | import ReactMixin from 'react-mixin';
252 | import {TrackerReactMixin} from 'meteor/ultimatejs:tracker-react';
253 |
254 | class MethodShowcase extends React.Component {
255 |
256 | /* Meteor, React
257 | Same reasoning as before. See above [CollectionShowcase].
258 | */
259 | constructor() {
260 | super();
261 |
262 | this.state = {
263 | subscription: {
264 | temperature: Meteor.subscribe('temperature')
265 | }
266 | }
267 | }
268 |
269 | /* Meteor, React
270 | Same reasoning as before. See above [CollectionShowcase].
271 | */
272 | //noinspection JSUnusedGlobalSymbols
273 | componentWillUnmount() {
274 | this.state.subscription.temperature.stop();
275 | }
276 |
277 | /* Meteor
278 | FindOne returns the last document object added to the "temperature" collection. Note that we are returning a
279 | loading stub for when the data is not loaded yet. With TrackerReact, loading indicators can be implicit to data
280 | availability and is not limited to whole subscriptions.
281 | */
282 | //noinspection JSMethodCanBeStatic
283 | temperature() {
284 | return Temperature.findOne({}, {sort: {created: -1}}) || {current: 0, source: "loading"};
285 | }
286 |
287 | /* Meteor
288 | Before we inserted data directly into a client side collection which propagated itself to the server. Here, a
289 | Meteor Method is "called" to trigger an insert on the server. But the method is also available on the client,
290 | allowing for an optimistic update of data.
291 |
292 | In the end, the data inserted on the server slightly diverts with a different "source" property. As soon the
293 | server caught up, the optimistic data of the client is corrected by the data on the server (source changed from
294 | client to server). Despite any network latency.
295 |
296 | See "/server/dataPublications.js".
297 | */
298 | handleTemperatureReading(e) {
299 | e.preventDefault();
300 |
301 | let reading = this.refs["reading"].value;
302 | let delay = this.refs["delay"].value;
303 |
304 | if (reading >= 0 && reading <= 30) {
305 | Meteor.call("addMeasure", reading, delay);
306 | }
307 | }
308 |
309 | /* Meteor
310 | A Meteor Method that only runs on the server, without optimistic updates, changing the last reading
311 | on the server's mongo collection.
312 |
313 | See "/server/dataPublications.js".
314 | */
315 | //noinspection JSMethodCanBeStatic
316 | changeTemperature(amount) {
317 | Meteor.call("changeMeasure", amount);
318 | }
319 |
320 | render() {
321 | return (
322 |
323 |
324 |
Optimistic Updates
325 |
Make use of Meteor Methods. Simulate
326 | different network latencies below and observe how
327 | data changes are first applied client side, than confirmed server side.
328 |
329 |
336 |
337 |
346 |
347 |
{this.temperature().current + "° Celsius"}
348 |
{"( " + this.temperature().source + " )"}
349 |
350 | )
351 | }
352 | }
353 |
354 | ReactMixin(MethodShowcase.prototype, TrackerReactMixin);
355 |
--------------------------------------------------------------------------------
/client/styles.js:
--------------------------------------------------------------------------------
1 | export default style = {
2 | heading: {
3 | fontStyle: "italic",
4 | fontSize: "30px",
5 | textAlign: "center",
6 | color: "#fff",
7 | marginBottom: "40px"
8 | },
9 | subHeading: {
10 | fontSize: "14px",
11 | fontWeight: "normal",
12 | textAlign: "center",
13 | color: "#fff",
14 | padding: "0 20%",
15 | marginBottom: "10px"
16 | },
17 | install: {
18 | textAlign: "center",
19 | marginBottom: "20px",
20 | color: "white",
21 | repo: {
22 | fontVariant: "normal",
23 | fontStyle: "normal",
24 | textDecoration: "none",
25 | color: "white"
26 | },
27 | bash: {
28 | backgroundColor: "#58A6DB",
29 | display: "inline",
30 | padding: "8px 15px",
31 | borderRadius: "5px",
32 | fontFamily: "monospace",
33 | border: "solid 1px #2096CD",
34 | whiteSpace: "nowrap",
35 | lineHeight: "45px"
36 | }
37 | },
38 | content: {
39 | background: "#fff",
40 | padding: "35px 20px 25px 20px",
41 | heading: {
42 | color: "#565656",
43 | margin: "0 0 1rem 0"
44 | },
45 | intro: {
46 | marginBottom: "1rem",
47 | color: "#747474",
48 | lineHeight: "1.2rem",
49 | textAlign: "justify"
50 | }
51 | },
52 | method: {
53 | thermometer: {
54 | padding: "20px 0 20px 50px",
55 | float: "left"
56 | },
57 | label: {
58 | textAlign: "center",
59 | paddingTop: "35%",
60 | fontSize: "50px",
61 | fontWeight: "bold",
62 | color: "lightgrey"
63 | },
64 | subLabel: {
65 | textAlign: "center",
66 | paddingTop: "1rem",
67 | fontSize: "25px",
68 | fontWeight: "bold",
69 | color: "lightgrey"
70 | }
71 | },
72 | collection: {
73 | olList: {
74 | paddingLeft: "40px",
75 | maxHeight: "380px",
76 | overflow: "scroll"
77 | }
78 | },
79 | input: {
80 | width: "100%",
81 | boxSizing: "border-box",
82 | padding: "10px",
83 | border: "solid 1px #CCC",
84 | outline: "none",
85 | marginBottom: "1rem"
86 | },
87 | button: {
88 | padding: "10px 20px",
89 | background: "#2096cd",
90 | color: "#FFF",
91 | border: "none",
92 | outline: "none",
93 | cursor: "pointer",
94 | marginRight: "20px",
95 | marginBottom: "1rem",
96 | last: function() {
97 | let last = Object.assign({}, this);
98 | last.marginRight = "none";
99 | return last;
100 | },
101 | inverted: function() {
102 | let last = Object.assign({}, this);
103 | last.background = "#58A6DB";
104 | return last;
105 | }
106 | },
107 | todo: {
108 | listStyleType: "decimal",
109 | marginBottom: "1rem",
110 | title: {
111 | fontWeight: "bold",
112 | borderBottom: "lightgray 1px solid",
113 | marginBottom: "3px"
114 | },
115 | text: {
116 | fontSize: "11px"
117 | }
118 | }
119 | };
120 |
--------------------------------------------------------------------------------
/imports/dataTemperature.js:
--------------------------------------------------------------------------------
1 | /* This code is not eagerly loaded (needs import) */
2 |
3 | /***********************************************************************************************************************
4 | *
5 | * [Temperature] Collection & Methods
6 | * Instantiate Mongo Collection and declare Meteor Methods to be used on the server and the client for controlled,
7 | * optimistic updates.
8 | *
9 | **********************************************************************************************************************/
10 | Temperature = new Mongo.Collection("temperature");
11 |
12 | /* Meteor
13 | On fresh install, when there are no readings available, insert one fixture on the server.
14 | */
15 | if (Meteor.isServer && Temperature.find().count() <= 0) {
16 | Temperature.insert({current: 21, created: new Date(), source: "fixture"});
17 | }
18 |
19 | /*
20 | All methods are available to the client and the server. However, within the method blocks functionality can also be
21 | separated.
22 | */
23 | Meteor.methods({
24 | addMeasure (reading = 10, delay = 3) {
25 |
26 | delay = delay > 0 ? delay : 3;
27 | reading = reading <= 30 ? reading : 30;
28 |
29 | let measure = {
30 | current: reading,
31 | created: new Date()
32 | };
33 |
34 | /*
35 | On the server, we insert the "source" property as "from Server" and pretend to have a network latency defined by
36 | the delay argument of the method call.
37 | */
38 | if (Meteor.isServer) {
39 | measure.source = "from Server";
40 | // Only available on the server. Pause execution for delay.
41 | Meteor._sleepForMs(delay * 1000);
42 | } else {
43 | measure.source = "from Client";
44 | }
45 |
46 | /*
47 | The measure is both inserted on the server and optimistically on the client. The server is however always the
48 | source of truth (correcting optimistic updates; here = source "from server").
49 | */
50 | Temperature.insert(measure);
51 | },
52 | changeMeasure (amount) {
53 | if (Meteor.isServer) {
54 |
55 | // Get last result
56 | let reading = Temperature.findOne({}, {sort: {created: -1}});
57 |
58 | if (typeof reading === "undefined") return;
59 |
60 | const newTemp = Number(reading.current) + Number(amount);
61 | amount = newTemp <= 30 ? newTemp : 30;
62 |
63 | /*
64 | Based of the original document _id, the record is updated and re-propagated across all clients.
65 | */
66 | Temperature.update({_id: reading._id}, {$set: {current: amount}});
67 |
68 | } else {
69 | /*
70 | The client did not do any optimist update.
71 | */
72 | console.log("A reading correction was sent to the server.")
73 | }
74 | }
75 | });
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "dependencies": {
7 | "history": "^1.17.0",
8 | "react": "^0.14.7",
9 | "react-dom": "^0.14.7",
10 | "react-mixin": "^3.0.3",
11 | "react-router": "^2.0.0-rc5",
12 | "react-thermometer": "0.0.3"
13 | },
14 | "devDependencies": {},
15 | "scripts": {
16 | "test": "echo \"Error: no test specified\" && exit 1"
17 | },
18 | "author": "",
19 | "license": "ISC"
20 | }
21 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## [TrackerReact](https://github.com/ultimatejs/tracker-react) Implementation Example
2 | "No-Config reactive React Components with Meteor"
3 |
4 | An implementation example of React with Meteor 1.3
5 | * clone/fork/download
6 | * `npm install`
7 | * `meteor`
8 |
9 | **Read the source file & comments**: [client/main.jsx](https://github.com/D1no/TrackerReact-Example/blob/master/client/main.jsx)
10 |
11 | *Note*: The source code is thoroughly annotated to showcase the benefit of [TrackerReact](https://github.com/ultimatejs/tracker-react). Please go through the code
12 | to
13 | identify implementation patterns and - in case of mistakes or areas of improvement - provide your feedback.
14 |
15 | ## TrackerReact
16 | Repo: https://github.com/ultimatejs/tracker-react
17 |
18 | Meteor Package
19 |
20 | ```
21 | meteor add ultimatejs:tracker-react
22 | ```
23 |
24 | 
25 |
26 |
27 | ## License
28 | MIT
29 |
--------------------------------------------------------------------------------
/server/dataPublications.js:
--------------------------------------------------------------------------------
1 | /* This code only runs on the SERVER */
2 |
3 | /***********************************************************************************************************************
4 | *
5 | * [Tasks] Collection
6 | * Instantiate Mongo Collection and publish it to the clients.
7 | *
8 | **********************************************************************************************************************/
9 | Tasks = new Mongo.Collection("tasks");
10 |
11 | /* Meteor
12 | The client needs to subscribe to this publication.
13 | */
14 | Meteor.publish('tasks', function () {
15 | return Tasks.find({});
16 | });
17 |
18 | /* Meteor
19 | We allow all clients to make changes directly to the collection. But only the server can remove documents.
20 | Still, any user can update any document. Here are no further auth checks = bad for production. Read up on it online.
21 | */
22 | Tasks.allow({
23 | insert: function (userId, doc) {
24 | return true;
25 | },
26 | update: function (userId, doc, fields, modifier) {
27 | return true;
28 | },
29 | remove: function (userId, doc) {
30 | return false;
31 | }
32 | });
33 |
34 | /***********************************************************************************************************************
35 | *
36 | * [Temperature] Collection
37 | * Import Meteor Methods incl. instantiating the Mongo Collection and publish it to the clients.
38 | *
39 | **********************************************************************************************************************/
40 | import "/imports/dataTemperature";
41 |
42 | /* Meteor
43 | The client needs to subscribe to this publication.
44 | */
45 | Meteor.publish('temperature', function () {
46 | return Temperature.find({});
47 | });
48 |
49 | /* Meteor
50 | We allow all clients to make changes directly to the collection. But only the server can remove documents.
51 | Still, any user can update any document. Here are no further auth checks = bad for production. Read up on it online.
52 | */
53 | Temperature.allow({
54 | insert: function (userId, doc) {
55 | return true;
56 | },
57 | update: function (userId, doc, fields, modifier) {
58 | return true;
59 | },
60 | remove: function (userId, doc) {
61 | return false;
62 | }
63 | });
--------------------------------------------------------------------------------