parse-mobx
Version:
A wrapper for ParseJS SDK to make Parse Objects observable in Mobx
627 lines (505 loc) โข 14.7 kB
Markdown
<p align="center">
<h2 align="center">package: parse-mobx</h2>
<p align="center">Simple and efficient wrapper for Parse Objects to make it compatible with Mobx.</p>
<p align="center">
<a href="https://github.com/imana97/parse-mobx/ts-npm-package-boilerplate/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/imana97/parse-mobx?style=flat&color=336791" />
</a>
<a href="https://github.com/imana97/parse-mobx/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/imana97/parse-mobx?style=flat&color=336791" />
</a>
<a href="https://github.com/imana97/parse-mobx">
<img alt="GitHub Downloads" src="https://img.shields.io/npm/dw/parse-mobx?style=flat&color=336791" />
</a>
<a href="https://github.com/imana97/parse-mobx/">
<img alt="GitHub Total Downloads" src="https://img.shields.io/npm/dt/parse-mobx?color=336791&label=Total%20downloads" />
</a>
<a href="https://github.com/imana97/parse-mobx/">
<img alt="GitHub release" src="https://img.shields.io/github/release/imana97/parse-mobx.svg?style=flat&color=336791" />
</a>
<br />
<br />
<a href="https://github.com/imana97/parse-mobx/issues/new/choose">Report Bug</a>
<a href="https://github.com/imana97/parse-mobx/issues/new/choose">Request Feature</a>
</p>
<p align="center">Did you like the project? Please, considerate <a href="https://www.buymeacoffee.com/imana97">a donation</a> to help improve!</p>
# Getting Started
## ๐ API Documentation
https://imana97.github.io/parse-mobx/
## ๐ฆ Installation
Parse-MobX requires **MobX** and **Parse SDK** as peer dependencies:
```bash
npm install parse-mobx mobx parse
# For TypeScript projects, also install types
npm install -D /parse
```
### Requirements
- **MobX**: `^6.13.0`
- **Parse SDK**: `^6.1.1`
- **Node.js**: `^18.0.0` or higher
- **TypeScript**: `^5.0.0` (if using TypeScript)
## ๐ Quick Start
### 1. Parse Server Setup & Configuration
First, initialize Parse and configure ParseMobx:
```typescript
import Parse from 'parse';
import { configureParseMobx } from 'parse-mobx';
// Initialize Parse
Parse.initialize(
'YOUR_APP_ID',
'YOUR_JAVASCRIPT_KEY'
);
Parse.serverURL = 'https://your-parse-server.com/parse';
// Configure ParseMobx with your Parse instance
configureParseMobx(Parse);
```
### 2. Basic Usage
```typescript
import { ParseMobx } from 'parse-mobx';
import { observable, action } from 'mobx';
// Create a Parse object and make it observable
const parseObject = await new Parse.Query('Todo').first();
const observableTodo = new ParseMobx(parseObject);
// Now you can use it reactively in your components
console.log(observableTodo.get('title')); // Gets the title
observableTodo.set('completed', true).save(); // Updates and saves
```
## ๐ Complete Todo App Example
Here's a comprehensive React Todo application using **parse-mobx**, **MobX**, and **React**:
### Store Setup (`stores/TodoStore.ts`)
```typescript
import { makeObservable, observable, action, runInAction } from 'mobx';
import Parse from 'parse';
import { ParseMobx, MobxStore, configureParseMobx } from 'parse-mobx';
// Initialize Parse and configure ParseMobx
Parse.initialize('YOUR_APP_ID', 'YOUR_JS_KEY');
Parse.serverURL = 'https://your-parse-server.com/parse';
configureParseMobx(Parse);
export class TodoStore extends MobxStore {
newTodoText = '';
filter: 'all' | 'active' | 'completed' = 'all';
constructor() {
super('Todo'); // Pass Parse class name
makeObservable(this);
}
setNewTodoText(text: string) {
this.newTodoText = text;
}
setFilter(filter: 'all' | 'active' | 'completed') {
this.filter = filter;
}
async addTodo() {
if (!this.newTodoText.trim()) return;
await this.createObject({
title: this.newTodoText.trim(),
completed: false,
createdAt: new Date()
}, { updateList: true });
this.newTodoText = '';
}
async toggleTodo(todo: ParseMobx) {
const completed = !todo.get('completed');
await todo.set('completed', completed).save();
}
async deleteTodo(todo: ParseMobx) {
await this.deleteObject(todo);
}
async loadTodos() {
const query = new Parse.Query('Todo');
query.ascending('createdAt');
this.fetchObjects(query);
}
get filteredTodos() {
switch (this.filter) {
case 'active':
return this.objects.filter(todo => !todo.get('completed'));
case 'completed':
return this.objects.filter(todo => todo.get('completed'));
default:
return this.objects;
}
}
get activeTodosCount() {
return this.objects.filter(todo => !todo.get('completed')).length;
}
}
// Create a singleton instance
export const todoStore = new TodoStore();
```
### React Components
#### Main Todo App (`components/TodoApp.tsx`)
```typescript
import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { todoStore } from '../stores/TodoStore';
import { TodoInput } from './TodoInput';
import { TodoList } from './TodoList';
import { TodoFilters } from './TodoFilters';
import './TodoApp.css';
export const TodoApp = observer(() => {
useEffect(() => {
todoStore.loadTodos();
}, []);
if (todoStore.loading) {
return (
<div className="todo-app">
<div className="loading">Loading todos...</div>
</div>
);
}
return (
<div className="todo-app">
<header className="header">
<h1>todos</h1>
<TodoInput />
</header>
<main className="main">
<TodoList />
</main>
<footer className="footer">
<TodoFilters />
<span className="todo-count">
{todoStore.activeTodosCount} items left
</span>
</footer>
{todoStore.parseError && (
<div className="error">
Error: {todoStore.parseError.message}
<button onClick={() => todoStore.clearError()}>
Dismiss
</button>
</div>
)}
</div>
);
});
```
#### Todo Input (`components/TodoInput.tsx`)
```typescript
import React from 'react';
import { observer } from 'mobx-react-lite';
import { todoStore } from '../stores/TodoStore';
export const TodoInput = observer(() => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
todoStore.addTodo();
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
todoStore.addTodo();
}
};
return (
<form onSubmit={handleSubmit}>
<input
className="new-todo"
placeholder="What needs to be done?"
value={todoStore.newTodoText}
onChange={(e) => todoStore.setNewTodoText(e.target.value)}
onKeyPress={handleKeyPress}
autoFocus
/>
</form>
);
});
```
#### Todo List (`components/TodoList.tsx`)
```typescript
import React from 'react';
import { observer } from 'mobx-react-lite';
import { todoStore } from '../stores/TodoStore';
import { TodoItem } from './TodoItem';
export const TodoList = observer(() => {
if (todoStore.filteredTodos.length === 0) {
return (
<div className="no-todos">
{todoStore.filter === 'completed'
? 'No completed todos'
: 'No todos yet. Add one above!'}
</div>
);
}
return (
<ul className="todo-list">
{todoStore.filteredTodos.map((todo) => (
<TodoItem key={todo.getId()} todo={todo} />
))}
</ul>
);
});
```
#### Individual Todo Item (`components/TodoItem.tsx`)
```typescript
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import { ParseMobx } from 'parse-mobx';
import { todoStore } from '../stores/TodoStore';
interface TodoItemProps {
todo: ParseMobx;
}
export const TodoItem = observer(({ todo }: TodoItemProps) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.get('title'));
const handleSave = async () => {
if (editText.trim()) {
await todo.set('title', editText.trim()).save();
setIsEditing(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
setEditText(todo.get('title'));
setIsEditing(false);
}
};
const isCompleted = todo.get('completed');
const title = todo.get('title');
return (
<li className={`todo-item ${isCompleted ? 'completed' : ''}`}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={isCompleted}
onChange={() => todoStore.toggleTodo(todo)}
/>
{isEditing ? (
<input
className="edit"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={handleSave}
onKeyPress={handleKeyPress}
autoFocus
/>
) : (
<label onDoubleClick={() => setIsEditing(true)}>
{title}
</label>
)}
<button
className="destroy"
onClick={() => todoStore.deleteTodo(todo)}
>
ร
</button>
</div>
{todo.loading && <div className="todo-loading">Saving...</div>}
</li>
);
});
```
#### Filter Controls (`components/TodoFilters.tsx`)
```typescript
import React from 'react';
import { observer } from 'mobx-react-lite';
import { todoStore } from '../stores/TodoStore';
export const TodoFilters = observer(() => {
const filters = [
{ key: 'all' as const, label: 'All' },
{ key: 'active' as const, label: 'Active' },
{ key: 'completed' as const, label: 'Completed' },
];
return (
<div className="filters">
{filters.map(({ key, label }) => (
<button
key={key}
className={todoStore.filter === key ? 'selected' : ''}
onClick={() => todoStore.setFilter(key)}
>
{label}
</button>
))}
</div>
);
});
```
## ๐ Real-time Updates with LiveQuery
Enable real-time synchronization across clients:
```typescript
export class TodoStore extends MobxStore {
constructor() {
super('Todo');
makeObservable(this);
// Subscribe to real-time updates
this.subscribe();
// Setup event handlers
this.onCreate((todo) => {
console.log('New todo created:', todo.get('title'));
});
this.onUpdate((todo) => {
console.log('Todo updated:', todo.get('title'));
});
this.onDelete((todo) => {
console.log('Todo deleted');
});
}
}
```
## ๐จ CSS Styling (`TodoApp.css`)
```css
.todo-app {
max-width: 550px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header h1 {
font-size: 3rem;
color: #b83f45;
text-align: center;
margin-bottom: 20px;
}
.new-todo {
width: 100%;
padding: 15px;
font-size: 1.2rem;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.todo-list {
list-style: none;
padding: 0;
margin: 20px 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
position: relative;
}
.todo-item.completed label {
text-decoration: line-through;
color: #999;
}
.toggle {
margin-right: 15px;
}
.destroy {
position: absolute;
right: 10px;
background: none;
border: none;
font-size: 1.5rem;
color: #cc9a9a;
cursor: pointer;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.filters button {
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
}
.filters button.selected {
background: #b83f45;
color: white;
}
.loading, .no-todos {
text-align: center;
padding: 20px;
color: #999;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
```
## โ๏ธ Configuration System
ParseMobx requires explicit configuration to avoid global namespace pollution and give you full control over your Parse setup:
### Benefits of Configuration
- โ
**No global pollution** - Parse is not set on the global object
- โ
**Full control** - Configure Parse exactly how you need it
- โ
**Better testing** - Easy to mock and test with different Parse instances
- โ
**Clean architecture** - Clear separation between Parse setup and usage
### Configuration API
```typescript
import { configureParseMobx } from 'parse-mobx';
import Parse from 'parse';
// Configure with your Parse instance
configureParseMobx(Parse);
// Now you can use ParseMobx and MobxStore
import { ParseMobx, MobxStore } from 'parse-mobx';
```
### Error Handling
If you try to use ParseMobx without configuration, you'll get a clear error:
```typescript
// This will throw an error:
const todo = new ParseMobx(parseObject);
// Error: ParseMobx is not configured. Please call configureParseMobx(parse) first.
```
## ๐ Advanced Features
### Custom Parse Object Classes
```typescript
// Parse is already configured above, so we can use it directly
// Define a custom Parse object
class Todo extends Parse.Object {
constructor() {
super('Todo');
}
static spawn(attrs: any) {
const todo = new Todo();
return todo.set(attrs);
}
}
// Register the subclass
Parse.Object.registerSubclass('Todo', Todo);
// Use with ParseMobx
const todo = Todo.spawn({ title: 'Learn Parse-MobX' });
await todo.save();
const observableTodo = new ParseMobx(todo);
```
### Optimistic Updates
```typescript
async toggleTodoOptimistic(todo: ParseMobx) {
// Update UI immediately
const oldValue = todo.get('completed');
todo.set('completed', !oldValue);
try {
// Save to server
await todo.save();
} catch (error) {
// Revert on error
todo.set('completed', oldValue);
console.error('Failed to update todo:', error);
}
}
```
This example demonstrates the full power of parse-mobx with reactive UI updates, real-time synchronization, and clean separation of concerns using MobX stores!
## ๐งช Testing
The configuration system makes testing much easier:
```typescript
import { configureParseMobx, resetConfiguration } from 'parse-mobx';
// Mock Parse for testing
const mockParse = {
Object: jest.fn(),
Query: jest.fn(),
// ... other Parse methods
};
// Configure with mock
configureParseMobx(mockParse);
// Run your tests
const store = new MobxStore('Test');
store.fetchObjects();
// Reset between tests
resetConfiguration();
```