Skip to main content

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" },
});
});
Last updated on by RohitGeekyAnts