Recipes
Fetching the root state/document
const [doc, setDoc] = useDoc();
Fetching state at a path
The path here is similar (but not the same) to JSON patches RFC 6902. Instead of a string of keys/indexes joined by /
, it is an array of keys/indexes.
const [state, setState] = useDoc("/path/to/3/state");
You can specify the depth which will define how deep the changes into your state will be listened to by the component to rerender.
const [state, setState] = useDoc("/path/to/3/state", 2);
The above will rerender the component if the generated patch has path matching any of the below
"/path"
"/path/to"
"/path/to/3"
"/path/to/3/state"
"/path/to/3/state/anything"
"/path/to/3/state/anything/anything2"
Replacing vs mutating state
const store = createDocStore({ user: { name: "John" } });
// 1. Mutating user.name
const [user, setUser] = useDoc("/user")
setUser(user => {
user.name = "Jane"
})
// 2. Replacing user
// This approach is generally not recommended because it updates
// the whole state(user object in this case) and may trigger unnecessary reactions.
const [user, setUser] = useDoc("/user")
setUser({ name: "Jane" }})
// 3. Alternatively, you can replace user.name
// This works the same as example 1 above but reads better and also, your component
// will only listen to the changes to name while for example 1, it will listen to changes
// on the user object
const [username, setUsername] = useDoc("/user/name")
setUsername("Jane")
Actions
We are using action as a generic term for a function that updates your document.
You can define actions outside your component. This way, the state management part of your application can stay separate from your UI.
function addTodos(setTodos, todoItem) {
setTodos((todos) => {
todos.push({
caption: todoItem,
});
});
}
// In React Component
const [todos, setTodos] = useDoc(todoPath);
<button onClick={() => addTodos(setTodos, "Hello world!")}>Click me!</button>;
Async actions
Performing async actions doesn't require any special API in SyncState. You can call the setter function returned from useDoc whenever your data is ready.
// /index.js
const store = createDocStore({ authUser: auth.getInitialState() });
// /actions/auth.js
async function login(setAuthUser, { username, password }) {
setAuthUser((authUser) => (authUser.loading = true));
const user = await fetchUser(username, password);
setAuthUser((authUser) => {
authUser.name = user.name;
authUser.loading = false;
});
}
function getInitialState() {
return { name: "", loading: false };
}
// /components/login.js
const [authUser, setAuthUser] = useDoc("/authUser");
auth.login(setAuthUser, { username: "John", password: "password" });
Observing and intercepting changes at a path
const dispose = store.observe(
"/todos",
(todos, change) => {
console.log("todos has been updated");
console.log("Updated todos: ", todos);
console.log("Patch generated: ", change.patch);
},
1 // depth to observe
);
const dispose = store.intercept(
"/todos",
(todos, change) => {
// return modified patches for some case
if (change.patch.path === "/todos/0/caption") {
return {
...change,
patch: {
...change.patch,
value: {
...change.patch.value,
caption: "first task: " + change.patch.value.caption,
},
},
};
} else if (
// Don't allow adding more than 10 items, return null will stop the change
change.patch.length === 2 &&
change.patch.op === "add" &&
change.patch.path.split("/")[2] > 9
) {
return null;
}
// no modification
return change;
},
2 // depth to intercept
);
Combine separate trees
If you are coming from Redux, you might be wondering how to have different state trees with their separate login(reducers) and combine them using something like combineReducers.
SyncState is document based because it works on JSON patches that update a single document in the store. This makes it easy to sync this document across network or threads and create a multi-user realtime app.
You can keep the logic separate for different parts of your app just by having different actions
import * as posts from "./actions/posts";
import * as auth from "./actions/auth";
// ./index.js
const store = createDocStore({
auth: auth.getInitialState(),
posts: posts.getInitialState(),
});
// ./actions/auth.js
async function login(setAuth, { username, password }) {
const response = await fetchUser(username, password);
setAuth((auth) => {
auth.user = response.user;
auth.token = response.token;
});
}
function getInitialState() {
return {};
}
// ./actions/posts
async function addPost(setPosts, post) {
setPosts((posts) => {
posts.push(post);
});
}
function getInitialState() {
return [];
}
Sync a document with another document
You can observe a document in a store for changes and apply the changes to a document in a another store. The other store may be on another thread or a server or another client.
const store1 = createDocStore({ count: 0 });
const store2 = createDocStore({ count: 0 });
const dispose1 = store1.observe([], (newValue, change) => {
if (origin !== "store2")
// exclude own patches
// SyncState's internal reducer will apply this patch to store2
store2.dispatch({
type: "PATCH",
payload: { ...change, origin: "store1" },
});
});
const dispose2 = store2.observe([], (newValue, change) => {
if (origin !== "store1")
// exclude own patches
store1.dispatch({
type: "PATCH",
payload: { ...change, origin: "store2" },
});
});