In much of your development with the Flex UI, your component state can be informed by information that already lives in Flex - for example, you can get access to the current Task and render the Task data in your custom component.
There are some instances, however, in which adding information to a Task might compromise security, or even just add unnecessary data to the Task itself. In these cases, you can extend the Redux store of your Flex UI and pass the new subscription information to your custom components.
On this page, we'll cover two strategies for modifying the Flex Redux reducer. The first relies on the Plugin Builder to extend your contact center that's hosted on flex.twilio.com. The second strategy involves directly modifying the way Flex builds Redux, and is ideal if you're planning to host Flex on your own infrastructure.
Redux is a software package that helps developers manage application state. Flex uses Redux to manage a bunch of application state—for example, a new Task appearing in the UI or an agent changing from Available
to Busy
are both examples of the application state changing. Redux offers you some nice features:
Check out the Redux documentation to learn more about all the great features it offers and become a Redux master. You can also get a useful overview of Redux in the Code Cartoon Intro to Redux.
If you are building your first Flex plugin, you will need to add a new folder and some files to help manage your UI state with Redux.
For the sake of reading this page, the content of the necessary new files is located in the code samples on this page, so don't stress out if you can't set up your first plugin just yet.
Flex 2.0 ships with the immensely helpful Redux Toolkit package, enabling you to create the necessary reducers and Actions to power your Redux-based application state, but without the dreaded boilerplate that is typically associated with Redux.
First you'll see a complete example of a reducer implementation using Redux Toolkit, then we'll break down each major segment so that you may understand how to wrangle Redux on your own.
1import { createSlice } from '@reduxjs/toolkit';23// Define the initial state for your reducer4const initialState = {5isOpen: true,6};78// Create your reducer and actions in one function call9// with the createSlice utility10export const customTaskListSlice = createSlice({11name: 'customTaskList',12initialState,13reducers: {14setOpen: (state, action) => {15// Instead of recreating state, you can directly mutate16// state values in these reducers. Immer will handle the17// immutability aspects under the hood for you18state.isOpen = action.payload;19},20},21});2223// You can now export your reducer and actions24// with none of the old boilerplate25export const { setOpen } = customTaskListSlice.actions;26export default customTaskListSlice.reducer;
In the dark times before Redux Toolkit, you would start this process by first thinking about the shape of your application state, then the Actions that could be dispatched to Redux to initiate changes to that state. This required lots of repetitive boilerplate and SCREAMING_CASE constants, such as:
const SET_OPEN = 'SET_OPEN';
Now, this is necessary to an extent so that Redux is able to uniquely identify your Actions and determine what effect they should have on your state. However, defining and maintaining your own string constants to identify Actions is better left to a library (Redux Toolkit, in this case).
Instead, you can create a Slice, which tells Redux toolkit to automatically define unique Action identifiers on your behalf, and associate them with the corresponding Reducer logic of the same name, all in one function call.
For example, when you see
1reducers: {2setOpen: (state, action) => {...},3},
in the sample code, Redux Toolkit generates an Action with the type
of "customTaskList/setOpen"
. The createSlice
function creates Actions by combining the name of the slice, and the name of each case in your Reducer into unique strings.
Now that you have an idea of how to succinctly create an Action, let's look at how you can create a Reducer to handle said Action.
You may have seen code like this used to describe a Reducer:
1export function reduce(state = initialState, action) {2switch (action.type) {3case ACTION_SET_OPEN: {4return {5...state,6isOpen: action.payload,7};8}910default:11return state;12}13}
This is a fundamental of Redux: a Reducer takes your current application state and an Action, and returns an updated state based on the Action.
So, what is the application state? If you'll recall from the first sample code:
1const initialState = {2isOpen: true,3};
This JavaScript object reflects the slice of application state that's modified by this Reducer.
A Reducer is usually one long switch statement, with the various case
s that correspond to your different Actions. In this case (ha! See what we did there?) the Reducer deals with two cases: if it sees an Action with the ACTION_SET_OPEN
identifier, it changes the isOpen
value to whatever Boolean value was passed to the Action. Otherwise, if it doesn't recognize the Action identifier (or there isn't one), it'll just maintain the current application state and your UI will not update.
You might also notice that the Reducer is creating a brand-new state each time it is executed, instead of breaking immutability by directly changing the value of isOpen
. The Reducer does this by using the spread operator to clone the existing state, spread its properties to a new object, and then inject a value for isOpen
that will override the previous value. This works and is how Reducers have been written for years. However, this results in creating brand-new objects every time the Reducer runs (which could pose performance penalties if done frequently enough). Also, while the syntax doesn't look too messy in this example, it can quickly become a mess if you need to update a property located several levels deep in the state object.
With the createSlice
API from Redux Toolkit, you define your Reducer's logic at the same time as you create the associated Action. Not only that, but you might have noticed that the Reducer is directly mutating the state
object:
1reducers: {2setOpen: (state, action) => {3state.isOpen = action.payload;4},5},
Now, technically this code is still immutable, and it isn't directly mutating state (which would prevent Redux from detecting any changes or updating your UI, yikes!). This is because Redux Toolkit uses Immer under the hood to manage Redux state, and the value of state
in each Reducer function is actually an Immer Draft. If you're using TypeScript, you can hover over state and see that it is type WritableDraft<CustomTaskListState>
, not merely CustomTaskListState
.
What this means is:
state
and action
as arguments (already a Redux pattern)createSlice
(syntax shown in the sample)Now that you have some Redux state, Actions, and Reducers, you'll need to make all of that available to Flex.
src
folder. The path should be src/states
.src/states
directory, and create a new CustomTaskListState.js
file (or CustomTaskListState.ts
).src/states/CustomTaskListState.js
file, and save.index.js
(or index.ts
) file in the same directory, copy-paste the following sample code below, and save.1import { combineReducers } from '@reduxjs/toolkit';2import customTaskReducer, { setOpen } from './customTaskListState';34// You need to register your redux store(s) under a unique namespace5export const namespace = 'pluginState';67// It can be helpful to create a map of all actions8export const actions = {9customTaskList: {10setOpen,11},12};1314// Combine any number of reducers to support the needs of your plugin15export const reducers = combineReducers({16customTaskList: customTaskReducer,17});
src/YourPluginName.js
(or .ts
) file, replace its contents with the corresponding sample code from below, and save.1import React from 'react';2import { FlexPlugin } from '@twilio/flex-plugin';34import CustomTaskList from './components/CustomTaskList/CustomTaskList';5import { namespace, reducers } from './states';67const PLUGIN_NAME = 'ReduxSamplePlugin';89export default class SamplePlugin extends FlexPlugin {10constructor() {11super(PLUGIN_NAME);12}1314/**15* This code is run when your plugin is being started16* Use this to modify any UI components or attach to the actions framework17*18* @param flex { typeof import('@twilio/flex-ui') }19* @param manager { import('@twilio/flex-ui').Manager }20*/21async init(flex, manager) {22manager.store.addReducer(namespace, reducers);2324const options = { sortOrder: -1 };25flex.AgentDesktopView.Panel1.Content.add(26<CustomTaskList key="ReduxSamplePlugin-component" />,27options28);29}30}
What exactly is all of this code accomplishing?
First, the index file serves as a place to import all of your Reducers, and combine them into a single Reducer which can be added to Flex, as well as creating a helpful mapping of all Actions. If you're using TypeScript, it's also an excellent place to create an overall typed interface for your Application state and Actions, which will lead to fantastic autocomplete when accessing your state (and Actions) in your React components.
Next, your plugin file imports your Reducer, and adds it to the existing Flex UI Redux store so that it is accessible across your entire application and by other potential plugins. It does so by using the manager.store.addReducer
method, which registers the given reducer under the provided namespace key.
That's great, but how can you now access that state, and dispatch Actions to modify it from your app? Let's cover that now.
Now that your Reducer logic is defined and integrated with Flex, you need to connect all of that logic to the UI. In the Plugin Builder sample app, this will happen in the existing src/components/CustomTaskList/CustomTaskList.tsx
(or .jsx
) file.
Like with the Redux sample, let's start with the complete code, and break it down:
1import React from 'react';2import { useSelector, useDispatch } from 'react-redux';3import { Alert } from '@twilio-paste/core/alert';4import { Theme } from '@twilio-paste/core/theme';5import { Text } from '@twilio-paste/core/text';67import { actions } from '../../states';89const CustomTaskList = () => {10const isOpen = useSelector(11(state) => state.pluginState.customTaskList.isOpen12);13const dispatch = useDispatch();1415const dismiss = () => dispatch(actions.customTaskList.setOpen(false));1617if (!isOpen) {18return null;19}2021return (22<Theme.Provider theme="default">23<Alert onDismiss={dismiss} variant="neutral">24<Text>This is a dismissible demo component.</Text>25</Alert>26</Theme.Provider>27);28};2930export default CustomTaskList;
When it comes to accessing Redux state, there is a best practice of using Selectors. Selectors allow you to efficiently access values within your Redux state, and will only tell your React component to re-render if the value targeted by the selector is changed by an Action.
Nowadays, it is best practice to leverage the useSelector hook to access state. (In older codebases, this is similar to the mapStateToProps function for connect, but we don't live in those times anymore, thankfully)
You can see this in action inside the component, here:
1const isOpen = useSelector(2(state: AppState) => state.pluginState.customTaskList.isOpen3);
With the useSelector
hook, the component is able to access the full Redux store, then the plugin's namespace (which we named pluginState
earlier), and so on until you have the value needed by your component's logic. If you are using TypeScript and defined your state types earlier, this entire process will be typed and provide excellent autocomplete to speed up development.
You can then do any valid TypeScript/JavaScript action based on that value, such as conditionally preventing rendering of the component:
1if (!isOpen) {2return null;3}
To update Redux state, you must dispatch Actions to Redux. You can gain access to the dispatch
method, which enables you to dispatch Actions, from any React component by using the useDispatch
hook. You can see this pattern in the CustomTaskList
component example:
1const dispatch = useDispatch();23const dismiss = () => dispatch(actions.customTaskList.setOpen(false));
In order to dispatch an Action, call dispatch
with that Action as an argument, and provide the Action with whatever payload
it might require (not all Actions require a payload). Redux will notice the dispatched Action, grab any attached payload
(and error
or meta
properties, if defined), and execute the related reducer function to update your app's state.
Similar to state, if you are using TypeScript and were careful to type your Actions, your Action creators will provide autocompletion as well as errors if you provide a mistyped payload
.
payload
, meta
, and error
are part of the Flux Standard Actions specification, and its best practice to make sure all Actions contain only these properties (and of course, the type
identifier).
As you can see, Redux helps you create a distributed flow of data through your whole plugin. The user interacts with the UI component, which invokes the dispatch
function. The dispatch
function sends out the relevant Action, which is observed by the reducers. The reducer takes whatever information is associated with that Action and modifies the Redux Store to reflect what's going on. At long last, the UI component, which is subscribed to the Redux Store, detects a new state and re-renders to reflect the new application state!
Redux has a complex data flow, but once you've mastered it, it makes reasoning about and testing complex apps—like the UI for an Omnichannel Contact Center—much easier. Thanks to tools like Redux Toolkit, this process is significantly faster and loaded with less boilerplate than it used to be, as well.
You are free to modify and add more state to your Plugin's component. Inside src/states/CustomTaskListState.ts
(or .js
), update the type definition (if applicable) of your state, and define a new counter in your initialState
object called taskCounter
.
Next, update the existing call to createSlice
to include a new Action/Reducer pair that increments the task counter, the updated reducer will look like this:
1import { createSlice } from '@reduxjs/toolkit';23// Define the initial state for your reducer4const initialState = {5isOpen: true,6};78// Create your reducer and actions in one function call9// with the createSlice utility10export const customTaskListSlice = createSlice({11name: 'customTaskList',12initialState,13reducers: {14setOpen: (state, action) => {15// Instead of recreating state, you can directly mutate16// state values in these reducers. Immer will handle the17// immutability aspects under the hood for you18state.isOpen = action.payload;19},20},21});2223// You can now export your reducer and actions24// with none of the old boilerplate25export const { setOpen } = customTaskListSlice.actions;26export default customTaskListSlice.reducer;
With the reducer updated, you will need to make a few tiny adjustments to the src/states/index
file so that it will export the new Action that you just created. You'll notice that the only change here is the added import
of incrementTasks
, and its addition to the actions
map.
1import { combineReducers } from '@reduxjs/toolkit';2import customTaskReducer, { setOpen } from './customTaskListState';34// You need to register your redux store(s) under a unique namespace5export const namespace = 'pluginState';67// It can be helpful to create a map of all actions8export const actions = {9customTaskList: {10setOpen,11},12};1314// Combine any number of reducers to support the needs of your plugin15export const reducers = combineReducers({16customTaskList: customTaskReducer,17});
Alright, now that the reducer and Actions are updated, they can be referenced and used by your CustomTaskList
component. Open the CustomTaskList
component once again.
Now, you'll add a new selector to track the counter value, some new JSX to render the counter and a button, and use the button to dispatch new incrementTasks
Actions.
1import React from 'react';2import { useSelector, useDispatch } from 'react-redux';3import { Alert } from '@twilio-paste/core/alert';4import { Theme } from '@twilio-paste/core/theme';5import { Text } from '@twilio-paste/core/text';67import { actions } from '../../states';89const CustomTaskList = () => {10const isOpen = useSelector(11(state) => state.pluginState.customTaskList.isOpen12);13const dispatch = useDispatch();1415const dismiss = () => dispatch(actions.customTaskList.setOpen(false));1617if (!isOpen) {18return null;19}2021return (22<Theme.Provider theme="default">23<Alert onDismiss={dismiss} variant="neutral">24<Text>This is a dismissible demo component.</Text>25</Alert>26</Theme.Provider>27);28};2930export default CustomTaskList;
Your plugin now has multiple state values in Redux, and components that not only react to state changes, but are able to update the state as well from anywhere in the component tree.
Managing local state is a great use case for Redux, but most applications involve asynchronous code that takes some time to complete, like network requests.
Flex UI includes the redux-promise middleware, which enables you to dispatch Actions with asynchronous behavior.
Suppose you want to have an Action that, when dispatched, triggers a request for an image, which then is saved to your state and rendered in the UI. You also want to communicate to your user that something is in progress, say, with a spinner.
An example looks like this:
1// Helper method that returns a promise, which will resolve to a2// dog image URL3const getCompanion = async () => {4const response = await fetch('https://dog.ceo/api/breeds/image/random');5const data = await response.json();6return data.message;7};89// We're manually creating a Redux Action in this instance, so10// define a unique string identifier for the fetch dog Action11const FETCH_DOG = 'customTaskList/fetchDog';12export const fetchDog = () => {13return {14type: FETCH_DOG,15payload: getCompanion(),16};17};
Without the presence of redux-promise-middleware
, dispatching this Action would case your app to throw an error and crash. That's because this Action doesn't return an object containing a type
and an image string as the payload
— the payload is a Promise which will resolve to the image string at some point in the future. By default, Redux only accepts plain objects, no Promises.
If you need to brush up on your understanding of Promises, check out Mozilla's Promise API Docs to learn more.
Thankfully, Flex's inclusion of redux-promise-middleware
, means that this type of Action is valid to dispatch. The middleware intercepts any Actions that contain a Promise, and replaces them with a sequence of pending, fulfilled, or rejected Actions that reflect each state of a Promise.
First, it will dispatch a pending Action, which is useful for rendering loading spinners and letting your user know that something is happening. Next, if the Promise resolves successfully, it will dispatch a fulfilled Action containing whatever data the Promise returns. Otherwise, the Promise ran into a failure, and the middleware will dispatch a rejected Action which contains information related to the error that occurred.
Assuming your original Action had a type
of 'FETCH_DOG'
, the three replacement Actions that redux-promise-middleware
will generate are:
'FETCH_DOG_PENDING'
'FETCH_DOG_FULFILLED'
'FETCH_DOG_REJECTED'
Let's look at how you would modify your CustomTaskListState
to take advantage of this behavior:
1import { createSlice } from '@reduxjs/toolkit';23// Define the initial state for your reducer4const initialState = {5isOpen: true,6};78// Create your reducer and actions in one function call9// with the createSlice utility10export const customTaskListSlice = createSlice({11name: 'customTaskList',12initialState,13reducers: {14setOpen: (state, action) => {15// Instead of recreating state, you can directly mutate16// state values in these reducers. Immer will handle the17// immutability aspects under the hood for you18state.isOpen = action.payload;19},20},21});2223// You can now export your reducer and actions24// with none of the old boilerplate25export const { setOpen } = customTaskListSlice.actions;26export default customTaskListSlice.reducer;
This update to the code introduces:
fetchDog
You might be wondering about this new extraReducers
property that's been added to the Slice. You can read more about it in the official Redux Toolkit docs, but essentially extraReducers
is how you add extra cases to the reducer that you're creating, even if the Action wasn't created by the same Slice.
The Action has been created, and your reducer knows how to handle every variation of the request that it creates. To see this in… action, you'll need to update your UI to dispatch this new Action:
1import React from 'react';2import { useSelector, useDispatch } from 'react-redux';3import { Alert } from '@twilio-paste/core/alert';4import { Theme } from '@twilio-paste/core/theme';5import { Text } from '@twilio-paste/core/text';67import { actions } from '../../states';89const CustomTaskList = () => {10const isOpen = useSelector(11(state) => state.pluginState.customTaskList.isOpen12);13const dispatch = useDispatch();1415const dismiss = () => dispatch(actions.customTaskList.setOpen(false));1617if (!isOpen) {18return null;19}2021return (22<Theme.Provider theme="default">23<Alert onDismiss={dismiss} variant="neutral">24<Text>This is a dismissible demo component.</Text>25</Alert>26</Theme.Provider>27);28};2930export default CustomTaskList;
In short, this updated component pulls in the new values from Redux, dispatches the new Action when it appears in the UI, and includes some new rendering logic to keep the UI responsive to the network request.
If you reload your plugin with this updated code, you should very briefly see a spinner appear in the UI before it is replaced with a cute dog avatar. Asynchronous Redux at its finest.
You may apply this pattern in your app in a variety of ways, such as firing requests on button clicks, or triggering timers as a user navigates between routes.
You might be wondering why this async Action is being created with redux-promise-middleware
in mind, instead of using the createAsyncThunk helper from Redux Toolkit.
Unfortunately, Flex does not currently ship with the necessary middleware, redux-thunk
, and this is not yet supported.
If you've built your own Redux application, you can extend your own UI to include all of the stateful goodness in Flex. This option is only recommended if you already have an existing React app - otherwise, Plugins are likely a better choice. The following sample code is a brief example of how you can integrate Flex into your own application.
1import React from 'react';2import ReactDOM from 'react-dom/client';3import {4configureStore5} from '@reduxjs/toolkit';6import Flex from '@twilio/flex-ui';78import myReducer from './myReducerLocation';9import configuration from './appConfig';101112// Configure a new Redux store13const store = configureStore({14// Add the Flex reducer to your existing reducers15reducer: {16app: myReducer,17flex: Flex.FlexReducer,18},19middleware: (getDefaultMiddleware) => getDefaultMiddleware({20// if you are using the default Redux middlewares, make sure to disable21// 'serializableCheck' and 'immutableCheck', as they are not compatible22// with the Flex UI Reducers23serializableCheck: false,24immutableCheck: false,25}).concat([...Flex.getFlexMiddleware()]),26enhancers: [27Flex.flexStoreEnhancer28]29})3031// Flex is instantiated with the new Redux store,32// which includes your custom reducers33Flex.Manager.create(configuration, store).then((manager) => {34const root = ReactDOM.createRoot(document.getElementById('root'));35root.render(36<Provider store={store}>37<Flex.ContextProvider manager={manager}>38<Flex.RootContainer />39</Flex.ContextProvider>40</Provider>41);42});43