Redux architecture. Yes or no?

Original author: Ankit Goyal
  • Transfer
The author of the material, the translation of which we are publishing today, says that he is part of the Hike messenger team , which deals with new features of the application. The goal of this team is to translate into reality and explore ideas that users might like. This means that developers need to act quickly, and that they often have to make changes to the innovations they explore, which are aimed at making the work of users as comfortable and enjoyable as possible. They prefer to conduct their experiments using React Native, as this library speeds up development and allows you to use the same code on different platforms. In addition, they use the Redux library.



When the developers at Hike start working on something new, then, when discussing the architecture of the solution under study, they have several questions:

  • This is an experimental possibility that can, as they say, “do not take off,” and will have to be abandoned. Is it necessary, given this, to spend time designing an application architecture?
  • The experimental application is just MVP, a minimally viable product that has 1-2 screens and needs to be created as quickly as possible. Is it worth it, considering this, to contact Redux?
  • How to justify to the product managers the time required to prepare the auxiliary infrastructure of the experimental application?

As a matter of fact, Redux helps to find the right answers to all these questions. The Redux architecture helps to separate the state of the application from React. It allows you to create a global storage located at the top level of the application and providing state access for all other components.

Separation of responsibilities


What is a “division of responsibility”? This is what Wikipedia says about this : “In computer science, the division of responsibilities is the process of dividing a computer program into functional blocks that overlap each other’s functions as little as possible. In a more general case, the division of responsibilities is the simplification of a single process for solving a problem by dividing it into interacting processes to solve subtasks. ”

The Redux architecture allows you to implement the principle of separation of responsibilities in applications, breaking them into four blocks, presented in the following figure.


Redux Architecture

Here is a brief description of these blocks:

  • Views or user interface components (UI Components) resemble pure functions (that is, functions that do not change the data passed to them and have some other properties) that are responsible for displaying information on the screen based on the data passed to them from the repository. They do not change the data directly. When an event occurs, or if a user interacts with them, they contact the action creators.
  • Action Creators are responsible for creating and dispatching actions.
  • Reducers receive dispatched actions and update the state of the storage.
  • Data Store is responsible for storing application data.

Consider the Redux architecture by example.

What if different components need the same data?


The Hike app has a screen that displays a list of the user's friends. In the upper part of this screen displays information about their number.


A screen with information about friends in the Hike application.

There are 3 React components here:

  • FriendRow - a component containing the friend’s name of the user and some other information about him.
  • FriendsHeader - a component that displays the inscription "MY FRIENDS" and information about the number of friends.
  • ContainerView- a container component that combines the screen header represented by the component FriendsHeaderand the list of friends obtained by traversing an array containing information about the user's friends, each element of which is a component represented on the screen FriendRow.

Here is the code for the friendsContainer.js file , which illustrates the above:

classContainerextendsReact.Component{
    constructor(props) {
      super(props);
      this.state = {
        friends: []
      };
    }
    componentDidMount() {
      FriendsService.fetchFriends().then((data) => {
        this.setState({
          friends: data
        });
      });
    }
    render() {
      const { friends } = this.state;
      return (
        <View style={styles.flex}>
        <FriendsHeader count={friends.length} text='My friends' />
        {friends.map((friend) => (<FriendRow {...friend} />)) }
        </View>
      );
    }
}

An absolutely obvious way to create such an application page is to load data about friends into a component container and pass them as properties to child components.

Let us now consider that this data about friends may be needed in some other components used in the application.


Chat screen in a Hike application.

Suppose the application has a chat screen that also contains a list of friends. It can be seen that the same data is used on the screen with the list of friends and on the chat screen. What to do in this situation? We have two options:

  • You can load friend data again in the component ComposeChatresponsible for displaying chat lists. However, this approach is not particularly good, since its use will mean duplication of data and may lead to problems with synchronization.
  • You can download data about friends in the top-level component (the main container of the application) and transfer this data to the components responsible for displaying the list of friends and displaying the list of chats. In addition, we need to transfer functions to these components in order to update friend data, which is necessary to support data synchronization between components. This approach will lead to the fact that the top-level component will be literally packed with methods and data that it does not directly use.

Both of these options are not so attractive. Now let's look at how our problem can be solved using the Redux architecture.

Using redux


Here we are talking about the organization of work with data using the repository, action creators, reducers and two user interface components.

▍1. Data store


The repository contains downloaded data about the user's friends. This data can be sent to any component if it is needed there.

▍2. Action creators


In this case, the creator of the action is used to dispatch events to save and update data about friends. Here is the code for the friendsActions.js file :

exportconst onFriendsFetch = (friendsData) => {
  return {
    type: 'FRIENDS_FETCHED',
    payload: friendsData
  };
};

▍3. Reductors


Reducers await the arrival of events representing dispatching actions and update the data about friends. Here is the code for the friendsReducer.js file :

const INITIAL_STATE = {
       friends: [],
    friendsFetched: false
};
function(state = INITIAL_STATE, action){
    switch(action.type) {
    case'FRIENDS_FETCHED':
        return {
            ...state,
            friends: action.payload,
            friendsFetched: true
        };
    }
}

▍4. Friend List Component


This container component views friends and updates the interface as they change. In addition, he is responsible for downloading data from the storage in the event that he does not have them. Here is the code for the friendsContainer.js file :

classContainerextendsReact.Component {
    constructor(props) {
      super(props);
    }
    componentDidMount() {
      if(!this.props.friendsFetched) {
        FriendsService.fetchFriends().then((data) => {
          this.props.onFriendsFetch(data);
        });
      }
    }
    render() {
      const { friends } = this.props;
      return (
        <View style={styles.flex}>
        <FriendsHeader count={friends.length} text='My friends' />
        {friends.map((friend) => (<FriendRow {...friend} />)) }
        </View>
      );
    }
}
const mapStateToProps = (state) => ({
  ...state.friendsReducer
});
const mapActionToProps = (dispatch) => ({
  onFriendsFetch: (data) => {
    dispatch(FriendActions.onFriendsFetch(data)); 
  }
});
exportdefault connect(mapStateToProps, mapActionToProps)(Container);

▍5. Component that displays the chat list


This container component also uses data from the repository and responds to their updating.

About the implementation of the Redux architecture


In order to bring the above architecture to a working condition, it may take a day or two, but when changes need to be made to the project, they are made very simply and quickly. If you need to add a new component to your application that uses friend data, you can do this without having to worry about data synchronization or about having to redo other components. The same applies to the removal of components.

Testing


When using Redux, each application block is independently testable.
For example, each component of the user interface can be easily subjected to unit testing, since it turns out to be data independent. The point is that a function representing such a component always returns the same representation for the same data. This makes the application predictable and reduces the likelihood of errors occurring during data visualization.

Each component can be comprehensively tested using a variety of data. Such testing allows to reveal hidden problems and contributes to ensuring the high quality of the code.

It should be noted that not only components responsible for data visualization, but also reducers and action creators can be subjected to independent testing.

Redux is great, but using this technology we encountered some difficulties.

Difficulty Using Redux


▍ excess template code


In order to implement the Redux architecture in an application, you have to spend a lot of time, encountering all sorts of strange concepts and entities.

These are the so-called sledges (thunks), reducers (reducers), actions (actions), intermediate software layers (middlewares), these are functions mapStateToPropsand mapDispatchToProps, and much more. It takes time to learn all this, but it takes practice to learn how to use it properly. There are a lot of files in a project, and, for example, one minor change to a component to visualize data may result in the need to make changes to four files.

Red Redux vault is a singleton


In Redux, the data warehouse is built using the Singleton pattern, although components may have multiple instances. Most often this is not a problem, but in certain situations such an approach to data storage can create some difficulties. For example, imagine that there are two instances of a certain component. When data changes in any of these instances, the changes also affect the other instance. In certain cases, this behavior may be undesirable; it may be necessary for each instance of the component to use its own copy of the data.

Results


Recall our main question, which is whether to spend time and effort on the implementation of the Redux architecture. We, in response to this question, say Redux "yes." This architecture helps save time and effort when developing and developing applications. Using Redux makes it easier for programmers to make frequent changes to the application, and makes testing easier. Of course, the Redux architecture provides for a considerable amount of template code, but it contributes to breaking the code into modules that are convenient to work with. Each such module can be tested independently of the others, which contributes to the identification of errors at the design stage and allows for high quality programs.

Dear readers! Do you use Redux in your projects?


Also popular now: