v3-docs/docs/tutorials/data-loading/data-loading.md
After reading this guide, you'll know:
In a traditional, HTTP-based web application, the client and server communicate in a "request-response" fashion. Typically the client makes RESTful HTTP requests to the server and receives HTML or JSON data in response, and there's no way for the server to "push" data to the client when changes happen at the backend.
Meteor is built from the ground up on the Distributed Data Protocol (DDP) to allow data transfer in both directions. Instead of setting up REST endpoints, you create publication endpoints that can push data from server to client.
In Meteor a publication is a named API on the server that constructs a set of data to send to a client. A client initiates a subscription which connects to a publication, and receives that data. That data consists of a first batch sent when the subscription is initialized and then incremental updates as the published data changes.
A subscription "bridges" a server-side MongoDB collection and the client-side Minimongo cache of that collection. You can think of a subscription as a pipe that connects a subset of the "real" collection with the client's version, and constantly keeps it up to date with the latest information on the server.
A publication should be defined in a server-only file. For instance, in the Todos example app, we want to publish the set of public lists to all users:
import { Meteor } from 'meteor/meteor';
import { Lists } from '/imports/api/lists/lists';
Meteor.publish('lists.public', function() {
return Lists.find({
userId: { $exists: false }
}, {
fields: Lists.publicFields
});
});
There are a few things to understand about this code block. First, we've named the publication with the unique string lists.public, and that will be how we access it from the client. Second, we are returning a Mongo cursor from the publication function. Note that the cursor is filtered to only return certain fields from the collection.
What that means is that the publication will ensure the set of data matching that query is available to any client that subscribes to it.
Every publication takes two types of parameters:
this context, which has information about the current DDP connection. For example, you can access the current user's _id with this.userId.Meteor.subscribe.::: info
Since we need to access context on this we need to use the function() {} form for publications rather than the ES2015 () => {} arrow function.
:::
Here's a publication that loads private lists for the current user:
Meteor.publish('lists.private', function() {
if (!this.userId) {
return this.ready();
}
return Lists.find({
userId: this.userId
}, {
fields: Lists.publicFields
});
});
Thanks to the guarantees provided by DDP and Meteor's accounts system, this publication will only ever publish private lists to the user that they belong to. The publication will re-run if the user logs out (or back in again), which means that the published set of private lists will change as the active user changes.
In the case of a logged-out user, we explicitly call this.ready(), which indicates to the subscription that we've sent all the data we are initially going to send. It's important to know that if you don't return a cursor from the publication or call this.ready(), the user's subscription will never become ready.
Here's an example of a publication which takes a named argument:
import SimpleSchema from 'simpl-schema';
Meteor.publish('todos.inList', function(listId) {
// Validate the argument
new SimpleSchema({
listId: { type: String }
}).validate({ listId });
if (!this.userId) {
return this.ready();
}
return Todos.find({ listId });
});
When we subscribe to this publication on the client, we can provide this argument:
Meteor.subscribe('todos.inList', list._id);
To use publications, you need to create a subscription to it on the client. To do so, you call Meteor.subscribe() with the name of the publication:
const handle = Meteor.subscribe('lists.public');
Meteor.subscribe() returns a "subscription handle" with these important properties:
.ready() - A reactive function that returns true when the publication is marked ready.stop() - Stops the subscription and clears the data from the client cacheWhen you are subscribing, it is very important to ensure that you always call .stop() on the subscription when you are done with it. This ensures that the documents sent by the subscription are cleared from your local Minimongo cache and the server stops doing the work required to service your subscription.
However, if you call Meteor.subscribe() inside a reactive context (such as a Tracker.autorun), then Meteor's reactive system will automatically call .stop() for you at the appropriate time.
Here's how to subscribe in a React component using the useSubscribe hook from react-meteor-data:
import { useSubscribe, useFind } from 'meteor/react-meteor-data';
import { Lists } from '/imports/api/lists/lists';
function ListsPage() {
const isLoading = useSubscribe('lists.public');
const lists = useFind(() => Lists.find(), []);
if (isLoading()) {
return <div>Loading...</div>;
}
return (
<ul>
{lists.map(list => (
<li key={list._id}>{list.name}</li>
))}
</ul>
);
}
For subscriptions with arguments:
function TodosPage({ listId }) {
const isLoading = useSubscribe('todos.inList', listId);
const todos = useFind(() => Todos.find({ listId }), [listId]);
if (isLoading()) {
return <div>Loading...</div>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo._id}>{todo.text}</li>
))}
</ul>
);
}
In Blaze, it's best to subscribe in the onCreated() callback:
Template.Lists_show_page.onCreated(function() {
this.getListId = () => FlowRouter.getParam('_id');
this.autorun(() => {
this.subscribe('todos.inList', this.getListId());
});
});
Calling this.subscribe() (rather than Meteor.subscribe) attaches a special subscriptionsReady() function to the template instance, which is true when all subscriptions made inside this template are ready.
Subscribing to data puts it in your client-side collection. To use the data in your user interface, you need to query your client-side collection.
If you're publishing a subset of your data, always re-specify the query when fetching data on the client:
// Good - specific query
const publicLists = Lists.find({ userId: { $exists: false } });
// Avoid - too broad, might include data from other subscriptions
const allLists = Lists.find();
Place the fetch logic close to where you subscribe to avoid action at a distance and make data flow easier to understand:
function TodoList({ listId }) {
const isLoading = useSubscribe('todos.inList', listId);
const todos = useFind(() => Todos.find({ listId }), [listId]);
// Both subscription and fetch are in the same component
if (isLoading()) return <Loading />;
return <TodoItems todos={todos} />;
}
It is key to understand that a subscription will not instantly provide its data. There will be a latency between subscribing to the data on the client and it arriving from the publication on the server.
const handle = Meteor.subscribe('lists.public');
Tracker.autorun(() => {
const isReady = handle.ready();
console.log(`Handle is ${isReady ? 'ready' : 'not ready'}`);
});
For multiple subscriptions:
const handles = [
Meteor.subscribe('lists.public'),
Meteor.subscribe('todos.inList', listId),
];
Tracker.autorun(() => {
const areReady = handles.every(handle => handle.ready());
console.log(`Handles are ${areReady ? 'ready' : 'not ready'}`);
});
When using React hooks or Blaze autoruns, subscriptions will automatically re-run when their reactive dependencies change:
function TodoList({ listId }) {
// Subscription will automatically re-run when listId changes
const isLoading = useSubscribe('todos.inList', listId);
// ...
}
Here's a pattern for paginated subscriptions:
const PAGE_SIZE = 20;
Meteor.publish('todos.paginated', function(listId, page = 1) {
new SimpleSchema({
listId: { type: String },
page: { type: Number, min: 1 }
}).validate({ listId, page });
const skip = (page - 1) * PAGE_SIZE;
return Todos.find({ listId }, {
sort: { createdAt: -1 },
skip,
limit: PAGE_SIZE
});
});
And on the client:
function PaginatedTodos({ listId }) {
const [page, setPage] = useState(1);
const isLoading = useSubscribe('todos.paginated', listId, page);
// ...
}
Sometimes you need to publish data from multiple collections that are related. There are two main approaches:
You can return an array of cursors from a publication:
Meteor.publish('lists.withTodos', function(listId) {
return [
Lists.find({ _id: listId }),
Todos.find({ listId })
];
});
For more complex relationships, use the reywood:publish-composite package:
meteor add reywood:publish-composite
import { publishComposite } from 'meteor/reywood:publish-composite';
publishComposite('lists.withTodosAndAuthors', function(listId) {
return {
find() {
return Lists.find({ _id: listId });
},
children: [{
find(list) {
return Todos.find({ listId: list._id });
},
children: [{
find(todo) {
return Meteor.users.find({ _id: todo.authorId }, {
fields: { profile: 1, username: 1 }
});
}
}]
}]
};
});
For more control over what gets published, you can use the low-level publish API:
Meteor.publish('custom.publication', function() {
const self = this;
// Add a document
self.added('collection-name', 'document-id', { field: 'value' });
// Change a document
self.changed('collection-name', 'document-id', { field: 'new-value' });
// Remove a document
self.removed('collection-name', 'document-id');
// Signal that initial data has been sent
self.ready();
// Clean up on stop
self.onStop(() => {
// cleanup code
});
});
This is useful for:
Meteor.publish('todos.inList', function(listId) {
// Always validate
check(listId, String);
// or use SimpleSchema
new SimpleSchema({
listId: { type: String }
}).validate({ listId });
// ...
});
Don't publish sensitive fields:
Meteor.publish('users.public', function() {
return Meteor.users.find({}, {
fields: {
username: 1,
profile: 1
// Don't include emails, services, etc.
}
});
});
Make sure users can only access data they're authorized to see:
Meteor.publish('lists.private', function() {
if (!this.userId) {
return this.ready();
}
// Only publish lists the user owns
return Lists.find({ userId: this.userId });
});
See the Security article for more details on securing publications.