Skip to content

Commit

Permalink
Use React context for controlled CommentsProvider values
Browse files Browse the repository at this point in the history
  • Loading branch information
12joan committed Nov 23, 2023
1 parent 5cc5908 commit c6fffba
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 61 deletions.
16 changes: 16 additions & 0 deletions .changeset/grumpy-planes-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@udecode/plate-comments': major
---

- Renamed the `comments` prop on CommentsProvider to `initialComments` to reflect the fact that updating its value after the initial render has no effect
- Removed the following props from CommentsProvider, since they represent the internal state of the comments plugin and should not be controlled externally:
- `activeCommentId`
- `addingCommentId`
- `newValue`
- `focusTextarea`
- The following props on CommentsProvider can now be updated after the initial render (whereas prior to this version, doing so had no effect):
- `myUserId`
- `users`
- `onCommentAdd`
- `onCommentUpdate`
- `onCommentDelete`
55 changes: 26 additions & 29 deletions apps/www/content/docs/comments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -276,21 +276,9 @@ An interface representing a user who can make comments.

### CommentsStoreState

An interface representing the state of the comments store.
An interface representing the internal state of the comments store.

<APIState>
<APIItem name="myUserId" type="string | null">
The unique ID of the currently logged in or active user.
</APIItem>
<APIItem name="users" type="Record&lt;string, CommentUser&gt;">
An object where each key is a user's unique ID and the corresponding value
is an instance of the `CommentUser` type, representing the user's data.
</APIItem>
<APIItem name="comments" type="Record&lt;string, TComment&gt;">
An object where each key is a comment's unique ID and the corresponding
value is an instance of the `TComment` type, representing the comment's
data.
</APIItem>
<APIItem name="activeCommentId" type="string | null">
The unique ID of the currently active or focused comment.
</APIItem>
Expand All @@ -303,18 +291,6 @@ An interface representing the state of the comments store.
<APIItem name="focusTextarea" type="boolean">
Indicates whether the comment textarea is focused.
</APIItem>
<APIItem name="onCommentAdd" type="function">
A function that is invoked when a new comment is added. It accepts a partial
comment object, which must include the user's ID.
</APIItem>
<APIItem name="onCommentUpdate" type="function">
A function that is invoked when a comment is updated. It accepts a comment
object, which must include the comment's ID.
</APIItem>
<APIItem name="onCommentDelete" type="function">
A function that is invoked when a comment is deleted. It accepts the unique
ID of the comment to be deleted.
</APIItem>
</APIState>

### TComment
Expand Down Expand Up @@ -563,12 +539,33 @@ A div component for positioning comments in the editor.

### CommentsProvider

A jotai provider for comments data and users data.
A provider for comments data and users data.

<APIProps>

Extends `CommentsStoreState`.

<APIItem name="myUserId" type="string | null">
The unique ID of the currently logged in or active user.
</APIItem>
<APIItem name="users" type="Record&lt;string, CommentUser&gt;">
An object where each key is a user's unique ID and the corresponding value
is an instance of the `CommentUser` type, representing the user's data.
</APIItem>
<APIItem name="initialComments" type="Record&lt;string, TComment&gt;">
An object where each key is a comment's unique ID and the corresponding
value is an instance of the `TComment` type, representing the comment's
data.
</APIItem>
<APIItem name="onCommentAdd" type="function">
A function that is invoked when a new comment is added. It accepts a partial
comment object, which must include the user's ID.
</APIItem>
<APIItem name="onCommentUpdate" type="function">
A function that is invoked when a comment is updated. It accepts a comment
object, which must include the comment's ID.
</APIItem>
<APIItem name="onCommentDelete" type="function">
A function that is invoked when a comment is deleted. It accepts the unique
ID of the comment to be deleted.
</APIItem>
</APIProps>

### useActiveCommentNode
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/lib/plate/demo/comments/CommentsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CommentsProvider as CommentsProviderPrimitive } from '@udecode/plate-co
export function CommentsProvider({ children }: { children: ReactNode }) {
return (
<CommentsProviderPrimitive
comments={commentsData}
initialComments={commentsData}
users={usersData}
myUserId="1"
>
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/lib/plate/demo/values/tableValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@ export const tableValue: any = (
</hp>
{createSpanningTable()}
</fragment>
);
);
100 changes: 70 additions & 30 deletions packages/comments/src/stores/comments/CommentsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import React, { createContext, ReactNode, useContext, useMemo } from 'react';
import {
createAtomStore,
getJotaiProviderInitialValues,
Expand All @@ -11,9 +11,7 @@ import {

import { CommentUser, TComment } from '../../types';

export const SCOPE_COMMENTS = Symbol('comments');

export interface CommentsStoreState {
export interface CommentsContext {
/**
* Id of the current user.
*/
Expand All @@ -24,6 +22,16 @@ export interface CommentsStoreState {
*/
users: Record<string, CommentUser>;

onCommentAdd: ((value: WithPartial<TComment, 'userId'>) => void) | null;
onCommentUpdate:
| ((value: Pick<TComment, 'id'> & Partial<Omit<TComment, 'id'>>) => void)
| null;
onCommentDelete: ((id: string) => void) | null;
}

export const SCOPE_COMMENTS = Symbol('comments');

export interface CommentsStoreState {
/**
* Comments data.
*/
Expand All @@ -39,26 +47,10 @@ export interface CommentsStoreState {
newValue: Value;

focusTextarea: boolean;

onCommentAdd: ((value: WithPartial<TComment, 'userId'>) => void) | null;
onCommentUpdate:
| ((value: Pick<TComment, 'id'> & Partial<Omit<TComment, 'id'>>) => void)
| null;
onCommentDelete: ((id: string) => void) | null;
}

export const { commentsStore, useCommentsStore } = createAtomStore(
{
/**
* Id of the current user.
*/
myUserId: null,

/**
* Users data.
*/
users: {},

/**
* Comments data.
*/
Expand All @@ -74,35 +66,83 @@ export const { commentsStore, useCommentsStore } = createAtomStore(
newValue: [{ type: 'p', children: [{ text: '' }] }],

focusTextarea: false,

onCommentAdd: null,
onCommentUpdate: null,
onCommentDelete: null,
} as CommentsStoreState,
} satisfies CommentsStoreState as CommentsStoreState,
{
name: 'comments',
scope: SCOPE_COMMENTS,
}
);

export const CommentsContext = createContext<CommentsContext>({
myUserId: null,
users: {},
onCommentAdd: null,
onCommentUpdate: null,
onCommentDelete: null,
});

export interface CommentsProviderProps extends Partial<CommentsContext> {
initialComments?: CommentsStoreState['comments'];
children: ReactNode;
}

export function CommentsProvider({
children,
...props
}: Partial<CommentsStoreState> & { children: ReactNode }) {
initialComments,
myUserId = null,
users = {},
onCommentAdd = null,
onCommentUpdate = null,
onCommentDelete = null,
}: CommentsProviderProps) {
return (
<JotaiProvider
initialValues={getJotaiProviderInitialValues(commentsStore, props)}
initialValues={getJotaiProviderInitialValues(commentsStore, {
comments: initialComments,
})}
scope={SCOPE_COMMENTS}
>
{children}
<CommentsContext.Provider
value={{
myUserId,
users,
onCommentAdd,
onCommentUpdate,
onCommentDelete,
}}
>
{children}
</CommentsContext.Provider>
</JotaiProvider>
);
}

export const useCommentsStates = () => useCommentsStore().use;
export const useCommentsSelectors = () => useCommentsStore().get;
export const useCommentsActions = () => useCommentsStore().set;

export const useCommentsSelectors = () => {
const context = useContext(CommentsContext);

const contextGetters = useMemo(
() =>
Object.fromEntries(
Object.entries(context).map(([key, value]) => [key, () => value])
) as { [K in keyof CommentsContext]: () => CommentsContext[K] },
[context]
);

const storeGetters = useCommentsStore().get;

// Combine getters from context and store
return useMemo(
() => ({
...contextGetters,
...storeGetters,
}),
[contextGetters, storeGetters]
);
};

export const useCommentById = (id?: string | null): TComment | null => {
const comments = useCommentsSelectors().comments();
if (!id) return null;
Expand Down

0 comments on commit c6fffba

Please sign in to comment.