Ionic React and Redux

Redux is a popular tool which is used to manage application state in JavaScript. Redux is based on three core principles: that application state should be kept in a single store, that the state should only be changed by discrete and well-defined actions, and that changes to state should be made by pure functions called reducers. While Redux can be used with any JavaScript framework, it is most commonly used with React, as it solves the common problem of passing state across a large number of React components.

Since I've been learning a lot about both Redux and Ionic recently, I decided to explore how Redux can be used alongside Ionic React. As it turns out, Redux complements Ionic React well, and Ionic's features can make it easier to add Redux to a React application. I'd like to explain some of the ways I've been integrating Ionic React with Redux so others can do the same.

This post assumes a basic level of Redux knowledge, though it will explain more advanced concepts. If you are unfamiliar with Redux and would like to learn more about it, the official tutorial is a good starting point.

Redux and Components

The structure of Redux encourages splitting React applications into components. Since updates to the application state must be made through specific actions, developers using Redux must always specify where and how state updates happen in their application. A recommended pattern in Redux is to only dispatch one action in any given component, which means that larger components typically must be split into smaller ones. Fortunately, this is a natural pattern in Redux, as Redux eliminates the need to pass pieces of the application state across several components.

Using Ionic makes the process of splitting a React application into components even easier, since Ionic is already component-driven. With Ionic, creating a new component can be as simple as moving an Ionic component to a separate React component and connecting it to the Redux store. UI elements which are commonly used to dispatch actions, such as buttons, generally have pre-built components in Ionic that can be wrapped in their own React components as needed.

As an example of how this works in practice, consider everyone's favorite example application, the todo list. In Ionic, the items in a todo list might look something like this:

<IonList>
  <IonListHeader>
    <IonLabel>Items</IonLabel>
  </IonListHeader>
  {todos.map((item: Item) => (
    <IonItem key={item.id}>
      <IonItem class="ion-text-capitalize">
        {item.name}
      </IonItem>
      <IonButton onClick={() => removeTodo(item)}>
        <IonIcon slot="icon-only" icon={trash}/>
      </IonButton>
    </IonItem>
  ))}
</IonList>

If we use Redux with this todo list, the call to removeTodo will become an action. If we need to take any other actions on this list, we'll want to separate the remove todo button into its own component. Since it already has its own Ionic component, this is an easy process. We can separate the IonButton and connect it to the Redux store with React-Redux:

  interface RemoveTodoProps {
    item: Item,
    removeTodo: (todo: Item, listId: number) => removeTodoType,
  }

  function RemoveTodoButton(props: RemoveTodoProps) {
    const item = props.item
    return (
      <IonButton onClick={() => props.removeTodo(item)}>
        <IonIcon slot="icon-only" icon={trash}/>
      </IonButton>
    )
  }

  export default connect(
    null,
    { removeTodo }
  )(RemoveTodoButton)

We then replace the IonButton with our new component in the list:

<IonList>
  <IonListHeader>
    <IonLabel>Items</IonLabel>
  </IonListHeader>
  {todos.map((item: Item) => (
    <IonItem key={item.id}>
      <IonItem class="ion-text-capitalize">
        {item.name}
      </IonItem>
      <RemoveTodoList item={item} />
    </IonItem>
  ))}
</IonList>

Using Ionic Storage with middleware

Ionic Storage is a useful tool provided by Ionic to handle storage within an application. For mobile apps, it is a considerable improvement over solutions such as local storage, as it won't be cleared by the OS if the device is low on space. In our todo list example, it would be nice if we could use Ionic Storage to store the state when it changes and reload the stored state when the user reopens the application. However, Redux specifies that reducers must be pure functions; we can't just call the Ionic Storage APIs from our reducers whenever they update the state.

To get around this problem, we can use middleware to save and load our state. Middleware in Redux allows us to process additional tasks before or after changing the state. To load our state from the store, we'll use the redux-thunk middleware, which allows us to process additional tasks with side effects (such as loading from storage) before dispatching an action. To save our state, we'll write our own middleware which saves the state to Ionic Storage after it changes.

Loading the state from Ionic Storage with thunks

A thunk is a type of function which is used to delay the execution of some code, often another function, until the thunk is called. In Redux, thunks are typically used through the redux-thunk middleware; in this context, they are higher-order functions which return a function that processes necessary tasks, such as API calls, before dispatching an action. Thunks are the primary method used to load an initial state from an outside source in Redux.

To load our initial state, we add the thunk middleware to our store:

const middleware = [thunk]

export const store = createStore(todoList, 
  applyMiddleware<ThunkDispatch<listState, undefined, actionType>, listState>(...middleware)
  )

(Note the use of the ThunkDispatch type, which is a type provided by redux-thunk so that thunks can more easily be used with TypeScript. In this case, listState is the type of the application state, actionType is an umbrella type for the various actions, and the undefined parameter is used for an extra argument that we don't need.)

We then set up our thunk and the action it dispatches. We will create a thunk called loadState which returns a function that dispatches the setLoadedState action:

export function setLoadedState(list: todoList): setLoadedStateType {
  return {
    type: SET_LOADED_STATE,
    list
  }
}

export const loadState = (): ThunkAction<void, listState, unknown, Action<string>> =>
  (dispatch: Dispatch) =>
    Storage.get({key: "state"})
    .then((res: { value: string | null}) => {
      if (res.value !== null && res.value !== undefined) {
        return JSON.parse(res.value)
      }
      else {
        return null
      }
    }
    ).then(res => {
      dispatch(setLoadedState(res))
    })

loadState returns a function that takes the dispatcher as an argument and first calls Ionic Storage to get the stored data. Once the data has been retrieved, it parses it back into an object (or returns null if there is no data) and passes it to the dispatcher. The dispatcher then fires an action to set the state into the Redux store.

We then finish our setup by calling loadState after we create our store:

const middleware = [thunk]

export const store = createStore(todoList,
  applyMiddleware<ThunkDispatch<listState, undefined, actionType>, listState>(...middleware)
  )

store.dispatch(loadState())

Creating an Ionic Storage middleware to save the state

If we want to set our state into Ionic Storage when we change it, we can write our own middleware to do it. Middleware can be any function that we call before or after changing the state, so writing a custom middleware is straightforward. Redux has an official tutorial specifically on middleware, which further explains the concepts we will be using to create our own.

First, we write an asynchronous wrapper function to set our state in Ionic Storage, which we will call from our middleware:

async function storeList(key: string, state: listState) {
  if (key !== null && key !== undefined) {
    await Storage.set({
      key,
      value: JSON.stringify(state),
    })
  }
}

We then set up a middleware that calls into this function. The middleware starts with the standard function cascade, then dispatches the action, since we need to update the state before we save it. If the action was something other than loading the state, we call into our state-setting function to save the new state.

  export const saveToStorage: Middleware = ({ getState }: MiddlewareAPI) => (next: Dispatch) => (action: actionType) => {
    let result = next(action)
    if (action.type !== SET_LOADED_STATE) {
      const state = getState()
      storeList("state", state.lists)
    }
    return result
  }

We then finish by adding our new middleware to our existing store:

  const middleware = [thunk, saveToStorage]

  export const store = createStore(todoList,
    applyMiddleware<ThunkDispatch<listState, undefined, actionType>, listState>(...middleware)
    )

Exploring further

This post is just a glimpse of the things you can do with Ionic React and Redux. I'm excited to learn more about both tools and the things I can build when I use them together. For anyone interested in learning about these technologies, here are some resources that I've found useful.


Category: Development
Tags: Ionic, React