Skip to content

Commit 324af4f

Browse files
authored
Use provider hooks for single hook (#970)
This commit adds support for StrictMode in React's standalone mode, updating the interface to be on par with the Provider mode. Additionally, it removes existing duplicate logic to streamline the codebase and enhance maintainability.
1 parent 2799b8b commit 324af4f

File tree

4 files changed

+153
-132
lines changed

4 files changed

+153
-132
lines changed

examples/react-flow/src/main.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client';
22
import { DocumentProvider, YorkieProvider } from '@yorkie-js/react';
33
import './index.css';
44
import App from './App';
5+
import { StrictMode } from 'react';
56

67
const initialNodes = [
78
{
@@ -42,18 +43,21 @@ const initialGraph = {
4243
};
4344

4445
createRoot(document.getElementById('root')!).render(
45-
<YorkieProvider
46-
apiKey={import.meta.env.VITE_YORKIE_API_KEY}
47-
rpcAddr={import.meta.env.VITE_YORKIE_API_ADDR}
48-
>
49-
<DocumentProvider
50-
docKey={`react-flow-${new Date()
51-
.toISOString()
52-
.substring(0, 10)
53-
.replace(/-/g, '')}`}
54-
initialRoot={initialGraph}
46+
<StrictMode>
47+
<YorkieProvider
48+
apiKey={import.meta.env.VITE_YORKIE_API_KEY}
49+
rpcAddr={import.meta.env.VITE_YORKIE_API_ADDR}
5550
>
56-
<App />
57-
</DocumentProvider>
58-
</YorkieProvider>,
51+
<DocumentProvider
52+
docKey={`react-flow-${new Date()
53+
.toISOString()
54+
.substring(0, 10)
55+
.replace(/-/g, '')}`}
56+
initialRoot={initialGraph}
57+
>
58+
<App />
59+
</DocumentProvider>
60+
</YorkieProvider>
61+
,
62+
</StrictMode>,
5963
);

packages/react/src/DocumentProvider.tsx

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,34 @@ import {
2626
Presence,
2727
Indexable,
2828
StreamConnectionStatus,
29+
Client,
2930
} from '@yorkie-js/sdk';
3031
import { useYorkie } from './YorkieProvider';
3132

32-
type DocumentContextType<R, P extends Indexable = Indexable> = {
33-
root: R;
34-
presences: { clientID: string; presence: P }[];
35-
connection: StreamConnectionStatus;
36-
update: (callback: (root: R) => void) => void;
37-
loading: boolean;
38-
error: Error | undefined;
39-
};
40-
41-
const DocumentContext = createContext<DocumentContextType<any> | null>(null);
42-
4333
/**
44-
* `DocumentProvider` is a component that provides a document to its children.
45-
* This component must be under a `YorkieProvider` component to initialize the
46-
* Yorkie client properly.
34+
* `useYorkieDocument` is a custom hook that initializes a Yorkie document.
35+
* @param client
36+
* @param clientLoading
37+
* @param clientError
38+
* @param docKey
39+
* @param initialRoot
40+
* @param initialPresence
41+
* @returns
4742
*/
48-
export const DocumentProvider = <R, P extends Indexable = Indexable>({
49-
docKey,
50-
initialRoot = {} as R,
51-
initialPresence = {} as P,
52-
children,
53-
}: {
54-
docKey: string;
55-
initialRoot?: R;
56-
initialPresence?: P;
57-
children?: React.ReactNode;
58-
}) => {
59-
const { client, loading: clientLoading, error: clientError } = useYorkie();
60-
const [doc, setDoc] = useState<Document<R, P> | undefined>(undefined);
61-
const [loading, setLoading] = useState<boolean>(true);
62-
const [error, setError] = useState<Error | undefined>(undefined);
43+
export function useYorkieDocument<R, P extends Indexable = Indexable>(
44+
client: Client | undefined,
45+
clientLoading: boolean,
46+
clientError: Error | undefined,
47+
docKey: string,
48+
initialRoot: R,
49+
initialPresence: P,
50+
) {
51+
const [doc, setDoc] = useState<Document<R, P>>();
52+
const [loading, setLoading] = useState(true);
53+
const [error, setError] = useState<Error>();
6354
const [root, setRoot] = useState(initialRoot);
6455
const [presences, setPresences] = useState<
65-
{ clientID: string; presence: any }[]
56+
Array<{ clientID: string; presence: P }>
6657
>([]);
6758
const [connection, setConnection] = useState<StreamConnectionStatus>(
6859
StreamConnectionStatus.Disconnected,
@@ -152,6 +143,54 @@ export const DocumentProvider = <R, P extends Indexable = Indexable>({
152143
},
153144
[doc],
154145
);
146+
return {
147+
doc,
148+
root,
149+
presences,
150+
connection,
151+
update,
152+
loading,
153+
error,
154+
};
155+
}
156+
157+
type DocumentContextType<R, P extends Indexable = Indexable> = {
158+
root: R;
159+
presences: { clientID: string; presence: P }[];
160+
connection: StreamConnectionStatus;
161+
update: (callback: (root: R, presence: Presence<P>) => void) => void;
162+
loading: boolean;
163+
error: Error | undefined;
164+
};
165+
166+
const DocumentContext = createContext<DocumentContextType<any> | null>(null);
167+
168+
/**
169+
* `DocumentProvider` is a component that provides a document to its children.
170+
* This component must be under a `YorkieProvider` component to initialize the
171+
* Yorkie client properly.
172+
*/
173+
export const DocumentProvider = <R, P extends Indexable = Indexable>({
174+
docKey,
175+
initialRoot = {} as R,
176+
initialPresence = {} as P,
177+
children,
178+
}: {
179+
docKey: string;
180+
initialRoot?: R;
181+
initialPresence?: P;
182+
children?: React.ReactNode;
183+
}) => {
184+
const { client, loading: clientLoading, error: clientError } = useYorkie();
185+
const { root, presences, connection, update, loading, error } =
186+
useYorkieDocument(
187+
client,
188+
clientLoading,
189+
clientError,
190+
docKey,
191+
initialRoot,
192+
initialPresence,
193+
);
155194

156195
return (
157196
<DocumentContext.Provider
@@ -176,7 +215,7 @@ export const useDocument = <R, P extends Indexable = Indexable>() => {
176215
presences: context.presences as { clientID: string; presence: P }[],
177216
connection: context.connection,
178217
update: context.update as (
179-
callback: (root: R, presence: P) => void,
218+
callback: (root: R, presence: Presence<P>) => void,
180219
) => void,
181220
loading: context.loading,
182221
error: context.error,

packages/react/src/YorkieProvider.tsx

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,37 +45,38 @@ interface YorkieProviderProps {
4545
}
4646

4747
/**
48-
* `YorkieProvider` is a component that provides the Yorkie client to its children.
49-
* It initializes the Yorkie client with the given API key and RPC address.
48+
* `useYorkieClient` is a custom hook that initializes a Yorkie client.
49+
* @param apiKey
50+
* @param rpcAddr
51+
* @returns
5052
*/
51-
export const YorkieProvider: React.FC<
52-
PropsWithChildren<YorkieProviderProps>
53-
> = ({ apiKey, rpcAddr = 'https://api.yorkie.dev', children }) => {
53+
export function useYorkieClient(apiKey: string, rpcAddr: string) {
5454
const [client, setClient] = useState<Client | undefined>(undefined);
5555
const [loading, setLoading] = useState<boolean>(true);
5656
const [error, setError] = useState<Error | undefined>(undefined);
57+
const [didMount, setDidMount] = useState(false);
5758

58-
// NOTE(hackerwins): We use `useRef` to keep the client instance to prevent
59-
// re-creating the client instance in StrictMode.
60-
const clientRef = useRef<Client | undefined>(undefined);
59+
// NOTE(hackerwins): In StrictMode, the component will call twice
60+
// useEffect in development mode. To prevent creating a new client
61+
// twice, create a client after the mounting.
62+
useEffect(() => {
63+
setDidMount(true);
64+
}, []);
6165

6266
useEffect(() => {
6367
setLoading(true);
6468
setError(undefined);
6569

6670
async function activateClient() {
67-
try {
68-
if (clientRef.current?.isActive()) {
69-
setClient(clientRef.current);
70-
setLoading(false);
71-
return;
72-
}
71+
if (!didMount) {
72+
return;
73+
}
7374

75+
try {
7476
const newClient = new Client(rpcAddr, {
7577
apiKey,
7678
});
7779
await newClient.activate();
78-
clientRef.current = newClient;
7980
setClient(newClient);
8081
} catch (e) {
8182
setError(
@@ -88,12 +89,23 @@ export const YorkieProvider: React.FC<
8889
activateClient();
8990

9091
return () => {
91-
if (clientRef.current?.isActive()) {
92-
clientRef.current.deactivate({ keepalive: true });
93-
clientRef.current = undefined;
92+
if (client?.isActive()) {
93+
client.deactivate({ keepalive: true });
9494
}
9595
};
96-
}, [apiKey, rpcAddr]);
96+
}, [apiKey, rpcAddr, didMount]);
97+
98+
return { client, loading, error };
99+
}
100+
101+
/**
102+
* `YorkieProvider` is a component that provides the Yorkie client to its children.
103+
* It initializes the Yorkie client with the given API key and RPC address.
104+
*/
105+
export const YorkieProvider: React.FC<
106+
PropsWithChildren<YorkieProviderProps>
107+
> = ({ apiKey, rpcAddr = 'https://api.yorkie.dev', children }) => {
108+
const { client, loading, error } = useYorkieClient(apiKey, rpcAddr);
97109

98110
return (
99111
<YorkieContext.Provider value={{ client, loading, error }}>
@@ -102,6 +114,10 @@ export const YorkieProvider: React.FC<
102114
);
103115
};
104116

117+
/**
118+
* `useYorkie` is a custom hook that returns the Yorkie client and its loading state.
119+
* @returns
120+
*/
105121
export const useYorkie = () => {
106122
const context = useContext(YorkieContext);
107123
if (!context) {

packages/react/src/useYorkieDoc.ts

Lines changed: 31 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,96 +14,58 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useCallback, useEffect, useState } from 'react';
18-
import { Client, Document } from '@yorkie-js/sdk';
17+
import { Indexable, Presence, StreamConnectionStatus } from '@yorkie-js/sdk';
18+
import { useYorkieClient } from './YorkieProvider';
19+
import { useYorkieDocument } from './DocumentProvider';
1920

2021
/**
21-
* `useYorkieDoc` is a custom hook that initializes a Yorkie client and attaches a document.
22+
* `useYorkieDoc` is a custom hook that initializes a Yorkie Client and a
23+
* document in a single hook.
2224
*
2325
* @param apiKey
2426
* @param docKey
25-
* @param initialRoot
2627
* @returns
2728
*/
28-
export function useYorkieDoc<T>(
29+
export function useYorkieDoc<R, P extends Indexable>(
2930
apiKey: string,
3031
docKey: string,
31-
initialRoot: T,
3232
options?: {
33+
initialRoot?: R;
34+
initialPresence?: P;
3335
rpcAddr?: string;
3436
},
3537
): {
36-
root: T;
37-
update: (callback: (root: T) => void) => void;
38+
root: R;
39+
presences: Array<{ clientID: string; presence: P }>;
40+
connection: StreamConnectionStatus;
41+
update: (callback: (root: R, presence: Presence<P>) => void) => void;
3842
loading: boolean;
3943
error: Error | undefined;
4044
} {
41-
const [client, setClient] = useState<Client | undefined>(undefined);
42-
const [doc, setDoc] = useState<Document<T> | undefined>(undefined);
43-
const [root, setRoot] = useState<T>(initialRoot);
44-
const [loading, setLoading] = useState<boolean>(true);
45-
const [error, setError] = useState<Error | undefined>(undefined);
45+
const rpcAddr = options?.rpcAddr || 'https://api.yorkie.dev';
46+
const initialRoot = options?.initialRoot || ({} as R);
47+
const initialPresence = options?.initialPresence || ({} as P);
4648

47-
useEffect(() => {
48-
setLoading(true);
49-
setError(undefined);
49+
const {
50+
client,
51+
loading: clientLoading,
52+
error: clientError,
53+
} = useYorkieClient(apiKey, rpcAddr);
5054

51-
/**
52-
* `setupYorkie` initializes the Yorkie client and attaches the document.
53-
*/
54-
async function setupYorkie() {
55-
try {
56-
const client = new Client(
57-
options?.rpcAddr || 'https://api.yorkie.dev',
58-
{ apiKey },
59-
);
60-
await client.activate();
61-
62-
const doc = new Document<T>(docKey);
63-
await client.attach(doc, {
64-
initialPresence: {},
65-
initialRoot,
66-
});
67-
68-
doc.subscribe((event) => {
69-
if (event.type === 'remote-change' || event.type === 'local-change') {
70-
setRoot(doc.getRoot());
71-
}
72-
});
73-
74-
setClient(client);
75-
setDoc(doc);
76-
setRoot(doc.getRoot());
77-
} catch (err) {
78-
setError(err instanceof Error ? err : new Error('Unknown error'));
79-
} finally {
80-
setLoading(false);
81-
}
82-
}
83-
84-
setupYorkie();
85-
86-
return () => {
87-
client?.deactivate({ keepalive: true });
88-
};
89-
}, [apiKey, docKey, JSON.stringify(initialRoot), JSON.stringify(options)]);
90-
91-
const update = useCallback(
92-
(callback: (root: T) => void) => {
93-
if (!doc) {
94-
console.warn('Attempted to update document before it was initialized');
95-
return;
96-
}
97-
98-
doc.update((root) => {
99-
callback(root);
100-
});
101-
},
102-
[doc],
103-
);
55+
const { root, presences, connection, update, loading, error } =
56+
useYorkieDocument(
57+
client,
58+
clientLoading,
59+
clientError,
60+
docKey,
61+
initialRoot,
62+
initialPresence,
63+
);
10464

10565
return {
10666
root,
67+
presences,
68+
connection,
10769
update,
10870
loading,
10971
error,

0 commit comments

Comments
 (0)