Creating responsive applications using Redux, RxJS, and Redux-Observable in React Native

In the ever-evolving world of web and mobile applications, managing application state, such as user information, loaded items, loading status, and errors, is becoming increasingly complex. Redux addresses this by centralizing the state in a global object.

However, Redux lacks built-in support for asynchronous operations. redux-observable, built upon the robust reactive programming library RxJS, offers a solution. RxJS, an implementation of the ReactiveX API, combines the strengths of reactive programming, functional programming, the observer pattern, and the iterator pattern.

This tutorial explores Redux and its integration with React. We’ll delve into reactive programming with RxJS, demonstrating its ability to simplify complex asynchronous tasks.

Finally, we’ll examine redux-observable, which leverages RxJS for asynchronous operations, and construct a React Native application using Redux and redux-observable.

Redux

Redux, as it describes itself on GitHub, is “a predictable state container for JavaScript apps.” It provides a global state for your JavaScript applications, separating state and actions from React components.

In a typical React application without Redux, data is passed from parent to child components using properties, or props. This data flow, manageable in small applications, can become unwieldy as complexity grows. Redux, acting as a single source of truth, enables component independence.

In React, Redux integration is achieved using react-redux, which provides bindings for React components to access Redux data and dispatch actions to update the Redux state.

Redux

Redux can be summarized in three key principles:

1. Single Source of Truth

The state of your entire application resides in a single object, held by a store within Redux. A Redux app should have only one store.

1
2
3

» console.log(store.getState())
« { user: {...}, todos: {...} }

To access Redux data in a React component, we utilize the connect function from react-redux. connect accepts four optional arguments. We’ll concentrate on the first, mapStateToProps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

/* UserTile.js */

import { connect } from 'react-redux';

class UserTile extends React.Component {
    render() {
        return <p>{ this.props.user.name }</p>
    }
}
function mapStateToProps(state) {
    return { user: state.user }
}

export default connect(mapStateToProps)(UserTile)

In this example, mapStateToProps receives the global Redux state and returns an object merged with the props passed to <UserTile /> by its parent.

2. State Is Read-Only

React components cannot directly modify Redux state. Changes are made by emitting an action, a plain object signifying an intention to modify the state. Each action object must have a type field, a string value. While action content is flexible, most applications adhere to a flux-standard-action format, limiting an action’s structure to four keys:

  1. type: A unique string identifier for the action.
  2. payload: Optional data related to the action, of any type.
  3. error: An optional boolean flag set to true if the action represents an error, akin to a rejected Promise. When error is true, payload should contain an error object.
  4. meta: Any type of value intended for additional information not part of the payload.

Here are two example actions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

store.dispatch({
    type: 'GET_USER',
    payload: '21',
});

store.dispatch({
    type: 'GET_USER_SUCCESS',
    payload: {
        user: {
            id: '21',
            name: 'Foo'
        }
    }
});

3. State Is Changed with Pure Functions

Global Redux state changes are handled by pure functions called reducers. A reducer receives the previous state and an action, returning the next state. Instead of modifying the existing state object, it creates a new one. Depending on application size, a Redux store may have one or multiple reducers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

/* store.js */

import { combineReducers, createStore } from 'redux'

function user(state = {}, action) {
  switch (action.type) {
    case 'GET_USER_SUCCESS':
      return action.payload.user
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO_SUCCESS':
      return [
        ...state,
        {
          id: uuid(), // a random uuid generator function
          text: action.text,
          completed: false
        }
      ]
    case 'COMPLETE_TODO_SUCCESS':
      return state.map(todo => {
        if (todo.id === action.id) {
          return {
            ...todo,
            completed: true
          }
        }
        return todo
      })
    default:
      return state
  }
}
const rootReducer = combineReducers({ user, todos })
const store = createStore(rootReducer)

Similar to reading state, we can dispatch actions using the connect function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

/* UserProfile.js */
class Profile extends React.Component {
    handleSave(user) {
        this.props.updateUser(user);
    }
}

function mapDispatchToProps(dispatch) {
    return ({
        updateUser: (user) => dispatch({
            type: 'GET_USER_SUCCESS',
            user,
        }),
    })
}

export default connect(mapStateToProps, mapDispatchToProps)(Profile);

RxJS

RxJS

Reactive Programming

Reactive programming is a declarative paradigm centered around data flow in “streams” and their propagation and modifications. RxJS, a JavaScript reactive programming library, introduces observables, streams of data that an observer can subscribe to, receiving data over time.

An observable’s observer is an object with three optional functions: next, error, and complete.

1
2
3
4
5
6

observable.subscribe({
  next: value => console.log(`Value is ${value}`),
  error: err => console.log(err),
  complete: () => console.log(`Completed`),
})

The .subscribe function can also accept three functions instead of an object.

1
2
3
4
5
6

observable.subscribe(
  value => console.log(`Value is ${value}`),
  err => console.log(err),
  () => console.log(`Completed`)
)

We can create observables by instantiating an observable object and passing a function that receives a subscriber (observer). The subscriber has next, error, and complete methods. The subscriber can call next with a value multiple times and complete or error at the end. After calling complete or error, the observable stops emitting values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

import { Observable } from 'rxjs'
const observable$ = new Observable(function subscribe(subscriber) {
  const intervalId = setInterval(() => {
    subscriber.next('hi');
    subscriber.complete()
    clearInterval(intervalId);
  }, 1000);
});
observable$.subscribe(
  value => console.log(`Value is ${value}`),
  err => console.log(err)
)

This example prints “Value is hi” after 1000 milliseconds.

Manually creating observables can be repetitive. RxJS provides functions like of, from, and ajax for easier observable creation.

of

of transforms a sequence of values into a stream:

1
2
3
4

import { of } from 'rxjs'
of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value))
// 1 2 3 Hello World

from

from converts various data types into value streams:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

import { from } from 'rxjs'


from([1, 2, 3]).subscribe(console.log)
// 1 2 3


from(new Promise.resolve('Hello World')).subscribe(console.log)
// 'Hello World'


from(fibonacciGenerator).subscribe(console.log)
// 1 1 2 3 5 8 13 21 ...

ajax

ajax takes a URL string or creates an observable for making HTTP requests. ajax.getJSON returns only the nested response object from an AJAX call, omitting other ajax() properties:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

import { ajax } from 'rxjs/ajax'

ajax('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log)
// {request, response: {userId, id, title, completed}, responseType, status}


ajax.getJSON('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log)
// {userId, id, title, completed}


ajax({ url, method, headers, body }).subscribe(console.log)
// {...}

Numerous other methods exist for observable creation (see the complete list here).

Operators

Operators, the heart of RxJS, provide functionality for various tasks. Since RxJS 6, operators are pure functions applied to observables using .pipe, rather than observable methods.

map

map applies a projection to each stream element using a provided function:

1
2
3
4
5
6
7
8

import { of } from 'rxjs'
import { map } from 'rxjs/operators'

of(1, 2, 3, 4, 5).pipe(
  map(i=> i * 2)
).subscribe(console.log)
// 2, 4, 6, 8, 10
Map

filter

filter removes values from the stream based on a provided function’s truthiness:

1
2
3
4
5
6
7
8
9

import { of } from 'rxjs'
import { map, filter } from 'rxjs/operators'

of(1, 2, 3, 4, 5).pipe(
  map(i => i * i),
  filter(i => i % 2 === 0)
).subscribe(console.log)
// 4, 16
Filter

flatMap

flatMap maps each stream item into another stream using a provided function and flattens the resulting streams’ values:

1
2
3
4
5
6
7
8
9

import { of } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { flatMap } from 'rxjs/operators'

of(1, 2, 3).pipe(
  flatMap(page => ajax.toJSON(`https://example.com/blog?size=2&page=${page}`)),
).subscribe(console.log)
// [ { blog 1 }, { blog 2 }, { blog 3 }, { blog 4 }, { blog 5 }, { blog 6 } ]
FlatMap

merge

merge combines items from two streams in their arrival order:

1
2
3
4
5
6
7
8
9

import { interval, merge } from 'rxjs'
import { pipe, take, mapTo } from 'rxjs/operators'

merge(
  interval(150).pipe(take(5), mapTo('A')),
  interval(250).pipe(take(5), mapTo('B'))
).subscribe(console.log)
// A B A A B A A B B B
Merge

A comprehensive list of operators is available here.

Redux-Observable

Redux-Observable

Redux actions are inherently synchronous. Redux-observable, a Redux middleware, leverages observable streams for asynchronous operations, dispatching new actions with the results.

Redux-observable revolves around Epics, functions that take an action stream and optionally a state stream, returning an action stream.

1
function (action$: Observable, state$: StateObservable): Observable;

By convention, stream variables (observables) end with $. Before using redux-observable, we add it as middleware to our store. Since epics are observable streams, returning the same action within an epic would create an infinite loop.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

const epic = action$ => action$.pipe(
    filter(action => action.type === 'FOO'),
    mapTo({ type: 'BAR' }) // not changing the type of action returned
                           // will also result in an infinite loop
)
// or
import { ofType } from 'redux-observable'
const epic = action$ => action$.pipe(
    ofType('FOO'),
    mapTo({ type: BAZ' })
)

Imagine this reactive architecture as interconnected pipes where each pipe’s output feeds back into itself and other pipes, including Redux reducers. Filters on these pipes control data flow.

Consider a Ping-Pong epic. It receives a ping, sends it to the server, and upon request completion, sends a pong back to the app.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

const pingEpic = action$ => action$.pipe(
    ofType('PING'),
    flatMap(action => ajax('https://example.com/pinger')),
    mapTo({ type: 'PONG' })
)

Now, we are going to update our original todo store by adding epics and retrieving users.

import { combineReducers, createStore } from 'redux'
import { ofType, combineEpics, createEpicMiddleware } from 'redux-observable';
import { map, flatMap } from 'rxjs/operators'
import { ajax } from 'rxjs/ajax'


// ...
/* user and todos reducers defined as above */
const rootReducer = combineReducers({ user, todos }) 

const epicMiddleware = createEpicMiddleware();
const userEpic = action$ => action$.pipe(
    ofType('GET_USER'),
    flatMap(() => ajax.getJSON('https://foo.bar.com/get-user')),
    map(user => ({ type: 'GET_USER_SUCCESS', payload: user }))
)

const addTodoEpic = action$ => action$.pipe(
    ofType('ADD_TODO'),
    flatMap(action => ajax({
        url: 'https://foo.bar.com/add-todo',
        method: 'POST',
        body: { text: action.payload }
    })),
    map(data => data.response),
    map(todo => ({ type: 'ADD_TODO_SUCCESS', payload: todo }))
)
const completeTodoEpic = action$ => action$.pipe(
    ofType('COMPLETE_TODO'),
    flatMap(action => ajax({
        url: 'https://foo.bar.com/complete-todo',
        method: 'POST',
        body: { id: action.payload }
    })),
    map(data => data.response),
    map(todo => ({ type: 'COMPLEE_TODO_SUCCESS', payload: todo }))
)

const rootEpic = combineEpics(userEpic, addTodoEpic, completeTodoEpic)
const store = createStore(rootReducer, applyMiddleware(epicMiddleware))
epicMiddleware.run(rootEpic);

Important: Epics, like other RxJS observables, can reach a complete or error state, halting the epic and the app. Handle potential errors using the __catchError__ operator. More information: Error Handling in redux-observable.

A Reactive Todo App

Here’s a simplified demo app with UI:

A Reactive Todo App

A React, Redux, and RxJS Tutorial Summarized

We explored reactive applications, Redux, RxJS, and redux-observable, even building a reactive Todo app in Expo with React Native. Modern state management options for React and React Native developers offer significant power.

The app’s source code is available on GitHub. Share your thoughts on state management for reactive apps in the comments below.

Licensed under CC BY-NC-SA 4.0