Skip to contentSkip to navigationSkip to topbar
On this page

Use Redux with Flex


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.


Brief Intro to Redux

brief-intro-to-redux page anchor

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:

  • A single source of truth about the state of your application (called the store)
  • An interface for dispatching Actions that will update the store
  • An architecture that helps you test and debug state changes, and, perhaps most importantly,
  • An integration with React that allows your React components to subscribe to changes in the store.

Check out the Redux documentation(link takes you to an external page) 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(link takes you to an external page).


Using Redux in Your Flex Plugin

using-redux-in-your-flex-plugin page anchor

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(link takes you to an external page) package, enabling you to create the necessary reducers and Actions to power your Redux-based application state, but without the dreaded boilerplate(link takes you to an external page) 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.

src/states/customTaskListState.jsLink to code sample: src/states/customTaskListState.js
1
import { createSlice } from '@reduxjs/toolkit';
2
3
// Define the initial state for your reducer
4
const initialState = {
5
isOpen: true,
6
};
7
8
// Create your reducer and actions in one function call
9
// with the createSlice utility
10
export const customTaskListSlice = createSlice({
11
name: 'customTaskList',
12
initialState,
13
reducers: {
14
setOpen: (state, action) => {
15
// Instead of recreating state, you can directly mutate
16
// state values in these reducers. Immer will handle the
17
// immutability aspects under the hood for you
18
state.isOpen = action.payload;
19
},
20
},
21
});
22
23
// You can now export your reducer and actions
24
// with none of the old boilerplate
25
export const { setOpen } = customTaskListSlice.actions;
26
export default customTaskListSlice.reducer;

Create Actions

create-actions page anchor

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(link takes you to an external page), 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

1
reducers: {
2
setOpen: (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:

1
export function reduce(state = initialState, action) {
2
switch (action.type) {
3
case ACTION_SET_OPEN: {
4
return {
5
...state,
6
isOpen: action.payload,
7
};
8
}
9
10
default:
11
return 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:

1
const initialState = {
2
isOpen: 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(link takes you to an external page), with the various cases 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(link takes you to an external page) by directly changing the value of isOpen. The Reducer does this by using the spread operator(link takes you to an external page) 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:

1
reducers: {
2
setOpen: (state, action) => {
3
state.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(link takes you to an external page) 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:

  • You can now quickly define your Reducers as a collection of functions that take state and action as arguments (already a Redux pattern)
  • You may use mutative operations in your Reducers, which means cleaner syntax and less noisy Reducer code
  • You can access unique string identifiers for your Actions by name from the result of createSlice (syntax shown in the sample)

Adding Plugin State to the Flex Store

adding-plugin-state-to-the-flex-store page anchor

Now that you have some Redux state, Actions, and Reducers, you'll need to make all of that available to Flex.

  1. In your plugin directory, create a new states folder in the existing src folder. The path should be src/states.
  2. Navigate to the newly-created src/states directory, and create a new CustomTaskListState.js file (or CustomTaskListState.ts).
  3. Copy and paste the contents of the full CustomTaskListState sample code into the new src/states/CustomTaskListState.js file, and save.
  4. Create a new index.js (or index.ts) file in the same directory, copy-paste the following sample code below, and save.
1
import { combineReducers } from '@reduxjs/toolkit';
2
import customTaskReducer, { setOpen } from './customTaskListState';
3
4
// You need to register your redux store(s) under a unique namespace
5
export const namespace = 'pluginState';
6
7
// It can be helpful to create a map of all actions
8
export const actions = {
9
customTaskList: {
10
setOpen,
11
},
12
};
13
14
// Combine any number of reducers to support the needs of your plugin
15
export const reducers = combineReducers({
16
customTaskList: customTaskReducer,
17
});
  1. Open the existing src/YourPluginName.js (or .ts) file, replace its contents with the corresponding sample code from below, and save.
1
import React from 'react';
2
import { FlexPlugin } from '@twilio/flex-plugin';
3
4
import CustomTaskList from './components/CustomTaskList/CustomTaskList';
5
import { namespace, reducers } from './states';
6
7
const PLUGIN_NAME = 'ReduxSamplePlugin';
8
9
export default class SamplePlugin extends FlexPlugin {
10
constructor() {
11
super(PLUGIN_NAME);
12
}
13
14
/**
15
* This code is run when your plugin is being started
16
* Use this to modify any UI components or attach to the actions framework
17
*
18
* @param flex { typeof import('@twilio/flex-ui') }
19
* @param manager { import('@twilio/flex-ui').Manager }
20
*/
21
async init(flex, manager) {
22
manager.store.addReducer(namespace, reducers);
23
24
const options = { sortOrder: -1 };
25
flex.AgentDesktopView.Panel1.Content.add(
26
<CustomTaskList key="ReduxSamplePlugin-component" />,
27
options
28
);
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.


Access Redux from your Plugin Component(s)

access-redux-from-your-plugin-components page anchor

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:

src/components/CustomTaskList/CustomTaskList.jsxLink to code sample: src/components/CustomTaskList/CustomTaskList.jsx
1
import React from 'react';
2
import { useSelector, useDispatch } from 'react-redux';
3
import { Alert } from '@twilio-paste/core/alert';
4
import { Theme } from '@twilio-paste/core/theme';
5
import { Text } from '@twilio-paste/core/text';
6
7
import { actions } from '../../states';
8
9
const CustomTaskList = () => {
10
const isOpen = useSelector(
11
(state) => state.pluginState.customTaskList.isOpen
12
);
13
const dispatch = useDispatch();
14
15
const dismiss = () => dispatch(actions.customTaskList.setOpen(false));
16
17
if (!isOpen) {
18
return null;
19
}
20
21
return (
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
};
29
30
export default CustomTaskList;

When it comes to accessing Redux state, there is a best practice of using Selectors(link takes you to an external page). 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(link takes you to an external page) to access state. (In older codebases, this is similar to the mapStateToProps function for connect(link takes you to an external page), but we don't live in those times anymore, thankfully)

You can see this in action inside the component, here:

1
const isOpen = useSelector(
2
(state: AppState) => state.pluginState.customTaskList.isOpen
3
);

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:

1
if (!isOpen) {
2
return null;
3
}

Access and dispatch Actions

access-and-dispatch-actions page anchor

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:

1
const dispatch = useDispatch();
2
3
const 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.

(information)

Info

payload, meta, and error are part of the Flux Standard Actions(link takes you to an external page) 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.


Add more state to the Redux Store

add-more-state-to-the-redux-store page anchor

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:

1
import { createSlice } from '@reduxjs/toolkit';
2
3
// Define the initial state for your reducer
4
const initialState = {
5
isOpen: true,
6
};
7
8
// Create your reducer and actions in one function call
9
// with the createSlice utility
10
export const customTaskListSlice = createSlice({
11
name: 'customTaskList',
12
initialState,
13
reducers: {
14
setOpen: (state, action) => {
15
// Instead of recreating state, you can directly mutate
16
// state values in these reducers. Immer will handle the
17
// immutability aspects under the hood for you
18
state.isOpen = action.payload;
19
},
20
},
21
});
22
23
// You can now export your reducer and actions
24
// with none of the old boilerplate
25
export const { setOpen } = customTaskListSlice.actions;
26
export 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.

1
import { combineReducers } from '@reduxjs/toolkit';
2
import customTaskReducer, { setOpen } from './customTaskListState';
3
4
// You need to register your redux store(s) under a unique namespace
5
export const namespace = 'pluginState';
6
7
// It can be helpful to create a map of all actions
8
export const actions = {
9
customTaskList: {
10
setOpen,
11
},
12
};
13
14
// Combine any number of reducers to support the needs of your plugin
15
export const reducers = combineReducers({
16
customTaskList: 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.

src/components/CustomTaskList/CustomTaskList.jsxLink to code sample: src/components/CustomTaskList/CustomTaskList.jsx
1
import React from 'react';
2
import { useSelector, useDispatch } from 'react-redux';
3
import { Alert } from '@twilio-paste/core/alert';
4
import { Theme } from '@twilio-paste/core/theme';
5
import { Text } from '@twilio-paste/core/text';
6
7
import { actions } from '../../states';
8
9
const CustomTaskList = () => {
10
const isOpen = useSelector(
11
(state) => state.pluginState.customTaskList.isOpen
12
);
13
const dispatch = useDispatch();
14
15
const dismiss = () => dispatch(actions.customTaskList.setOpen(false));
16
17
if (!isOpen) {
18
return null;
19
}
20
21
return (
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
};
29
30
export 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.


Write asynchronous Actions

write-asynchronous-actions page anchor

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(link takes you to an external page), 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 a
2
// dog image URL
3
const getCompanion = async () => {
4
const response = await fetch('https://dog.ceo/api/breeds/image/random');
5
const data = await response.json();
6
return data.message;
7
};
8
9
// We're manually creating a Redux Action in this instance, so
10
// define a unique string identifier for the fetch dog Action
11
const FETCH_DOG = 'customTaskList/fetchDog';
12
export const fetchDog = () => {
13
return {
14
type: FETCH_DOG,
15
payload: 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.

(information)

Info

If you need to brush up on your understanding of Promises, check out Mozilla's Promise API Docs(link takes you to an external page) 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:

1
import { createSlice } from '@reduxjs/toolkit';
2
3
// Define the initial state for your reducer
4
const initialState = {
5
isOpen: true,
6
};
7
8
// Create your reducer and actions in one function call
9
// with the createSlice utility
10
export const customTaskListSlice = createSlice({
11
name: 'customTaskList',
12
initialState,
13
reducers: {
14
setOpen: (state, action) => {
15
// Instead of recreating state, you can directly mutate
16
// state values in these reducers. Immer will handle the
17
// immutability aspects under the hood for you
18
state.isOpen = action.payload;
19
},
20
},
21
});
22
23
// You can now export your reducer and actions
24
// with none of the old boilerplate
25
export const { setOpen } = customTaskListSlice.actions;
26
export default customTaskListSlice.reducer;

This update to the code introduces:

  • A new asynchronous Action fetchDog
  • Additional state for tracking the image URL and loading state
  • Extra cases to the reducer logic, to handle each of the three middleware-generated Actions.

You might be wondering about this new extraReducers property that's been added to the Slice. You can read more about it(link takes you to an external page) 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:

src/components/CustomTaskList/CustomTaskList.jsxLink to code sample: src/components/CustomTaskList/CustomTaskList.jsx
1
import React from 'react';
2
import { useSelector, useDispatch } from 'react-redux';
3
import { Alert } from '@twilio-paste/core/alert';
4
import { Theme } from '@twilio-paste/core/theme';
5
import { Text } from '@twilio-paste/core/text';
6
7
import { actions } from '../../states';
8
9
const CustomTaskList = () => {
10
const isOpen = useSelector(
11
(state) => state.pluginState.customTaskList.isOpen
12
);
13
const dispatch = useDispatch();
14
15
const dismiss = () => dispatch(actions.customTaskList.setOpen(false));
16
17
if (!isOpen) {
18
return null;
19
}
20
21
return (
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
};
29
30
export 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.

(warning)

Warning

You might be wondering why this async Action is being created with redux-promise-middleware in mind, instead of using the createAsyncThunk(link takes you to an external page) helper from Redux Toolkit.

Unfortunately, Flex does not currently ship with the necessary middleware, redux-thunk, and this is not yet supported.


Combine Your Application's Reducer and the Flex UI's Reducer

combine-your-applications-reducer-and-the-flex-uis-reducer page anchor

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.

1
import React from 'react';
2
import ReactDOM from 'react-dom/client';
3
import {
4
configureStore
5
} from '@reduxjs/toolkit';
6
import Flex from '@twilio/flex-ui';
7
8
import myReducer from './myReducerLocation';
9
import configuration from './appConfig';
10
11
12
// Configure a new Redux store
13
const store = configureStore({
14
// Add the Flex reducer to your existing reducers
15
reducer: {
16
app: myReducer,
17
flex: Flex.FlexReducer,
18
},
19
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
20
// if you are using the default Redux middlewares, make sure to disable
21
// 'serializableCheck' and 'immutableCheck', as they are not compatible
22
// with the Flex UI Reducers
23
serializableCheck: false,
24
immutableCheck: false,
25
}).concat([...Flex.getFlexMiddleware()]),
26
enhancers: [
27
Flex.flexStoreEnhancer
28
]
29
})
30
31
// Flex is instantiated with the new Redux store,
32
// which includes your custom reducers
33
Flex.Manager.create(configuration, store).then((manager) => {
34
const root = ReactDOM.createRoot(document.getElementById('root'));
35
root.render(
36
<Provider store={store}>
37
<Flex.ContextProvider manager={manager}>
38
<Flex.RootContainer />
39
</Flex.ContextProvider>
40
</Provider>
41
);
42
});
43

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.