Documentation Index
Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt
Use this file to discover all available pages before exploring further.
What You’ll Build
In this comprehensive tutorial, you’ll build a fully-functional To-Do application with:
- ✅ Real-time task synchronization across clients
- ✅ User authentication and private tasks
- ✅ Task filtering (all, active, completed)
- ✅ Add, update, and delete operations
- ✅ Responsive UI with React
- ✅ Production-ready deployment
This tutorial follows the official Meteor React tutorial structure, adapted from the source at v3-docs/docs/tutorials/react/.
Prerequisites
- Basic JavaScript and React knowledge
- Meteor installed (Installation Guide)
- A code editor (VS Code, Sublime, etc.)
1. Creating the App
Create Your Project
The easiest way to set up Meteor with React is using the --react option:meteor create simple-todos-react
cd simple-todos-react
Meteor 3.4+ uses Rspack by default for faster builds and built-in Hot Module Replacement (HMR).
Start the Development Server
Your app will be available at http://localhost:3000. Create the Task Component
Create imports/ui/Task.jsx:import React from 'react';
export const Task = ({ task }) => {
return <li>{task.text}</li>;
};
Create Sample Tasks
Add sample data to imports/ui/App.jsx:import React from 'react';
import { Task } from './Task';
const tasks = [
{ _id: 1, text: 'First Task' },
{ _id: 2, text: 'Second Task' },
{ _id: 3, text: 'Third Task' },
];
export const App = () => (
<div>
<h1>Welcome to Meteor!</h1>
<ul>
{tasks.map(task => (
<Task key={task._id} task={task} />
))}
</ul>
</div>
);
2. Collections - Connecting to MongoDB
Meteor uses MongoDB for data storage. Let’s create a collection for our tasks.
Create the Tasks Collection
Create imports/api/TasksCollection.js:import { Mongo } from 'meteor/mongo';
export const TasksCollection = new Mongo.Collection('tasks');
This single line creates a MongoDB collection on the server and an in-memory Minimongo cache on the client.
Initialize with Sample Data
Update server/main.js to seed the database:import { Meteor } from 'meteor/meteor';
import { TasksCollection } from '/imports/api/TasksCollection';
const insertTask = (taskText) =>
TasksCollection.insertAsync({ text: taskText });
Meteor.startup(async () => {
if ((await TasksCollection.find().countAsync()) === 0) {
[
'First Task',
'Second Task',
'Third Task',
'Fourth Task',
'Fifth Task',
'Sixth Task',
'Seventh Task',
].forEach(insertTask);
}
});
Render Collection Data
Update imports/ui/App.jsx to use real data:import React from 'react';
import { useTracker, useSubscribe } from 'meteor/react-meteor-data';
import { TasksCollection } from '/imports/api/TasksCollection';
import { Task } from './Task';
export const App = () => {
const isLoading = useSubscribe('tasks');
const tasks = useTracker(() => TasksCollection.find({}).fetch());
if (isLoading()) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Welcome to Meteor!</h1>
<ul>
{tasks.map(task => (
<Task key={task._id} task={task} />
))}
</ul>
</div>
);
};
useTracker is a React hook that creates a reactive computation. When the collection changes, the component re-renders automatically.
Let’s add the ability to create new tasks.
Create the Task Form
Create imports/ui/TaskForm.jsx:import React, { useState } from 'react';
import { TasksCollection } from '/imports/api/TasksCollection';
export const TaskForm = () => {
const [text, setText] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
if (!text.trim()) return;
await TasksCollection.insertAsync({
text: text.trim(),
createdAt: new Date(),
});
setText('');
};
return (
<form className="task-form" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Type to add new tasks"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Task</button>
</form>
);
};
Add Form to App
Update imports/ui/App.jsx:import { TaskForm } from './TaskForm';
export const App = () => {
// ... previous code
return (
<div>
<h1>Welcome to Meteor!</h1>
<TaskForm />
<ul>
{tasks.map(task => (
<Task key={task._id} task={task} />
))}
</ul>
</div>
);
};
Notice you’re calling insertAsync directly from the client. This works during development, but we’ll secure it later with methods and publications.
4. Update and Remove Tasks
Add Toggle and Delete Functions
Update imports/ui/Task.jsx:import React from 'react';
import { TasksCollection } from '/imports/api/TasksCollection';
export const Task = ({ task }) => {
const toggleChecked = async () => {
await TasksCollection.updateAsync(task._id, {
$set: { isChecked: !task.isChecked }
});
};
const deleteTask = async () => {
await TasksCollection.removeAsync(task._id);
};
return (
<li>
<input
type="checkbox"
checked={!!task.isChecked}
onChange={toggleChecked}
/>
<span>{task.text}</span>
<button onClick={deleteTask}>×</button>
</li>
);
};
5. Styling Your App
Add CSS
Create client/main.css (or update if it exists):body {
font-family: sans-serif;
background-color: #315481;
background-image: linear-gradient(to bottom, #315481, #918e82 100%);
background-attachment: fixed;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 0;
margin: 0;
font-size: 14px;
}
h1 {
font-size: 1.5em;
margin: 0;
margin-bottom: 10px;
display: inline-block;
margin-right: 1em;
}
.task-form {
margin-bottom: 10px;
display: flex;
}
.task-form input {
flex-grow: 1;
box-sizing: border-box;
padding: 10px 0;
background: transparent;
border: none;
border-bottom: 2px solid #555;
color: white;
font-size: 1em;
}
.task-form button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
ul {
margin: 0;
padding: 0;
background: white;
}
li {
position: relative;
list-style: none;
padding: 15px;
border-bottom: #eee solid 1px;
display: flex;
align-items: center;
}
li input[type="checkbox"] {
margin-right: 10px;
}
li span {
flex-grow: 1;
}
li.checked span {
text-decoration: line-through;
color: #888;
}
li button {
background-color: #d9534f;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
Apply CSS Classes
Update imports/ui/Task.jsx to add the checked class:<li className={task.isChecked ? 'checked' : ''}>
{/* ... */}
</li>
6. Filter Tasks
Add Filter State
Update imports/ui/App.jsx to add filtering:import React, { useState } from 'react';
export const App = () => {
const [hideCompleted, setHideCompleted] = useState(false);
const isLoading = useSubscribe('tasks');
const tasks = useTracker(() => {
const filter = hideCompleted ? { isChecked: { $ne: true } } : {};
return TasksCollection.find(filter, { sort: { createdAt: -1 } }).fetch();
});
const pendingTasksCount = useTracker(() =>
TasksCollection.find({ isChecked: { $ne: true } }).count()
);
if (isLoading()) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Todo List ({pendingTasksCount})</h1>
<label>
<input
type="checkbox"
checked={hideCompleted}
onChange={() => setHideCompleted(!hideCompleted)}
/>
Hide Completed Tasks
</label>
<TaskForm />
<ul>
{tasks.map(task => (
<Task key={task._id} task={task} />
))}
</ul>
</div>
);
};
7. Adding User Accounts
Add Account Packages
meteor add accounts-password
meteor npm install --save bcrypt
Create Default User
Update server/main.js:import { Accounts } from 'meteor/accounts-base';
const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';
Meteor.startup(async () => {
if (!(await Accounts.findUserByUsername(SEED_USERNAME))) {
await Accounts.createUser({
username: SEED_USERNAME,
password: SEED_PASSWORD,
});
}
// ... rest of startup code
});
Create Login Form
Create imports/ui/LoginForm.jsx:import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
export const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const submit = (e) => {
e.preventDefault();
Meteor.loginWithPassword(username, password);
};
return (
<form onSubmit={submit} className="login-form">
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Log In</button>
</form>
);
};
Add Authentication to App
Update imports/ui/App.jsx:import { Meteor } from 'meteor/meteor';
import { useTracker } from 'meteor/react-meteor-data';
import { LoginForm } from './LoginForm';
export const App = () => {
const user = useTracker(() => Meteor.user());
if (!user) {
return (
<div>
<h1>Please Log In</h1>
<LoginForm />
</div>
);
}
const logout = () => Meteor.logout();
// ... rest of component
return (
<div>
<h1>
Todo List ({pendingTasksCount})
<button onClick={logout}>Logout</button>
</h1>
{/* ... */}
</div>
);
};
Associate Tasks with Users
Update the TaskForm to include user ID:await TasksCollection.insertAsync({
text: text.trim(),
createdAt: new Date(),
userId: Meteor.userId(),
});
8. Security with Methods and Publications
By default, Meteor apps have insecure and autopublish packages that allow full client access to the database. Remove these before deploying!
Remove Insecure Packages
meteor remove insecure autopublish
Create Methods
Create imports/api/tasksMethods.js:import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { TasksCollection } from './TasksCollection';
Meteor.methods({
async 'tasks.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
return await TasksCollection.insertAsync({
text,
createdAt: new Date(),
userId: this.userId,
});
},
async 'tasks.remove'(taskId) {
check(taskId, String);
const task = await TasksCollection.findOneAsync(taskId);
if (task.userId !== this.userId) {
throw new Meteor.Error('Not authorized.');
}
return await TasksCollection.removeAsync(taskId);
},
async 'tasks.setIsChecked'(taskId, isChecked) {
check(taskId, String);
check(isChecked, Boolean);
const task = await TasksCollection.findOneAsync(taskId);
if (task.userId !== this.userId) {
throw new Meteor.Error('Not authorized.');
}
return await TasksCollection.updateAsync(taskId, {
$set: { isChecked }
});
},
});
Import this file in server/main.js:import '/imports/api/tasksMethods';
Create Publications
Create imports/api/tasksPublications.js:import { Meteor } from 'meteor/meteor';
import { TasksCollection } from './TasksCollection';
Meteor.publish('tasks', function publishTasks() {
return TasksCollection.find({ userId: this.userId });
});
Import in server/main.js:import '/imports/api/tasksPublications';
Update Components to Use Methods
Update imports/ui/TaskForm.jsx:await Meteor.callAsync('tasks.insert', text.trim());
Update imports/ui/Task.jsx:const toggleChecked = async () => {
await Meteor.callAsync('tasks.setIsChecked', task._id, !task.isChecked);
};
const deleteTask = async () => {
await Meteor.callAsync('tasks.remove', task._id);
};
9. Deploying Your App
Galaxy (Recommended)
Build for Custom Server
Galaxy is Meteor’s official hosting platform:# Set your settings
DEPLOY_HOSTNAME=galaxy.meteor.com meteor deploy myapp.meteorapp.com --settings settings.json
Galaxy provides MongoDB hosting, SSL certificates, and automatic scaling.
Build a production bundle:meteor build ../output --architecture os.linux.x86_64
This creates a tarball you can deploy to any Node.js server.
What You’ve Learned
Congratulations! You’ve built a complete Meteor application with:
Collections
MongoDB integration with reactive queries
Real-Time Sync
Automatic data synchronization across clients
Authentication
User accounts with secure login
Security
Methods and publications for data access control
React Integration
useTracker and useSubscribe hooks
CRUD Operations
Create, read, update, and delete tasks
Next Steps
Testing
Add tests with TinyTest or Jest
Deployment
Deploy to production with Galaxy or your own server
Advanced Patterns
Learn advanced methods, optimistic UI, and more
Mobile Apps
Build iOS and Android apps with Cordova
Additional Resources
Source Code: The complete tutorial code is available on GitHub:
Forums
Community support and discussions
Discord
Real-time chat with developers
API Docs
Complete API reference