Custom response types with React portals
Learn how to use your React application to render custom responses.
Overview
If you are rendering custom response types and have an existing React application. You can use React portals to render your custom response type as part of your application. For more details on rendering custom response types in general, refer to our Custom response types
tutorial for details.
The short summary for using React is that you will need to save the HTML elements provided by the chat widget as part of the custom response event to your application. Then you can attach your React components to those elements using React portals.
You can now add Carbon Components into your custom responses to help them fit stylistically within web chat. Your custom response types can just simply render the Carbon components of your choosing and web chat will apply the Carbon styles automatically. In this tutorial you will create a custom response type in React which will render a Carbon date picker.
Instructions
- In your top level application, add code that will load the web chat and grab the instance once created.
- Once the web chat instance is created, render a container that will be responsible for holding all the portals for all the custom responses.
- Create the container that will listen for custom response events from web chat and will create and attach a portal for each one.
- Create the custom component that will render the contents of each message and use it in each portal.
Capture the widget instance
Before we begin, install the [official Watson Assistant web chat with React package](https://github.com/watson-developer-cloud/assistant-web-chat-react).
npm install @ibm-watson/assistant-web-chat-react
This package allows you to wrap your component with a higher order component to inject `createWebChatInstance` as a prop on App.js.
In your App component, you will call the passed `createWebChatInstance` function. When web chat is loaded, you will use the built in `onLoad` configuration option to add the passed instance to state to be able to use easily inside your React application. Update your App.js
to look like the following example code:
App.js
import React, { useEffect, useState } from 'react';
import { useWebChat } from '@ibm-watson/assistant-web-chat-react';
import CustomResponsePortalsContainer from './CustomResponsePortalsContainer';
function App({ createWebChatInstance }) {
const [instance, setInstance] = useState(null);
useEffect(() => {
const watsonAssistantChatOptions = {
integrationID: 'YOUR_INTEGRATION_ID',
region: 'YOUR_REGION',
onLoad: wacInstance => {
setInstance(wacInstance);
wacInstance.render();
},
}
createWebChatInstance(watsonAssistantChatOptions);
}, []);
return (
<div className="App">
{instance && <CustomResponsePortalsContainer instance={instance} />}
</div>
);
}
export default withWebChat()(App);
Custom responses using portals
Now that we have code to capture the widget instance, the next thing to do is utilize the instance to handle custom response types coming in and render our custom responses using React portals. To create React portals for your custom responses, you can create a component that looks something like the code below.
CustomResponsePortalsContainer.js
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import CustomResponseComponent from './CustomResponseComponent';
/**
* We need to use a component here because the "customResponseHandler" is registered only once on the chat instance
* and it needs to be able to access the previous state so it can add elements to it. You can't do that using react
* hooks since any callbacks have to be recreated if their dependencies change.
*/
function CustomResponsePortalsContainer({ instance }) {
// This state will be used to record all of the custom response events that are fired from the widget.
// These events contain the HTML elements that we will attach our portals to as well as the messages that we wish to
// render in the message.
const [customResponseEvents, setCustomResponseEvents] = useState([]);
// When the component is mounted, register the custom response handler that will store the references to the custom
// response events.
useEffect(() => {
// This handler will fire each time a custom response occurs and we will update our state by appending the event
// to the end of our elements list.
function customResponseHandler(event) {
// Use functional updates since the state is computed and we can get the previous value of the events array.
// Passing in a reference to customResponseEvents and concatenating to it will not work since this function will
// capture the initial value of customResponseEvents, which is empty, and not updates made to it.
setCustomResponseEvents(eventsArray => eventsArray.concat(event));
}
instance.on({ type: 'customResponse', handler: customResponseHandler });
// Remove the custom response handler.
return () => {
instance.off({ type: 'customResponse', handler: customResponseHandler });
};
}, [instance]);
// All we need to do to enable the React portals is to render each portal somewhere in your application (it
// doesn't really matter where).
return (
<>
{customResponseEvents.map(function mapEvent(event, index) {
return (
// eslint-disable-next-line
<CustomResponseComponentPortal key={index} hostElement={event.data.element}>
{/* This is your custom response component. It can be whatever you like that renders the given message
in whatever manner your application needs. */}
<CustomResponseComponent message={event.data.message} />
</CustomResponseComponentPortal>
);
})}
</>
);
}
CustomResponsePortalsContainer.propTypes = {
instance: PropTypes.object.isRequired,
};
/**
* This is the component that will attach a React portal to the given host element. The host element is the element
* provided by the chat widget where your custom response will be displayed in the DOM. This portal will attached
* any React children passed to it under this component so you can render the response using your own React
* application. Those children will be rendered under the given element where it lives in the DOM.
*/
function CustomResponseComponentPortal({ hostElement, children }) {
return ReactDOM.createPortal(children, hostElement);
}
export default CustomResponsePortalsContainer;
Custom response component
Next, we will create the custom response component to render in <UsingWithReactClass>
. This custom response component can be whatever will fit your application's needs. In this tutorial we will have it render a Carbon date picker. Your custom response will inherit Carbon theming/styling.
Before we begin, install these Carbon packages using either npm
or yarn
.
carbon-components@10.45 carbon-components-react@7.45 @carbon/icons-react@10.40 carbon-icons@7.0.7
Then, create the following custom response component in your application.
CustomResponseComponent.js
import React, { useCallback, useState } from 'react';
import { DatePicker, DatePickerInput} from 'carbon-components-react';
import PropTypes from 'prop-types';
/**
* Your custom response component can also make use of the message object if you have set "user_defined" variables.
*/
function CustomResponseComponent({ message }) {
const [datePickerElement, setDatePickerElement] = useState(null);
return (
<>
{datePickerElement && (
<DatePicker datePickerType="range" appendTo={datePickerElement}>
<DatePickerInput id="date-picker-input-id-start" placeholder="mm/dd/yyyy" labelText="Start date" />
<DatePickerInput id="date-picker-input-id-finish" placeholder="mm/dd/yyyy" labelText="End date" />
</DatePicker>
)}
{/* This is where the date picker popup will appear. */}
<div ref={setDatePickerElement} style={{ position: 'relative' }} />
</>
)
}
CustomResponseComponent.propTypes = {
message: PropTypes.object,
};
export default CustomResponseComponent;
Your application is complete and a Carbon date picker should be rendered when web chat receives a custom response type.