fix: yjs collaboration plugin in react strict mode#6271
fix: yjs collaboration plugin in react strict mode#6271Sahejkm merged 6 commits intofacebook:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
|
Hi @meronogbai! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
size-limit report 📦
|
f6319c4 to
b700713
Compare
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
StyleT
left a comment
There was a problem hiding this comment.
Hi! Thanks for the fix but npm run tsc is failing. Can you pls fix it?
|
Could you use |
|
@fantactuka I believe I did that already. I renamed
The blame info hasn't changed as you can see in https://github.com/facebook/lexical/blame/e6982e727b59eddaf69b25ba8fd32a9e837d969b/packages/lexical-react/src/LexicalCollaborationPlugin.tsx |
e6982e7 to
9430398
Compare
|
@meronogbai Hi! Took another look at this.. So we still call Why? From my experience Here is simple test that you can add to this PR and make it pass it: /**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { CollaborationPlugin } from '@lexical/react/LexicalCollaborationPlugin';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'shared/react-test-utils';
import * as Y from 'yjs';
describe(`LexicalCollaborationPlugin`, () => {
let container: HTMLDivElement;
let reactRoot: Root;
const editorConfig = Object.freeze({
// NOTE: This is critical for collaboration plugin to set editor state to null. It
// would indicate that the editor should not try to set any default state
// (not even empty one), and let collaboration plugin do it instead
editorState: null,
namespace: 'Test editor',
nodes: [],
// Handling of errors during update
onError(error: Error) {
throw error;
},
});
beforeEach(() => {
container = document.createElement('div');
reactRoot = createRoot(container);
document.body.appendChild(container);
});
test(`providerFactory called only once`, () => {
const providerFactory = jest
.fn((id: string, yjsDocMap: Map<string, Y.Doc>) => {
const doc = new Y.Doc();
yjsDocMap.set(id, doc);
return {
awareness: {
getLocalState: () => null,
getStates: () => new Map(),
off: () => {},
on: () => {},
setLocalState: () => {},
},
connect: () => {},
disconnect: () => {},
off: () => {},
on: () => {},
}
});
function MemoComponent() {
return (<LexicalComposer initialConfig={editorConfig}>
{/* With CollaborationPlugin - we MUST NOT use @lexical/react/LexicalHistoryPlugin */}
<CollaborationPlugin
id="lexical/react-rich-collab"
providerFactory={providerFactory}
// Unless you have a way to avoid race condition between 2+ users trying to do bootstrap simultaneously
// you should never try to bootstrap on client. It's better to perform bootstrap within Yjs server.
shouldBootstrap={false}
/>
<RichTextPlugin
contentEditable={<ContentEditable className="editor-input" />}
placeholder={<div className="editor-placeholder">Enter some rich text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
</LexicalComposer>);
}
ReactTestUtils.act(() => {
reactRoot.render(
<React.StrictMode>
<MemoComponent />
</React.StrictMode>,
);
});
expect(providerFactory).toHaveBeenCalledTimes(1);
});
});How to make code pass this test? Use "dirty" hack from here https://taig.medium.com/prevent-react-from-triggering-useeffect-twice-307a475714d7 |
|
Thank you so much @StyleT! I pushed a commit with your suggested changes 🫡 |
872f301 to
9810d0a
Compare
|
I'm good to merge this, seems like test failures are unrelated |
|
Thanks for fixing this! Would it be possible to release a 0.16.1 version with this bug fix? |

Description
The collaboration plugin doesn't work well in nextjs 14.2 with strict mode enabled. See videos below to see how it breaks. I fixed the issues by relying on a useEffect instead of a useMemo to initialize yjs binding and provider.
Relevant thread
https://discord.com/channels/953974421008293909/1233102329931366531/1233102329931366531
Test plan
LIVEBLOCKS_SECRET_KEYto the root.env.localfile. Alternatively, I can send you a secret key as well.npm run devand test the editor on multiple tabs/windows/browsers.LexicalCollaborationPluginfile into node_modules.nextfolder because nextjs caches built modules.rm -r .next && npm run devBefore
https://www.loom.com/share/460727ea35b04e26a311d55d5283be3
After
https://www.loom.com/share/d3a2701d4ef6456a9bd7c9a202d38ebe
Note
I tried to runNever mind, it was just an error with my changes.npm run build-release, but it didn't work due to #5420 so I had to compile "manually".