18 KiB
Chatwoot React Components - Architecture Documentation
This document provides comprehensive context for the embeddable chat UI components built with React by wrapping Vue components as Web Components.
Table of Contents
- Overview
- Architecture Diagram
- Build System
- Component Hierarchy
- Key Patterns
- Global State & Configuration
- Real-time Communication
- File Structure
- Extension Guide
- Known Limitations
Overview
The system enables embedding Chatwoot's conversation UI into any React application. It achieves this through a three-layer architecture:
- React Layer - Provider pattern for configuration and React-friendly API
- Web Component Layer - Vue components converted to Custom Elements via
defineCustomElement - Vue Layer - Existing Chatwoot Vue components reused without modification
Why This Approach?
- Code Reuse: Leverage existing battle-tested Vue components (
components-next/message/*) - Framework Agnostic: Web Components work in any framework (React, Angular, vanilla JS)
- Isolated Styles: Shadow DOM encapsulation prevents CSS conflicts
- Shared State: Single Vuex store instance across all components
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ React Application │
├─────────────────────────────────────────────────────────────────┤
│ <ChatwootProvider> │
│ ├── Sets up globals (window.__WOOT_*) │
│ ├── Initializes Vuex store │
│ ├── Registers Web Components │
│ └── Initializes ActionCable (WebSocket) │
│ │
│ <ChatwootConversation> │
│ └── <ChatwootMessageListWrapper> │
│ └── <chatwoot-message-list> (Web Component) │
│ └── [Shadow DOM] │
│ └── MessageList.vue │
│ ├── Message.vue (components-next) │
│ ├── TypingIndicator.vue │
│ └── LiteReplyBox.vue │
└─────────────────────────────────────────────────────────────────┘
Build System
Build Modes (vite.config.ts)
The Vite configuration supports multiple build modes via BUILD_MODE environment variable:
| Mode | Command | Output | Purpose |
|---|---|---|---|
library |
pnpm build:sdk |
public/packs/js/sdk.js |
Widget SDK (IIFE) |
ui |
pnpm build:ui |
public/packs/js/ui.js |
Standalone UI (IIFE) |
react-components |
pnpm build:react |
public/packs/react-components.{es,cjs}.js |
NPM package (ES + CJS) |
| (default) | bin/vite build |
Standard Vite output | Main app build |
React Components Build Pipeline
# Full build and package
pnpm package:react
# Build only (no packaging)
pnpm build:react
# Copy mode (skip build, just package)
node scripts/package-react-components.js --copyMode
Build Output (dist/react-components/):
index.js- ES module entryindex.cjs- CommonJS entrystyle.css- Bundled stylespackage.json- Generated with timestamp versioning
Plugin Configuration
// react-components mode plugins
plugins = [
vue({ ...vueOptions, customElement: true }), // Enable CE mode
react() // React JSX transform
];
// Rollup externals - React is peer dependency
rollupOptions = {
external: ['react', 'react-dom'],
};
Component Hierarchy
React Components (src/components/)
ChatwootProvider.jsx
Purpose: Root provider that initializes the entire Chatwoot environment.
Key Responsibilities:
- Validates required props (
baseURL,userToken) - Registers Vue Web Components via
registerVueWebComponents() - Sets up global window variables for Vue components
- Initializes axios with authentication
- Dispatches
setUserto hydrate auth state - Initializes ActionCable for real-time updates
Props:
interface ChatwootProviderProps {
baseURL: string; // Required: Chatwoot API URL
userToken: string; // Required: Agent access token
accountId: number; // Account ID
conversationId: number; // Conversation to display
websocketURL?: string; // WebSocket server URL
pubsubToken?: string; // PubSub authentication token
disableUpload?: boolean; // Disable file attachments
disableEditor?: boolean; // Disable reply editor
}
Global Variables Set:
window.__WOOT_API_HOST__ // API base URL
window.__WOOT_ACCOUNT_ID__ // Account ID
window.__WOOT_ACCESS_TOKEN__ // Auth token (used by axios)
window.__WEBSOCKET_URL__ // WebSocket URL
window.__PUBSUB_TOKEN__ // Real-time auth token
window.__WOOT_CONVERSATION_ID__ // Active conversation
window.__EDITOR_DISABLE_UPLOAD__ // Upload flag
window.__DISABLE_EDITOR__ // Editor flag
window.__WOOT_ISOLATED_SHELL__ // Disables audio notifications
window.__CHATWOOT_STORE__ // Vuex store reference
window.WootConstants // Global constants
window.axios // Configured axios instance
ChatwootConversation.jsx
Purpose: High-level wrapper providing styled container.
Pattern: Uses useChatwoot() hook to enforce Provider context.
ChatwootMessageListWrapper.jsx
Purpose: Bridge between React and Web Component.
Key Pattern - Imperative Handle Updates:
// React props → Web Component properties
useEffect(() => {
element.conversationId = conversationId;
}, [conversationId]);
Event Forwarding:
element.addEventListener('chatwoot:loaded', handleLoad);
element.addEventListener('chatwoot:error', handleError);
Web Component Layer (src/vue-components/)
registerWebComponents.js
Purpose: Converts Vue SFCs to Web Components and registers them.
Key Pattern - Style Injection:
const ceOptions = {
configureApp(app) {
app.use(store);
app.use(VueDOMPurifyHTML, domPurifyConfig);
app.use(i18n);
app.provide(I18nInjectionKey, i18n);
vueActionCable.init(store, window.__PUBSUB_TOKEN__);
},
// Styles bundled into Shadow DOM
styles: [chatwootStyles, multiselectStyles, floatingVueStyles, uploadStyles],
};
const ChatwootMessageListElement = defineCustomElement(
ChatwootMessageListWebComponent,
ceOptions
);
Important: Styles are imported with ?inline query to get CSS as string:
import chatwootStyles from '../../../dashboard/assets/scss/app.scss?inline';
ChatwootMessageListWebComponent.vue
Purpose: Thin wrapper that renders the actual MessageList.
Pattern - Additional Styles for Third-Party Components:
<style>
/* vue-upload-component requires these styles */
.file-uploads { /* ... */ }
</style>
Vue Layer (/app/javascript/ui/)
MessageList.vue
Purpose: Core conversation view with infinite scroll.
Key Features:
- Infinite Scroll: Uses
@vueuse/core'suseInfiniteScroll - Typing Indicators: Real-time via
conversationTypingStatusstore module - Message Rendering: Uses
components-next/message/Message.vue - Reply Box: Integrated
LiteReplyBox.vue
Data Flow:
// Gets conversation ID from global
const conversationId = computed(() => window.__WOOT_CONVERSATION_ID__);
// Fetches conversation data on mount
onMounted(async () => {
await store.dispatch('inboxes/get');
await Promise.all([
store.dispatch('getConversation', conversationId.value),
store.dispatch('fetchAllAttachments', conversationId.value),
]);
});
Key Pattern - Store Access Without Vue Instance:
// From composables/store.js
export const useStore = () => {
if (window.__CHATWOOT_STORE__) {
return window.__CHATWOOT_STORE__;
}
// Fallback to Vue instance
const vm = getCurrentInstance();
return vm.proxy.$store;
};
LiteReplyBox.vue
Purpose: Message composition editor.
Key Features:
- Rich text editing via ProseMirror (
WootMessageEditor) - File attachments with preview
- Typing indicators (on/off status)
- Keyboard shortcuts (Cmd/Ctrl+Enter to send)
- Private notes support
Conditional Features via Globals:
isEditorDisabled() {
return window.__DISABLE_EDITOR__;
},
allowFileUpload() {
return window.__EDITOR_DISABLE_UPLOAD__ !== true;
},
axios.js (UI-specific axios factory)
Purpose: Creates axios instance configured for external use.
Pattern - Token from Global:
export default axios => {
const apiHost = window.__WOOT_API_HOST__;
const accessToken = window.__WOOT_ACCESS_TOKEN__;
const wootApi = axios.create({ baseURL: `${apiHost}/` });
Object.assign(wootApi.defaults.headers.common, {
api_access_token: accessToken,
});
return wootApi;
};
Key Patterns
1. Global Variable Bridge
Vue components expect certain globals. The React provider sets these before rendering:
// Set by ChatwootProvider
window.__WOOT_API_HOST__ = config.baseURL;
window.__WOOT_ACCOUNT_ID__ = config.accountId;
// ...etc
// Consumed by Vue components
const accountIdFromRoute = window.__WOOT_ACCOUNT_ID__;
2. Vuex Store as Global Singleton
The store is created once and shared via window.__CHATWOOT_STORE__:
// In ChatwootProvider
import store from '../../../dashboard/store';
window.__CHATWOOT_STORE__ = store;
// In Vue composables
export const useStore = () => {
if (window.__CHATWOOT_STORE__) {
return window.__CHATWOOT_STORE__;
}
// ...
};
3. I18n in Web Components
Vue I18n requires special injection for Web Components:
// Must provide I18nInjectionKey for useI18n() to work in CE mode
app.use(i18n);
app.provide(I18nInjectionKey, i18n);
Reference: https://vue-i18n.intlify.dev/guide/advanced/wc
4. Isolated Shell Mode
The __WOOT_ISOLATED_SHELL__ flag disables features not needed in embedded mode:
// In actionCable.js
if (!window.__WOOT_ISOLATED_SHELL__) {
DashboardAudioNotificationHelper.onNewMessage(data);
}
5. CamelCase Transformation
API returns snake_case, Vue components expect camelCase:
import { useCamelCase } from '../dashboard/composables/useTransformKeys';
const allMessages = computed(() => {
return useCamelCase(conversation.value.messages, { deep: true }).reverse();
});
6. Web Component Event Communication
Web Components emit custom events that React can listen to:
// In Web Component
this.$emit('chatwoot:loaded');
this.$emit('chatwoot:error', { message: 'Error details' });
// In React wrapper
element.addEventListener('chatwoot:loaded', handleLoad);
element.addEventListener('chatwoot:error', handleError);
Real-time Communication
ActionCable Setup
// BaseActionCableConnector.js
const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined;
this.consumer = createConsumer(websocketURL);
this.subscription = this.consumer.subscriptions.create({
channel: 'RoomChannel',
pubsub_token: pubsubToken,
account_id: app.$store.getters.getCurrentAccountId,
user_id: app.$store.getters.getCurrentUserID,
});
Events Handled
| Event | Action |
|---|---|
message.created |
Add message to conversation |
message.updated |
Update existing message |
conversation.typing_on |
Show typing indicator |
conversation.typing_off |
Hide typing indicator |
presence.update |
Update online status |
conversation.updated |
Refresh conversation data |
Presence Updates
Heartbeat every 20 seconds:
const PRESENCE_INTERVAL = 20000;
this.triggerPresenceInterval = () => {
setTimeout(() => {
this.subscription.updatePresence();
this.triggerPresenceInterval();
}, PRESENCE_INTERVAL);
};
File Structure
app/javascript/
├── react-components/
│ ├── src/
│ │ ├── index.jsx # Package exports
│ │ ├── components/
│ │ │ ├── ChatwootProvider.jsx # Root provider
│ │ │ ├── ChatwootConversation.jsx # Container component
│ │ │ └── ChatwootMessageListWrapper.jsx # WC bridge
│ │ ├── vue-components/
│ │ │ ├── registerWebComponents.js # WC registration
│ │ │ └── ChatwootMessageListWebComponent.vue
│ │ └── styles/
│ │ └── upload.css # File upload styles
│ └── doc.md # This file
│
├── ui/
│ ├── MessageList.vue # Core message list
│ ├── LiteReplyBox.vue # Reply editor
│ ├── axios.js # Axios factory
│ └── usage.md # Usage docs
│
├── dashboard/
│ ├── store/ # Vuex store
│ │ ├── index.js
│ │ └── modules/
│ │ ├── conversations/ # Conversation state
│ │ ├── auth.js # Auth state
│ │ └── conversationTypingStatus.js
│ ├── components-next/
│ │ └── message/ # Message components
│ │ ├── Message.vue
│ │ ├── TypingIndicator.vue
│ │ └── bubbles/
│ ├── composables/
│ │ ├── store.js # Store helpers
│ │ └── useTransformKeys.js # Case conversion
│ ├── helper/
│ │ ├── actionCable.js # WebSocket connector
│ │ └── commons.js # Utility functions
│ └── api/
│ ├── ApiClient.js # Base API client
│ └── auth.js # Auth API
│
└── shared/
└── helpers/
└── BaseActionCableConnector.js # WebSocket base class
scripts/
└── package-react-components.js # NPM packaging script
vite.config.ts # Multi-mode build config
Extension Guide
Adding a New Web Component
- Create Vue Component (
vue-components/NewComponent.vue):
<script setup>
import { useStore, useMapGetter } from 'dashboard/composables/store';
// Use composables that check window.__CHATWOOT_STORE__
</script>
- Register Web Component (
registerWebComponents.js):
import NewComponent from './NewComponent.vue';
const NewElement = defineCustomElement(NewComponent, ceOptions);
export const registerVueWebComponents = () => {
// ...existing registrations
if (!customElements.get('chatwoot-new-component')) {
customElements.define('chatwoot-new-component', NewElement);
}
};
- Create React Wrapper (
components/NewComponentWrapper.jsx):
export const NewComponentWrapper = (props) => {
const elementRef = useRef();
useEffect(() => {
// Sync React props to WC properties
}, [props]);
return <chatwoot-new-component ref={elementRef} />;
};
- Export from index.jsx:
export { NewComponentWrapper } from './components/NewComponentWrapper';
Adding New Global Configuration
- Add prop to
ChatwootProvider.jsx - Set corresponding
window.__WOOT_*variable - Access in Vue components via
window.__WOOT_*
Adding Store Modules
If the new component needs additional Vuex state:
- Ensure module is already in
dashboard/store/index.js - Access via
useStore().dispatch('module/action')oruseMapGetter('module/getter')
Known Limitations
1. Single Conversation Per Page
Currently, only one conversation can be displayed at a time due to global state (__WOOT_CONVERSATION_ID__).
Workaround: For multiple conversations, instantiate separate iframes.
2. Style Isolation
Shadow DOM prevents parent CSS from affecting components, but also means:
- Parent theme variables don't cascade
- Global CSS resets don't apply
Workaround: Pass theme configuration via props/globals.
3. React DevTools
Web Components don't appear in React DevTools component tree. Use browser's native Custom Elements inspector.
4. Hot Module Replacement
Vue components inside Web Components don't support HMR well. Full page reload often needed during development.
5. Bundle Size
The react-components bundle includes:
- Full Vuex store
- All SCSS styles
- I18n messages
Future optimization: Tree-shake unused store modules.
6. Single Language (English Only)
Currently hardcoded to English locale:
const i18n = createI18n({
locale: 'en',
messages: { en },
});
Debugging Tips
Inspect Vuex Store
// In browser console
window.__CHATWOOT_STORE__.state
window.__CHATWOOT_STORE__.getters.getConversationById(123)
Check WebSocket Connection
// ActionCable consumer status
window.__CHATWOOT_STORE__._modules.root._rawModule // Check if actions dispatch
Verify Web Component Registration
customElements.get('chatwoot-message-list') // Should return constructor
Force Re-render
// Dispatch any action to trigger reactivity
window.__CHATWOOT_STORE__.dispatch('setActiveChat', { data: { id: conversationId } });
Related Files
CLAUDE.md- General Chatwoot development guidelinesapp/javascript/ui/usage.md- UI components usage guidevite.config.ts- Build configuration documentation (inline comments)