-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add YorkieProvider, DocumentProvider and suspense hooks (#946)
YorkieProvider and DocumentProvider are React context providers that enable easy integration of Yorkie clients and documents into React applications. They also provide convenient hooks for accessing and updating shared documents in real-time. ```tsx <YorkieProvider apiKey={API_KEY}> <DocumentProvider docKey={DOC_KEY} initialRoot={{}} > <App /> </DocumentProvider> </YorkieProvider> // ... export default function App() { const { root, update, loading, error } = useDocument(); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; // ... } ```
- Loading branch information
1 parent
1111636
commit 7c98a12
Showing
14 changed files
with
340 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,22 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom/client'; | ||
import App from './App'; | ||
import { DocumentProvider, YorkieProvider } from '@yorkie-js/react'; | ||
|
||
const initalRoot = { | ||
todos: [ | ||
{ id: 0, text: 'Yorkie JS SDK', completed: false }, | ||
{ id: 1, text: 'Garbage collection', completed: false }, | ||
{ id: 2, text: 'RichText datatype', completed: false }, | ||
], | ||
}; | ||
|
||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | ||
<App />, | ||
<YorkieProvider apiKey={import.meta.env.VITE_YORKIE_API_KEY}> | ||
<DocumentProvider | ||
docKey={`react-todomvc-${new Date().toISOString().substring(0, 10).replace(/-/g, '')}`} | ||
initialRoot={initalRoot} | ||
> | ||
<App /> | ||
</DocumentProvider> | ||
</YorkieProvider>, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Yorkie React SDK | ||
|
||
Yorkie React SDK is a library that provides React hooks and components for building collaborative applications with Yorkie. | ||
|
||
## How to use React SDK | ||
|
||
To get started using Yorkie React SDK, see: https://yorkie.dev/docs/getting-started/with-react | ||
|
||
## Contributing | ||
|
||
See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. | ||
|
||
## Contributors ✨ | ||
|
||
Thanks goes to these incredible people: | ||
|
||
<a href="https://github.com/yorkie-team/yorkie-js-sdk/graphs/contributors"> | ||
<img src="https://contrib.rocks/image?repo=yorkie-team/yorkie-js-sdk" /> | ||
</a> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
/* | ||
* Copyright 2025 The Yorkie Authors. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { | ||
createContext, | ||
useCallback, | ||
useContext, | ||
useEffect, | ||
useState, | ||
} from 'react'; | ||
import { Document, Presence, Indexable } from '@yorkie-js/sdk'; | ||
import { useYorkie } from './YorkieProvider'; | ||
|
||
type DocumentContextType<R, P extends Indexable = Indexable> = { | ||
root: R; | ||
presences: { clientID: string; presence: P }[]; | ||
update: (callback: (root: R) => void) => void; | ||
loading: boolean; | ||
error: Error | undefined; | ||
}; | ||
|
||
const DocumentContext = createContext<DocumentContextType<any> | null>(null); | ||
|
||
/** | ||
* `DocumentProvider` is a component that provides a document to its children. | ||
* This component must be under a `YorkieProvider` component to initialize the | ||
* Yorkie client properly. | ||
*/ | ||
export const DocumentProvider = <R, P extends Indexable = Indexable>({ | ||
docKey, | ||
initialRoot, | ||
children, | ||
}: { | ||
docKey: string; | ||
initialRoot: R; | ||
children: React.ReactNode; | ||
}) => { | ||
const client = useYorkie(); | ||
const [doc, setDoc] = useState<Document<R, P> | undefined>(undefined); | ||
const [loading, setLoading] = useState<boolean>(true); | ||
const [error, setError] = useState<Error | undefined>(undefined); | ||
const [root, setRoot] = useState(initialRoot); | ||
const [presences, setPresences] = useState< | ||
{ clientID: string; presence: any }[] | ||
>([]); | ||
|
||
useEffect(() => { | ||
setLoading(true); | ||
setError(undefined); | ||
|
||
if (!client) return; | ||
|
||
const newDoc = new Document<R, P>(docKey); | ||
async function attachDocument() { | ||
try { | ||
await client?.attach(newDoc); | ||
|
||
newDoc.subscribe(() => { | ||
setRoot({ ...newDoc.getRoot() }); | ||
}); | ||
|
||
newDoc.subscribe('presence', () => { | ||
setPresences(newDoc.getPresences()); | ||
}); | ||
|
||
setDoc(newDoc); | ||
setRoot({ ...newDoc.getRoot() }); | ||
} catch (err) { | ||
setError( | ||
err instanceof Error ? err : new Error('Failed to attach document'), | ||
); | ||
} finally { | ||
setLoading(false); | ||
} | ||
} | ||
|
||
attachDocument(); | ||
|
||
return () => { | ||
client.detach(newDoc); | ||
}; | ||
}, [client, docKey]); | ||
|
||
const update = useCallback( | ||
(callback: (root: R, presence: Presence<P>) => void) => { | ||
if (!doc) { | ||
return; | ||
} | ||
|
||
try { | ||
doc.update(callback); | ||
} catch (err) { | ||
setError( | ||
err instanceof Error ? err : new Error('Failed to update document'), | ||
); | ||
} | ||
}, | ||
[doc], | ||
); | ||
|
||
return ( | ||
<DocumentContext.Provider | ||
value={{ root, presences, update, loading, error }} | ||
> | ||
{children} | ||
</DocumentContext.Provider> | ||
); | ||
}; | ||
|
||
/** | ||
* `useDocument` is a custom hook that returns the root object and update function of the document. | ||
* This hook must be used within a `DocumentProvider`. | ||
*/ | ||
export const useDocument = <R, P extends Indexable = Indexable>() => { | ||
const context = useContext(DocumentContext); | ||
if (!context) { | ||
throw new Error('useDocument must be used within a DocumentProvider'); | ||
} | ||
return { | ||
root: context.root as R, | ||
presences: context.presences as { clientID: string; presence: P }[], | ||
update: context.update as ( | ||
callback: (root: R, presence: P) => void, | ||
) => void, | ||
loading: context.loading, | ||
error: context.error, | ||
}; | ||
}; | ||
|
||
/** | ||
* `useRoot` is a custom hook that returns the root object of the document. | ||
* This hook must be used within a `DocumentProvider`. | ||
*/ | ||
export const useRoot = () => { | ||
const context = useContext(DocumentContext); | ||
if (!context) { | ||
throw new Error('useRoot must be used within a DocumentProvider'); | ||
} | ||
return { root: context.root }; | ||
}; | ||
|
||
/** | ||
* `usePresences` is a custom hook that returns the presences of the document. | ||
*/ | ||
export const usePresences = () => { | ||
const context = useContext(DocumentContext); | ||
if (!context) { | ||
throw new Error('usePresences must be used within a DocumentProvider'); | ||
} | ||
return context.presences; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* | ||
* Copyright 2025 The Yorkie Authors. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { | ||
createContext, | ||
PropsWithChildren, | ||
useContext, | ||
useEffect, | ||
useState, | ||
} from 'react'; | ||
import { Client } from '@yorkie-js/sdk'; | ||
|
||
const YorkieContext = createContext<{ client: Client | undefined }>({ | ||
client: undefined, | ||
}); | ||
|
||
/** | ||
* `YorkieProviderProps` is a set of properties for `YorkieProvider`. | ||
*/ | ||
interface YorkieProviderProps { | ||
apiKey: string; | ||
rpcAddr?: string; | ||
} | ||
|
||
/** | ||
* `YorkieProvider` is a component that provides the Yorkie client to its children. | ||
* It initializes the Yorkie client with the given API key and RPC address. | ||
*/ | ||
export const YorkieProvider: React.FC< | ||
PropsWithChildren<YorkieProviderProps> | ||
> = ({ apiKey, rpcAddr = 'https://api.yorkie.dev', children }) => { | ||
const [client, setClient] = useState<Client | undefined>(undefined); | ||
|
||
useEffect(() => { | ||
async function initClient() { | ||
const newClient = new Client(rpcAddr, { | ||
apiKey, | ||
}); | ||
await newClient.activate(); | ||
setClient(newClient); | ||
} | ||
initClient(); | ||
|
||
return () => { | ||
client?.deactivate({ keepalive: true }); | ||
}; | ||
}, [apiKey, rpcAddr]); | ||
|
||
return ( | ||
<YorkieContext.Provider value={{ client }}> | ||
{children} | ||
</YorkieContext.Provider> | ||
); | ||
}; | ||
|
||
export const useYorkie = () => { | ||
const context = useContext(YorkieContext); | ||
return context?.client; | ||
}; |
Oops, something went wrong.