Skip to main content

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

1

Install Meteor

If you haven’t already:
npx meteor
2

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).
3

Start the Development Server

meteor
Your app will be available at http://localhost:3000.
4

Create the Task Component

Create imports/ui/Task.jsx:
import React from 'react';

export const Task = ({ task }) => {
  return <li>{task.text}</li>;
};
5

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.
1

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.
2

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);
  }
});
3

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.

3. Forms and Events

Let’s add the ability to create new tasks.
1

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>
  );
};
2

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

1

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}>&times;</button>
    </li>
  );
};

5. Styling Your App

1

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;
}
2

Apply CSS Classes

Update imports/ui/Task.jsx to add the checked class:
<li className={task.isChecked ? 'checked' : ''}>
  {/* ... */}
</li>

6. Filter Tasks

1

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

1

Add Account Packages

meteor add accounts-password
meteor npm install --save bcrypt
2

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
});
3

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>
  );
};
4

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>
  );
};
5

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!
1

Remove Insecure Packages

meteor remove insecure autopublish
2

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';
3

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';
4

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

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