Skip to content

Commit 6cda9a2

Browse files
committed
feat(core,react): support draft-style setState; honor schema.Ignore; sync docs
- core(mirror): setState now supports mutative draft updaters or returning a new state; updated JSDoc - core(state): widen Store.setState type to allow void-return mutative updaters - core(diff): skip fields marked with schema.Ignore() during map diffs (insert/update/delete) - react(hooks): forward updaters directly to store; remove shallow-copy mutation and external Immer usage - docs(root,core,react): update READMEs to show both immutable and draft-style updates; fix imports and remove non-existent sync methods - config: simplify ESLint config to avoid missing react plugin; clean core tsconfig includes
1 parent 14b6dc8 commit 6cda9a2

File tree

11 files changed

+185
-155
lines changed

11 files changed

+185
-155
lines changed

.eslintrc.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,14 @@ module.exports = {
33
extends: [
44
'eslint:recommended',
55
'plugin:@typescript-eslint/recommended',
6-
'plugin:react/recommended',
7-
'plugin:react-hooks/recommended',
86
],
9-
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
7+
plugins: ['@typescript-eslint'],
108
env: {
119
browser: true,
1210
node: true,
1311
es6: true,
1412
},
15-
settings: {
16-
react: {
17-
version: 'detect',
18-
},
19-
},
2013
rules: {
21-
'react/prop-types': 'off',
2214
'@typescript-eslint/explicit-module-boundary-types': 'off',
2315
'@typescript-eslint/no-explicit-any': 'off',
2416
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
@@ -31,4 +23,4 @@ module.exports = {
3123
},
3224
},
3325
],
34-
};
26+
};

README.md

Lines changed: 105 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -43,134 +43,150 @@ pnpm add loro-mirror-react loro-mirror loro-crdt
4343
### Core Usage
4444

4545
```typescript
46-
import { LoroDoc } from 'loro-crdt';
47-
import { schema, createStore } from 'loro-mirror';
46+
import { LoroDoc } from "loro-crdt";
47+
import { schema, createStore } from "loro-mirror";
4848

4949
// Define your schema
5050
const todoSchema = schema({
51-
todos: schema.LoroList(
52-
schema.LoroMap({
53-
id: schema.String({ required: true }),
54-
text: schema.LoroText({ required: true }),
55-
completed: schema.Boolean({ defaultValue: false }),
56-
})
57-
),
51+
todos: schema.LoroList(
52+
schema.LoroMap({
53+
id: schema.String({ required: true }),
54+
text: schema.String({ required: true }),
55+
completed: schema.Boolean({ defaultValue: false }),
56+
}),
57+
),
5858
});
5959

6060
// Create a Loro document
6161
const doc = new LoroDoc();
6262
// Create a store
6363
const store = createStore({
64-
doc,
65-
schema: todoSchema,
66-
initialState: { todos: [] },
64+
doc,
65+
schema: todoSchema,
66+
initialState: { todos: [] },
6767
});
6868

69-
// Update the state
69+
// Update the state (immutable update)
70+
store.setState((s) => ({
71+
...s,
72+
todos: [
73+
...s.todos,
74+
{
75+
id: Date.now().toString(),
76+
text: "Learn Loro Mirror",
77+
completed: false,
78+
},
79+
],
80+
}));
81+
82+
// Or: draft-style updates (mutate a draft)
7083
store.setState((state) => {
71-
state.todos.push({
72-
id: Date.now().toString(),
73-
text: 'Learn Loro Mirror',
74-
completed: false,
75-
});
76-
return state;
84+
state.todos.push({
85+
id: Date.now().toString(),
86+
text: "Learn Loro Mirror",
87+
completed: false,
88+
});
89+
// no return needed
7790
});
7891

7992
// Subscribe to state changes
8093
store.subscribe((state) => {
81-
console.log('State updated:', state);
94+
console.log("State updated:", state);
8295
});
8396
```
8497

8598
### React Usage
8699

87100
```tsx
88-
import React, { useMemo } from 'react';
89-
import { LoroDoc } from 'loro-crdt';
90-
import { schema } from 'loro-mirror';
91-
import { createLoroContext } from 'loro-mirror-react';
101+
import React, { useMemo, useState } from "react";
102+
import { LoroDoc } from "loro-crdt";
103+
import { schema } from "loro-mirror";
104+
import { createLoroContext } from "loro-mirror-react";
92105

93106
// Define your schema
94107
const todoSchema = schema({
95-
todos: schema.LoroList(
96-
schema.LoroMap({
97-
id: schema.String({ required: true }),
98-
text: schema.String({ required: true }),
99-
completed: schema.Boolean({ defaultValue: false }),
100-
})
101-
),
108+
todos: schema.LoroList(
109+
schema.LoroMap({
110+
id: schema.String({ required: true }),
111+
text: schema.String({ required: true }),
112+
completed: schema.Boolean({ defaultValue: false }),
113+
}),
114+
),
102115
});
103116

104117
// Create a context
105-
const {
106-
LoroProvider,
107-
useLoroState,
108-
useLoroSelector,
109-
useLoroAction,
110-
} = createLoroContext(todoSchema);
118+
const { LoroProvider, useLoroState, useLoroSelector, useLoroAction } =
119+
createLoroContext(todoSchema);
111120

112121
// Root component
113122
function App() {
114-
const doc = useMemo(() => new LoroDoc(), []);
115-
116-
return (
117-
<LoroProvider doc={doc} initialState={{ todos: [] }}>
118-
<TodoList />
119-
<AddTodoForm />
120-
</LoroProvider>
121-
);
123+
const doc = useMemo(() => new LoroDoc(), []);
124+
125+
return (
126+
<LoroProvider doc={doc} initialState={{ todos: [] }}>
127+
<TodoList />
128+
<AddTodoForm />
129+
</LoroProvider>
130+
);
122131
}
123132

124133
// Todo list component
125134
function TodoList() {
126-
const todos = useLoroSelector(state => state.todos);
127-
128-
return (
129-
<ul>
130-
{todos.map(todo => (
131-
<li key={todo.id}>
132-
<input
133-
type="checkbox"
134-
checked={todo.completed}
135-
onChange={() => toggleTodo(todo.id)}
136-
/>
137-
<span>{todo.text}</span>
138-
</li>
139-
))}
140-
</ul>
141-
);
135+
const todos = useLoroSelector((state) => state.todos);
136+
const toggleTodo = useLoroAction((s, id: string) => {
137+
const i = s.todos.findIndex((t) => t.id === id);
138+
if (i !== -1) s.todos[i].completed = !s.todos[i].completed;
139+
}, []);
140+
141+
return (
142+
<ul>
143+
{todos.map((todo) => (
144+
<li key={todo.id}>
145+
<input
146+
type="checkbox"
147+
checked={todo.completed}
148+
onChange={() => toggleTodo(todo.id)}
149+
/>
150+
<span>{todo.text}</span>
151+
</li>
152+
))}
153+
</ul>
154+
);
142155
}
143156

144157
// Add todo form component
145158
function AddTodoForm() {
146-
const [text, setText] = useState('');
147-
148-
const addTodo = useLoroAction((state) => {
149-
state.todos.push({
150-
id: Date.now().toString(),
151-
text: text.trim(),
152-
completed: false,
153-
});
154-
}, [text]);
155-
156-
const handleSubmit = (e) => {
157-
e.preventDefault();
158-
if (text.trim()) {
159-
addTodo();
160-
setText('');
161-
}
162-
};
163-
164-
return (
165-
<form onSubmit={handleSubmit}>
166-
<input
167-
value={text}
168-
onChange={(e) => setText(e.target.value)}
169-
placeholder="What needs to be done?"
170-
/>
171-
<button type="submit">Add Todo</button>
172-
</form>
173-
);
159+
const [text, setText] = useState("");
160+
161+
const addTodo = useLoroAction(
162+
(state) => {
163+
state.todos.push({
164+
id: Date.now().toString(),
165+
text: text.trim(),
166+
completed: false,
167+
});
168+
},
169+
[text],
170+
);
171+
172+
const handleSubmit = (e) => {
173+
e.preventDefault();
174+
if (text.trim()) {
175+
addTodo();
176+
setText("");
177+
}
178+
};
179+
180+
return (
181+
<form onSubmit={handleSubmit}>
182+
<input
183+
value={text}
184+
onChange={(e) => setText(e.target.value)}
185+
placeholder="What needs to be done?"
186+
/>
187+
<button type="submit">Add Todo</button>
188+
</form>
189+
);
174190
}
175191
```
176192

packages/core/README.md

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ CRDT.
1414
## Installation
1515

1616
```bash
17-
npm install loro-mirror-core
17+
npm install loro-mirror loro-crdt
1818
```
1919

2020
## Usage
@@ -50,7 +50,7 @@ const mirror = new Mirror({
5050
// Get the current state
5151
const state = mirror.getState();
5252

53-
// Update the state (immutably)
53+
// Update the state (immutable update)
5454
mirror.setState({
5555
...state,
5656
todos: [
@@ -59,6 +59,12 @@ mirror.setState({
5959
],
6060
});
6161

62+
// Or: draft-style updates (mutate a draft)
63+
mirror.setState((draft) => {
64+
draft.todos.push({ id: "4", text: "Another task", completed: false });
65+
// no return needed
66+
});
67+
6268
// Subscribe to changes
6369
const unsubscribe = mirror.subscribe((state, direction) => {
6470
console.log("State updated:", state);
@@ -126,8 +132,7 @@ Here's a practical example showing how to use the Mirror with a todo list
126132
application:
127133

128134
```typescript
129-
import { Mirror } from "loro-mirror-core";
130-
import { schema } from "loro-mirror-core/schema";
135+
import { Mirror, schema } from "loro-mirror";
131136
import { LoroDoc } from "loro-crdt";
132137
import { useEffect, useState } from "react";
133138

@@ -148,7 +153,7 @@ const todoAppSchema = schema({
148153
sorting: schema.String({ defaultValue: "createdAt" }),
149154
});
150155

151-
// Create a custom hook to use the Mirror
156+
// Example hook to wire the Mirror into React state
152157
function useTodoApp() {
153158
const [mirror] = useState(() => {
154159
const doc = new LoroDoc();
@@ -169,10 +174,10 @@ function useTodoApp() {
169174

170175
// Add a new todo
171176
const addTodo = (text) => {
172-
setState({
173-
...state,
177+
setState((s) => ({
178+
...s,
174179
todos: [
175-
...state.todos,
180+
...s.todos,
176181
{
177182
id: Date.now().toString(),
178183
text,
@@ -181,17 +186,17 @@ function useTodoApp() {
181186
createdAt: Date.now(),
182187
},
183188
],
184-
});
189+
}));
185190
};
186191

187192
// Toggle a todo's completed status
188193
const toggleTodo = (id) => {
189-
setState({
190-
...state,
191-
todos: state.todos.map((todo) =>
194+
setState((s) => ({
195+
...s,
196+
todos: s.todos.map((todo) =>
192197
todo.id === id ? { ...todo, completed: !todo.completed } : todo
193198
),
194-
});
199+
}));
195200
};
196201

197202
// Remove a todo
@@ -306,8 +311,7 @@ For large lists, using an `idSelector` can significantly improve performance by:
306311
- Preserving item identity which can help with animations and React rendering
307312
optimizations
308313

309-
For more examples and detailed API documentation, see the
310-
[API Reference](API.md).
314+
For more examples and API notes, see below.
311315

312316
## Schema System
313317

@@ -401,7 +405,7 @@ const mySchema = schema({
401405
- `schema.String(options?)` - String type
402406
- `schema.Number(options?)` - Number type
403407
- `schema.Boolean(options?)` - Boolean type
404-
- `schema.Ignore(options?)` - Field to ignore (not synced with Loro)
408+
- `schema.Ignore(options?)` - Field to ignore (not synced with Loro). Any changes to this field are kept in app state only and are not mirrored into Loro.
405409
- `schema.LoroText(options?)` - Loro rich text
406410
- `schema.LoroMap(definition, options?)` - Loro map (object)
407411
- `schema.LoroList(itemSchema, idSelector?, options?)` - Loro list (array)
@@ -424,11 +428,8 @@ const store = createStore({
424428
### Store API
425429

426430
- `getState()` - Get the current state
427-
- `setState(updater)` - Update the state
431+
- `setState(updater)` - Update state; supports mutating a draft or returning a new state object
428432
- `subscribe(callback)` - Subscribe to state changes
429-
- `syncFromLoro()` - Sync from Loro to application state
430-
- `syncToLoro()` - Sync from application state to Loro
431-
- `sync()` - Full bidirectional sync
432433
- `getMirror()` - Get the underlying Mirror instance
433434

434435
### `createReducer`

0 commit comments

Comments
 (0)