Skip to content

Commit

Permalink
chore: merge pull request #47 from joebobmiles/fix/issue-41
Browse files Browse the repository at this point in the history
Fix for issue #41. Patching the Zustand store directly modifies the underlying state object due to pass-by-reference. Cloning the state object avoids this issue.
  • Loading branch information
joebobmiles authored Jun 1, 2023
2 parents 380dea5 + 0bc2931 commit 1403ad2
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 3 deletions.
74 changes: 74 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,4 +797,78 @@ describe("Yjs middleware in React", () =>

expect(typeof result.current.someOtherData.foo).toBe("function");
});

/**
* See Issue 41.
*/
it("Zustand is properly notified of updates from remote peer.", () =>
{
type Store =
{
count: number,
increment: () => void,
};

const doc1 = new Y.Doc();
const doc2 = new Y.Doc();

doc1.on("update", (update: any) =>
{
Y.applyUpdate(doc2, update);
});

doc2.on("update", (update: any) =>
{
Y.applyUpdate(doc1, update);
});

const useStore1 =
create<Store>(yjs(
doc1,
"hello",
(set) =>
({
"count": 0,
"increment": () =>
set((state) =>
({ "count": state.count + 1, })),
})
));


const useStore2 =
create<Store>(yjs(
doc2,
"hello",
(set) =>
({
"count": 0,
"increment": () =>
set((state) =>
({ "count": state.count + 1, })),
})
));

const { "result": result1, } = renderHook(() =>
useStore1(({ count, increment, }) =>
({
"count": count,
"increment": increment,
})));

const { "result": result2, } = renderHook(() =>
useStore2(({ count, increment, }) =>
({
"count": count,
"increment": increment,
})));

act(() =>
{
result1.current.increment();
});

expect(doc2.getMap("hello").get("count")).toBe(1); // Sanity check
expect(result2.current.count).toBe(1); // Actual issue
});
});
11 changes: 8 additions & 3 deletions src/patching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Y from "yjs";
import { ChangeType, Change, } from "./types";
import { getChanges, } from "./diff";
import { arrayToYArray, objectToYMap, stringToYText, } from "./mapping";
import { State, StoreApi, } from "zustand/vanilla";
import { StoreApi, } from "zustand/vanilla";

/**
* Diffs sharedType and newState to create a list of changes for transforming
Expand Down Expand Up @@ -256,14 +256,19 @@ export const patchState = (oldState: any, newState: any): any =>
* @param store The Zustand API that manages the store we want to patch.
* @param newState The new state that the Zustand store should be patched to.
*/
export const patchStore = <S extends State>(
export const patchStore = <S extends unknown>(
store: StoreApi<S>,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
newState: any
): void =>
{
// Clone the oldState instead of using it directly from store.getState().
const oldState = {
...(store.getState() as Record<string, unknown>),
};

store.setState(
patchState(store.getState() || {}, newState),
patchState(oldState, newState),
true // Replace with the patched state.
);
};

0 comments on commit 1403ad2

Please sign in to comment.