Using 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
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 makes it easy to 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 to learn more about all of 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.
Using Redux in Your Flex Plugin
When you use the Flex Plugin Builder, you’re already using Redux! The boilerplate plugin code includes all of the bits and pieces that you need to manage the Plugin UI state. You can learn all about building your first plugin here.
Once you have a plugin setup, you can find most of the boilerplate Redux code in src/states/CustomTaskListState.js
. For the sake of easily reading this page, the content of the plugin builder 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.
Defining an Action
Let's start with the Action that will cause some change in the UI state. In the sample plugin, it’s called ACTION_DISMISS_BAR
, and as you might imagine, it dismisses a bar at the top of the UI.
You'll notice that there's a lot of places that say ACTION_DISMISS_BAR
in the code.
const ACTION_DISMISS_BAR = 'DISMISS_BAR';
In the sample plugin code, we've created a variable called ACTION_DISMISS_BAR
assigned it a string with the contents 'DISMISS_BAR'
.
Once the Flex UI is running, Actions will start flying all over the place. Redux needs a unique identifier to understand what all of these Actions are and what they should do to the application state. So, all dispatched actions need a name, and Redux will look up the set of instructions for how it should change state for actions with that particular name. In order to make sure that you don't confuse Redux, the best practice is to use variables, which ensure that the identifier stays consistent in all of the places you're using the action.
You'll need the Action ID when you're writing your Reducer, Components, and tests. Using a variable for the name allows you to rename the Action REMOVE_BAR
by changing it in a single place. You can feel confident that your code will run correctly across the entire code base (and look good in the Redux DevTools - but that's a story for another time!)
Writing the Reducer
The reducer is defined with the following function:
export function reduce(state = initialState, action) {...}
This function returns an updated version of the application based on the dispatched Action.
So, what is the application state?
const initialState = {
isOpen: true,
};
This Javascript object reflects a small slice of the application state. The Reducer's job is to apply the Action to the current state object to create an updated version of the state. It will then return the updated state to Redux. Redux will later combine this Reducer with a bunch of other reducers to create one big Store of your application state.
Note that the Reducer returns
a fresh copy of the state every time it processes an Action. If you just modify the state without returning it, your application state won't update properly. Learn more about these types of patterns in the Redux Style Guide.
A Reducer is usually one long switch statement, with the various case
s being your different Action names. In this case (ha! See what we did there?) the Reducer deals with two cases: if it sees an Action with the ACTION_DISMISS_BAR
identifier, it changes the isOpen
value to false
. Otherwise, if it doesn't recognize the Action identifier (or there isn't one), it'll just maintain the current application state.
Adding Plugin State to the Flex Store
Now that you have this state object, you'll need to make it available to Flex. This happens in src/YourPluginName.js
with the addReducer
method. This is a method that Flex provides to include your reducer in the Flex store - so this is Flex code, not Redux. It makes your Reducer's state and associated logic available across the Flex UI.
Accessing Redux from your Plugin Component
Now that your Reducer logic is defined, you need to connect all of that logic to the UI. In the Plugin Builder sample app, this happens in src/components/CustomTaskList/CustomTaskList.Container.js
.
In React, it's common to create a Container component that contains the logic for fetching data for the component and a Presentational component that handles the logic for rendering the component itself. Don't get too hung up on this pattern, but there are lots of resources out there if you're curious about learning more about why the Plugin Sample is structured this way!
The code here contains a couple of key pieces of logic:
// Import redux methods
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// Import the Redux Actions
import { Actions } from '../../states/CustomTaskListState';
import CustomTaskList from './CustomTaskList';
First, it imports everything that it needs from NodeJS packages and the Action/Component files in the React app
const mapStateToProps = (state) => ({
isOpen: state['sample'].customTaskList.isOpen,
});
const mapDispatchToProps = (dispatch) => ({
// Note: the plugin's sample code uses a function called bindActionCreators here
// For the sake of clarity, we're using the dispatch function directly here
dismissBar: dispatch(Actions.dismissBar),
});
Two functions, mapStateToProps
and mapDispatchToProps
, are defined. All React components have properties, or props
, that you can use to change how the component is displayed. As the name implies, mapStateToProps
describes what particular "chunks" of application state the component should know about. In this case, it's defining the value of isOpen
.
mapDispatchToProps
describes the Actions that the component can send to Redux. In this case, the component's dismissBar
prop should have a function that can dispatch the DISMISS_BAR
action.
export default connect(mapStateToProps, mapDispatchToProps)(CustomTaskList);
Finally, you'll see a fancy connect
method. It associates those mapping functions with the presentational React component. Your presentational component's props
will now have values defined by those functions.
You can see the magic at work in the Component code itself.
The component's isOpen
prop also has a defined value, thanks to the mapStateToProps
function.
const CustomTaskList = (props) => {
if (!props.isOpen) {
return null;
}
Here, it's being used to conditionally render the component HTML.
The component is invoking the dismissBar prop.
<i className="accented" onClick={props.dismissBar}>
This prop was defined with mapDispatchToProps
, and allows the component to dispatch the DISMISS_BAR
Action whenever a user clicks the close
text.
Redux helps you create a flow of data through your whole plugin. The user interacts with the Component, which invokes the dispatch function. The dispatch function sends out the relevant Action to the Reducer. The Reducer takes whatever information is associated with that Action (in this case, toggling isOpen
) and modifies the Redux Store to reflect what’s going on. At long last, the Component, which is subscribed to the Store, updates using the power of React, reflecting 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 - a lot easier.
Adding State to the Redux Store
You can add additional state to your Plugin's component. Inside src/states/CustomTaskListState.js
, define your state in the initialState
object, as well as a variable that will represent the state's action.
const ACTION_DISMISS_BAR = 'DISMISS_BAR';
const ACTION_UPDATE_MESSAGE = 'UPDATE_MESSAGE';
const initialState = {
isOpen: true,
message: 'Flex is Awesome!',
}
Define an Action that will tell Redux how to update your component's state.
export class Actions {
static dismissBar = () => ({ type: ACTION_DISMISS_BAR });
static updateMessage = (value) => ({ type: ACTION_UPDATE_MESSAGE, value });
}
When your component updates the state, it will dispatch an action which will call the reducer in order to update the state.
export function reduce(state = initialState, action) {
switch (action.type) {
case ACTION_DISMISS_BAR: {
return {
...state,
isOpen: false,
};
}
case ACTION_UPDATE_MESSAGE: {
return {
...state,
message: action.value, // update the message state property with new value stored in action object.
};
}
default:
return state;
}
}
Now that you have added a new state value to the initialState object, defined a Redux Action, and handled the ACTION_UPDATE_MESSAGE
action by implementing a case inside the reducer function, you need to map the state properties and dispatch functions to your components props. Doing this will allow you to retrieve the data from the Redux Store, as well as update the state of the Redux Store with new values.
Inside src/CustomTaskList/CustomTaskList.Container.js
, modify the mapStateToProps method:
// Import redux methods
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// Import the Redux Actions
import { Actions } from '../../states/CustomTaskListState';
import CustomTaskList from './CustomTaskList';
// Define mapping functions
const mapStateToProps = (state) => ({
isOpen: state['sample'].customTaskList.isOpen,
message: state['sample'].customTaskList.message, // maps the message state property to component's props
});
const mapDispatchToProps = (dispatch) => ({
dismissBar: bindActionCreators(Actions.dismissBar, dispatch),
updateMessage: bindActionCreators(Actions.updateMessage, dispatch), // maps the updateMessage function to the component's props
});
// Connect presentational component to Redux
export default connect(mapStateToProps, mapDispatchToProps)(CustomTaskList);
Finally, inside src/CustomTaskList/CustomTaskList.jsx
, you can reference message
and alertMessage
as component props.
import React from 'react';
import { CustomTaskListComponentStyles } from './CustomTaskList.Styles';
// It is recommended to keep components stateless and use redux for managing states
const CustomTaskList = (props) => {
if (!props.isOpen) {
return null;
}
return (
<CustomTaskListComponentStyles>
This is a dismissible demo component
<i className="accented" onClick={props.dismissBar}>
close
</i>
<span>{props.message}</span>
{/* when you type something it will update the message state property */}
<input type="text" value={props.message} onChange={(e) => props.updateMessage(e.target.value) } />
</CustomTaskListComponentStyles>
);
};
export default CustomTaskList;
Writing Asynchronous Actions
The Flex UI uses the redux-promise middleware for processing asynchronous actions.
There are some key terms to call out here:
Asynchronous Actions refer to any action code that doesn't immediately return data. For example, in this code, we write a function that, in its simplest description, returns a photo of a dog. We associate this function with an action, with the idea that we could, for example, have a customer click a button to see a cute dog photo.
// Fetches a picture of a dog
function getDogPhoto() {
return fetch('https://dog.ceo/api/breeds/image/random')
.then((response) => response.json())
.catch((err) => {
return `Error: ${err}`;
});
}
export const Actions = {
// Invoke the getDogPhoto
samplePromise: () => ({
type: 'GET_DOG_PHOTO',
payload: getDogPhoto(),
})
};
By default, Redux would throw an error with this code, though! This is because our first function doesn't return a dog photo - it returns a Promise for a dog photo. Redux needs an object to return to the reducer, not a Promise for an object!
If you need to brush up on your understanding of Promises, check out Mozilla's Promise API Docs to learn more.
Promises can resolve in a lot of different ways, and so we need to teach Redux that it should be expecting Promises, and then, for each Promise in our code, we need to define how to handle the various outcomes of the Promise. Enter: our Middleware.
The redux-promise
middleware allows us to write a reducer that looks like this:
export function reduce(state = initialState, action) {
switch (action.type) {
// Action for Redux-Promise-Middleware Pending State
case `${ACTION_GET_DOG_PHOTO}_PENDING`:
return state;
// Action for Redux-Promise-Middleware Success State
case `${ACTION_GET_DOG_PHOTO}_FULFILLED`:
return {
...state,
image: action.payload.message,
};
// Action for Redux-Promise-Middleware Failed State
case `${ACTION_GET_DOG_PHOTO}_REJECTED`:
return {
...state,
error: action.payload.error,
};
default:
return state;
}
}
Instead of just the one GET_DOG_PHOTO
action, the reducer handles three actions that handle each Promise state. The middleware can throw a pending action (which is useful for rendering loading spinners and letting people know that something is happening), a fulfilled action if everything goes correctly (here, we use that for adding the dog image to our Redux Store) and a failed action (in case something goes wrong, like the dog photos API is down and we need to show an error.)
Because this middleware is already built into the Flex UI's reducer, you'll notice that there's no need to write extra code to import the middleware, or define promise-handling logic yourself. You can just start writing asynchronous code whenever you're ready!
Combine Your Application's Reducer and the Flex UI's Reducer
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.
import Flex from "@twilio/flex-ui"
import myReducer from './myReducerLocation'
// Use Redux's combineReducers method to add your reducer to the Flex reducer
const reducers = combineReducers({
flex: Flex.FlexReducer,
app: myReducer
});
const middleware = applyMiddleware();
// Redux builts a store using the reducers and applies Flex middleware
const store = createStore(
reducers,
compose(
middleware,
Flex.applyFlexMiddleware()
)
);
// Flex is instantiated with the new store, which includes your custom reducer
Flex
.Manager.create(configuration, store)
.then(manager => {
ReactDOM.render(
<Provider store={store}>
<Flex.ContextProvider manager={manager}>
<Flex.RootContainer />
</Flex.ContextProvider>
</Provider>,
container
);
})
What Next?
- Now that you're familiar with Redux, learn more about how Flex uses React
- Check out how to fetch data from a Plugin and add that data to your Redux store.
- Learn how to deploy your Plugin
Need some help?
We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd by visiting Twilio's Stack Overflow Collective or browsing the Twilio tag on Stack Overflow.