This is the first article in a series called “Managing Application State with RxJS + Redux,” which introduces the component autonomy capabilities redux-Obervable V1 brings to React + Redux.

Summary of article addresses in this series:

  • Use redux-Observable to implement component autonomy
  • How to implement a Redux-Observable

Introduction to the story – observables

Redux-observable is a redux middleware that uses RxJs to drive action side effects. Similar goals are known as Redux-Thunk and Redux-Saga. By integrating with Redux-Observable, we can leverage the functional Responsive programming (FRP) capabilities of RxJS in Redux to make it easier to manage asynchronous side effects (assuming you’re familiar with RxJS).

Epic is the core concept and basic type of Redux-Observable, which almost carries all of redux-Observable. Formally, Epic is a function that receives an action stream and outputs a new action stream:

function (action$: Observable<Action>, state$: StateObservable<State>) :Observable<Action>
Copy the code

As you can see, Epic acts as a stream converter.

From the perspective of redux-Observable, Redux acts as the central state collector. When an action is dispatched, a new action will be dispatched after a synchronous or asynchronous task. Bring its payload to the reducer, and so on. In this way, Epic defines action causality.

At the same time, THE FRP mode of RxJS also brings the following capabilities:

  • Race handling capability
  • Declarative task handling
  • Test the friendly
  • Component autonomy (redux-Observable 1. 0 starting support)

practice

This series assumes that the reader has the basics of FRP and RxJS, so there will be no more details about RxJS and Redux-Observable.

Now let’s practice a common business requirement — the list page. This example demonstrates the new redux-Observable 1.0 features and component autonomy implemented under 1.0.

Component autonomy: Components focus only on how to govern themselves.

First look at the list page appeal:

  • Poll the list of data at intervals
  • Support search, trigger search, re-polling
  • Support field sort, sort status change, re – polling
  • Support paging, page capacity modification, paging status changes, re – polling
  • The polling ends when the component is uninstalled

In the context of front-end componentization, we might design the following Container, with the base component based on Ant Design:

  • Table of data (including paging) : Based on the Table component

  • Search box: : Based on Input component

  • ** Sort selection box: ** Based on the **Select ** component

In React + Redux, container components use the connect method to pick the desired state from the state tree. Therefore, there must be a coupling between container components:

Next, we will discuss the state management and side effects handling of list applications in two different modes: the traditional mode based on Redux-Thunk or Redux-Saga, and the FRP mode based on Redux-Observable. You can see the difference in the coupling state between components and their data ecology (states and side effects) in addition to the basic coupling to Redux under different patterns.

Of course, in order to make you better understand the article, I also wrote a demo, you can clone & Run. The rest of the code comes from this demo. Here you can see that the user list is based on FRP and the Repo list is based on traditional mode:

Coupling of components in traditional mode

In traditional mode, we need to face the reality that state fetching can be proactive:

const state = store.getState()
Copy the code

That is, we need to actively retrieve the state, and cannot listen for state changes. Therefore, in this mode, our thinking of componentized development would be:

  • Components are mounted and polling is enabled
    • When searching, end the last poll, build new request parameters, and start a new poll
    • When the order changes, the last poll is ended, new request parameters are built, and a new poll is started
    • When the page changes, the last poll ends, new request parameters are built, and a new poll is started
  • The component is uninstalled, and polling ends

In this way, when we write containers for search, sort, paging, etc., when the values involved in the container change, we not only need to update those values in the state tree, but also need to restart the polling.

Assuming we use redux-Thunk to handle side effects, the code looks like this:

let pollingTimer: number = null

function fetchUsers() :ThunkResult {
  return (dispatch, getState) = > {
    const delay = pollingTimer === null ? 0 : 15 * 1000
    pollingTimer = setTimeout((a)= > {
      dispatch({
        type: FETCH_START,
        payload: {}
      })
      const { repo }: { repo: IState } = getState()
      const { pagination, sort, query } = repo
      // Encapsulate parameters
      const param: ISearchParam = {
        // ...
      }
      // make a request
      // fetch(param)...
  }, delay)
}}

export function polling() :ThunkResult {
  return (dispatch) = > {
    dispatch(stopPolling())
    dispatch({
      type: POLLING_START,
      payload: {}
    })
    dispatch(fetchUsers())
  }
}

export function stopPolling() :IAction {
  clearTimeout(pollingTimer)
  pollingTimer = null
  return {
    type: POLLING_STOP,
    payload: {}
  }
}

export function changePagination(pagination: IPagination) :ThunkResult {
  return (dispatch) = > {
    dispatch({
      type: CHANGE_PAGINATION,
      payload: {
        pagination
      }
    })
    dispatch(polling())
  }
}

export function changeQuery(query: string) :ThunkResult {
  return (dispatch) = > {
    dispatch({
      type: CHANGE_QUERY,
      payload: {
        query
      }
    })
    dispatch(polling())
  }
}

export function changeSort(sort: string) :ThunkResult {
  return (dispatch) = > {
    dispatch({
      type: CHANGE_SORT,
      payload: {
        sort
      }
    })
    dispatch(polling())
  }
}
Copy the code

As can be seen, several components involved in the request parameters, such as filtering items, paging, search, etc., when they dispatch an action to modify the corresponding business state, they also need to manually dispatch a restart poll action to end the last poll and start the next poll.

Maybe this scenario is as complex as you can get, but if we have a bigger project, or if the current project is going to be bigger in the future, there will be more components and more developers working together. Collaborating developers need to keep an eye on whether their components are influencing other developers’ components, and if so, how much, and what to do about it.

Components are not just UI components, but also the data ecology involved in components. Most front-end developers write business components that implement the business logic involved in the UI in addition to the UI.

We summarize the problems faced by using traditional modes to comb out data flow and side effects:

  1. Procedural programming, verbose code
  2. Race processing needs to be controlled artificially by token quantities and so on
  3. Components are coupled with each other.

FRP mode and component autonomy

In FRP mode, following passive mode, state should be observed and responded to, rather than actively acquired. Therefore, since 1.0, store.getState() is no longer recommended for redux-Observable state retrieval. Epic has a new function signature, and the second parameter is state$:

function (action$: Observable<Action>, state$: StateObservable<State>) :Observable<Action>
Copy the code

Redux-observable reached its milestone with the introduction of state$, and now we can further practice FRP in Redux. In this example (from redux-Observable official), we automatically store A Google document when its status changes:

const autoSaveEpic = (action$, state$) = >
  action$.pipe(
    ofType(AUTO_SAVE_ENABLE),
    exhaustMap((a)= >
      state$.pipe(
        pluck('googleDocument'),
        distinctUntilChanged(),
        throttleTime(500, { leading: false.trailing: true }),
        concatMap(googleDocument= >
          saveGoogleDoc(googleDocument).pipe(
            map((a)= > saveGoogleDocFulfilled()),
            catchError(e= > of(saveGoogleDocRejected(e)))
          )
        ),
        takeUntil(action$.pipe(
          ofType(AUTO_SAVE_DISABLE)
        ))
      )
    )
  );
Copy the code

Going back, we can also summarize the list page requirements as follows:

  • Poll the list of data at intervals
  • When parameters (sorting, paging, etc.) change, the polling is restarted
  • When actively searching, rerun polling
  • End polling when the component is uninstalled

In FRP mode, we define a polling EPIC:

const pollingEpic: Epic = (action$, state$) = > {
  const stopPolling$ = action$.ofType(POLLING_STOP)
  const params$: Observable<ISearchParam> = state$.pipe(
    map(({user}: {user: IState}) = > {
      const { pagination, sort, query } = user
      return {
        q: `${query ? query + ' ' : ''}language:javascript`,
        language: 'javascript',
        page: pagination.page,
        per_page: pagination.pageSize,
        sort,
        order: EOrder.Desc
      }
    }),
    distinctUntilChanged(isEqual)
  )

  return action$.pipe(
    ofType(LISTEN_POLLING_START, SEARCH),
    combineLatest(params$, (action, params) = > params),
    switchMap((params: ISearchParam) = > {
      const polling$ = merge(
        interval(15 * 1000).pipe(
          takeUntil(stopPolling$),
          startWith(null),
          switchMap((a)= > from(fetch(params)).pipe(
            map(({data}: ISearchResp) = > ({
              type: FETCH_SUCCESS,
              payload: {
                total: data.total_count,
                list: data.items
              }
            })),
            startWith({
              type: FETCH_START,
              payload: {}
            }),
            catchError((error: AxiosError) = > of({
              type: FETCH_ERROR,
              payload: {
                error: error.response.statusText
              }
            }))
          )),
          startWith({
            type: POLLING_START,
            payload: {}
          })
      ))
      return polling$
    })
  )
}
Copy the code

Here are some explanations for this Epic.

  • First, we declare the polling end stream, which is terminated when a value is generated for the polling end stream:
const stopPolling$ = action$.ofType(POLLING_STOP)
Copy the code
  • The parameters come from the state, and since the state is now observable, we can flow from the statestate$Distribute a downstream —Parameters of the flow:
const params$: Observable<ISearchParam> = state$.pipe(
  map(({user}: {user: IState}) = > {
    const { pagination, sort, query } = user
    return {
      // Construct parameters
    }
  }),
  distinctUntilChanged(isEqual)
)
Copy the code

We expected the parameter streams to be up-to-date parameters, and therefore used the Dinstinct TUntilChanged (isEqual) to determine the similarities and differences between the two parameters

  • A poll flow is created when the search is initiated, or when the parameters changecombineLatestFinally, the new action depends on the result of the data pull:
return action$.pipe(
  ofType(LISTEN_POLLING_START, SEARCH),
  combineLatest(params$, (action, params) = > params),
  switchMap((params: ISearchParam) = > {
    const polling$ = merge(
      interval(15 * 1000).pipe(
        takeUntil(stopPolling$),
        // Start polling automatically
        startWith(null),
        switchMap((a)= > from(fetch(params)).pipe(
          map(({data}: ISearchResp) = > {
            / /... Process the response
          }),
          startWith({
            type: FETCH_START,
            payload: {}
          }),
          catchError((error: AxiosError) = > {
            // ...
          })
        )),
        startWith({
          type: POLLING_START,
          payload: {}
        })
      ))
    return polling$
  })
)
Copy the code

OK, now all we need to do is dispatch a LISTEN_POLLING_START event when the data table container component is mounted to start our polling, and in its Epic counterpart, it knows exactly when to end the polling and when to restart the polling. Our paging components, sorting and selection components are no longer concerned with the need to restart polling. For example, the pagination component’s state-changing action only needs to change the state without worrying about polling:

export function changePagination(pagination: IPagination) :IAction {
  return {
    type: CHANGE_PAGINATION,
    payload: {
      pagination
    }
  }
}
Copy the code

In THE FRP mode, passive model allows us to observe state, declare the cause of polling, and integrate polling into the data table component, thus removing the coupling of polling and data table with pagination, search, sorting and other components. The component autonomy of data table is realized.

In conclusion, side effect management using FRP brings:

  • Asynchronous tasks are described declaratively and the code is concise
  • useswitchMapThe operator to deal withracetask
  • Component autonomy is achieved by minimizing component coupling. Large projects that facilitate collaboration among many people.

The good news is that it hits the traditional model where it hurts. The following figure shows a more intuitive comparison. The same business logic is implemented by Redux-Saga and redux-Observable. You can tell at a glance who’s more succinct:

Access to the story – observables

Redux-observable is just a redux middleware, so it can coexist with your existing redux-Thunk, redux-Saga, etc. You can gradually plug into redux-Observable to handle complex business logic. Once you are familiar with the BASIC RxJS and FRP patterns, you will find it can do everything.

In the future, considering the style control of the whole project, it is still suggested to choose only one set of models. FRP can perform well in complex scenes, and it will not shoot mosquitoes in simple scenes.

conclusion

This paper describes how state$provided by Redux-Observable 1.0 decouples the business association between components and realizes the business autonomy of individual components.

Next, a redux-Observable middleware will be implemented step by step to explain the design concept and implementation principle of Redux-Observable.

The resources

  • redux-observable official docs

About this Series

  • This series will start with the introduction of Redux-Observable 1.0 and explain my experience in combining RxJS with Redux. The content involved will include redux-Observable practice introduction, redux-Observable implementation principle exploration, Finally, I will introduce reObservable, a state management framework based on Redux-Observble + DVA Architecture.

  • This series is not an introduction to RxJS or Redux, but rather their basic concepts and core strengths. If you search for RxJS and stumble into this series and become interested in RxJS and FRP programming, then I would recommend getting started:

    • learnrxjs.io
    • Andre Staltz’s series of classes at Egghead. IO
    • RxJS by Cheng Mo
  • This series is not intended to be a tutorial, but rather an introduction to some of my own ideas on how to use RxJS in Redux. Hopefully, more people will point out some of the pitfalls and share more elegant practices.

  • I would like to express my sincere thanks for the help of some senior students on the way of practice, especially For the guidance of Questguo from Tencent Cloud. Reobservable is derived from TCFF, the React framework led by Tencent Cloud QuestGuo, and looks forward to the open source of TCFF in the future.

  • Thanks to Xiaoyu for her design support.