SyncState

SyncState

  • Docs
  • FAQ
  • Github
  • Hire The Creators

›Recipes

Introduction

  • Getting started
  • Installation
  • Motivation
  • Core Concepts

Recipes

  • Recipes

Examples

  • Counter
  • Counter with Undo / Redo
  • Todo app
  • Multi User Counter
  • Multi User Todo
  • Multi User Todo With Undo/Redo

Plugins

  • History
  • Remote

API Reference

  • API Overview
  • createDocStore
  • Store
  • Provider
  • useDoc
  • useSyncState

Tips & Tricks

  • Common Pitfalls & Best Practices
  • Using SyncState without React
  • Performance Tuning

FAQ

  • FAQ's

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" },
        });
});
← Core ConceptsCounter →
  • Fetching the root state/document
  • Fetching state at a path
  • Replacing vs mutating state
  • Actions
  • Async actions
  • Observing and intercepting changes at a path
  • Combine separate trees
  • Sync a document with another document
Docs
Getting StartedExamplesPluginsAPI ReferenceFAQ
Community
TwitterStack OverflowDiscord
More
GitHub   Contribution Guidelines
Stars
Built with ❤️ at GeekyAnts.
Copyright © 2020 SyncState