Repository navigation
feat(react): add <ChatCops> component and useChatCops() hook (#573) - #583
Saifjafri212213 wants to merge 3 commits into
Conversation
anurag629
left a comment
There was a problem hiding this comment.
Thanks @Saifjafri212213, this mostly works. The StrictMode handling is right: the act flag at packages/react/src/index.ts:77-81 drops the first effect's late import, and because ChatCops.on() handlers outlive init() (packages/widget/src/index.ts:42-46), a re-init keeps the listeners without subscribing again. Server rendering never touches window, and the package setup follows CONTRIBUTING.md:244: the peers, exports, scripts, tsconfig, coverage floor, api/react.api.md and the size entry are all there.
A few things need to change before it can go in:
-
A function prop re-inits the widget on every render.
eq()atpackages/react/src/index.ts:37-38sees two different functions as a change, anduserTokenandonConsentRequestaren't in the update list, sodiff()returnsreinitat line 63. WithuserToken={async () => getToken()}, the form the server docs recommend, any re-render of the parent callsChatCops.init(), which destroys the running widget (packages/widget/src/index.ts:38-40) and stops the reply in progress (packages/widget/src/widget.ts:648). An open chat closes because a cart count changed, andwebsite/src/content/docs/widget/react.mdx:87promises the opposite. Keep the latest function inrefthe way the event props work, and giveinit()a wrapper that calls it, so a new arrow isn't a change.containerhas a related problem: two elements go throughJSON.stringify, which gives{}for any element React didn't render (so a new container is missed) and throws "Converting circular structure to JSON" for one it did, because of React's__reactFiber$property. ComparecontainerwithObject.is. Tests: re-rendering with a new inlineuserTokenleavesinitat one call, and movingcontainerto another element inits again. Please also say on the docs page that any other prop change starts the widget over, which closes the panel and stops a reply in progress. -
Removing a prop doesn't reach the widget. Line 60 leaves out keys whose new value is
undefined, so going fromwelcomeMessage="Hi"to nowelcomeMessagecallsupdate({})and "Hi" stays, though the widget shows the locale's welcome forundefined(widget.ts:762-763). The same goes forsuggestions,placeholder,privacyUrlanddisclosure. Send those asundefined:update()spreadschanges, so that puts the default back.branding,stringsandthemeare merged instead (widget.ts:569-580), so droppingavatarfrombranding={{ name: 'Acme', avatar: '/a.png' }}keeps the avatar. When one of those three loses a field, init again. Tests: removingwelcomeMessagecallsupdate({ welcomeMessage: undefined }), andbrandinglosingavatarcallsinit(). -
The package needs
'use client'. Without it,<ChatCops apiUrl="/api/chat" />can't be rendered from a Next.js server component, whichapp/layout.tsxis by default, because the component callsuseEffect. The docs get around that by putting'use client'on the root layout (react.mdx:21), but then the layout can't exportmetadata, and most root layouts do. Put'use client';on the first line ofsrc/index.ts(tsckeeps the directive indist/index.js). Then change the docs example to a small client component that holds the event props, since functions can't be passed from a server component, and import it from the server layout. -
packages/react/package.json:3starts at1.1.1, copied from the widget.@chatcops/ingestand@chatcops/clistarted at0.1.0with aminorchangeset, and at1.1.1this one would go out as1.2.0. Start it at0.1.0. -
Two gaps in the tests. The StrictMode test checks
initwithtoHaveBeenCalled()(packages/react/tests/react.test.tsx:111), so two inits would pass. Make ittoHaveBeenCalledTimes(1). Nothing covers unmounting before the import resolves, theif (!act) returnatindex.ts:81, which is what a fast route change does: render, unmount straight away, wait, and expect noinit(). The whole file mocks@chatcops/widget, so one StrictMode test in a second file against the real widget would also help: expect one.chatcops-rootindocument.bodyafter mounting and none after unmounting. -
The "AI assistance" section doesn't name the tool you used. CONTRIBUTING asks for the tool and what you used it for.
These fixes will probably take dist/index.js past the 1 kB budget (your description has it at 999 B, and the directive alone adds bytes). Write them with readable names anyway and post the new size here, and we'll look at the budget then. The branch is also behind dev, with no conflicts, and dev only merges branches that are up to date, so merge dev in when you push. I'll review it again then.
|
Thanks @anurag629 for the thorough review! I've addressed all the requested changes:
|
anurag629
left a comment
There was a problem hiding this comment.
Thanks @Saifjafri212213, all six points are in. A new inline userToken or onConsentRequest goes through the wrappers at packages/react/src/index.ts:58-66 instead of starting the widget over, container is compared with Object.is, and removing welcomeMessage sends update({ welcomeMessage: undefined }). branding, strings and theme losing a field inits again, which covers exactly the keys update() merges (packages/widget/src/widget.ts:569-580). The new unmount test covers the if (!act) return at line 137, the real-widget test checks for one .chatcops-root under StrictMode, 'use client' is on line 1, and the package starts at 0.1.0. All checks pass, including e2e.
Summary
Adds the
@chatcops/reactpackage with a<ChatCops>component anduseChatCops()hook for React and Next.js applications (F-12 tasks 2 to 4).Closes #573
What was done
@chatcops/react:CONTRIBUTING.md:244("private": true,"type": "module", package exports,build,typecheck, andtestscripts).0.1.0.'use client';directive to support Next.js server component layout usage.@chatcops/widget(workspace:^) andreact(>=18) as peer dependencies.<ChatCops {...props} />:null, safe for Server-Side Rendering (SSR).@chatcops/widgeton mount insideuseEffectwithout touchingwindowduring SSR.open,close,message,error,leadCaptured,preChatSubmit) usingChatCops.on(), keeping callbacks in a ref to avoid resubscribing on re-render.userToken,onConsentRequest) are kept in refs and passed as stable wrappers toinit(), preventing re-renders from re-initializing the widget.containeris compared usingObject.is(avoiding circular JSON issues on React fiber elements).ChatCops.update()(branding,welcomeMessage,suggestions,placeholder,strings,theme,privacyUrl,disclosure) throughupdate().welcomeMessage) sendsundefinedtoupdate().branding,strings, orthemeloses a field, re-initializes cleanly.apiUrl,mode,container, ortheme.position).useChatCops()hook:{ open, close, toggle, send, update }controlling the widget instance.send()returns a resolved promise)..size-limit.jsonwith a 1.5 kB gzipped budget.website/src/content/docs/widget/react.mdxdocumenting setup with Next.js client component pattern, metadata export support, props, updates, anduseChatCops().website/astro.config.mjs.pnpm docs:check.How I tested this
packages/react/tests/react.test.tsx(21 tests) andpackages/react/tests/real-widget.test.tsx(1 test against the real@chatcops/widget) covering:toHaveBeenCalledTimes(1)), and unmount cleanup..chatcops-rootappears and is removed on unmount.userTokenfunction prop leaves init at 1 call.containerto another element inits again; same container does not re-init.welcomeMessagecallsupdate({ welcomeMessage: undefined }).brandinglosingavatarcallsinit().themeorstringslosing a field inits again.renderToString.pnpm test:coverageverifying 100% statements, 85.71% branches, and 100% lines on@chatcops/react(exceeding the 85/80 threshold).pnpm lint(0 errors)pnpm publint(All good)pnpm api:check(Snapshots up to date)pnpm size(1.36 kB gzipped vs 1.5 kB limit)pnpm docs:check(All 205 doc samples validated)pnpm -r build(Full workspace built clean)AI assistance
Written with an AI coding assistant (Google Antigravity / Gemini) for boilerplate generation, diff logic, and test authoring. I reviewed all lines, updated diff/ref handling, verified test coverage, and ran the checks above.