Introduction to Redux & React-redux

image

Table of contents


Introduction
1. Installation and getting started
2. Redux
.... 2.1 createStore
.... 2.2 reducer ()
.... 2.3 dispatch ()
.... 2.4 actionCreator ()
.... 2.5 Actions
.... 2.6 getState ()
.... 2.7 subscribe ()
.... 2.8 combineReducers ()
.... 2.9 initialState
3. React-redux
.... 3.1 Provider
.... 3.2 mapStateToProps ()
.... 3.3 mapDispatchToProps ()
.... 3.4 connect ()

Introduction


So you read my article about React (if not, I strongly recommend that you do this) and you started developing applications on it. But what is it? You notice how with the extension of your application it becomes more difficult to monitor the current state, it is difficult to monitor when and which components are rendered, when they are not rendered and why they are not rendered, it is difficult to monitor the flow of changing data. For this, there is a Redux library. React itself, although lightweight, but for a comfortable development on it you need to learn a lot.

And today we will analyze 2 libraries: Redux and React-redux. To use Redux, you do not need to download additional libraries, but if you use it in conjunction with the React-redux library, development becomes even more convenient and easier.

You can find all the examples from this article in this repository on Github. There is a fully customized React application using Redux and React-redux. You can use it as a starting point for your project. Change the file names and add new ones to this repository to create your own application. Look at the releases tab to find different versions of the application. The first contains the application using only Redux, the second using Redux and React-redux.

Motivation to use Redux

The local storage mechanism of the component that comes with the core library (React) is inconvenient in that such storage is isolated. For example, if you want different independent components to respond to an event, you will either have to pass the local state in the form of props to the child components, or raise it up to the nearest parent component. In both cases, this is not convenient. The code becomes more dirty, hard to read, and the components depend on their nesting. Redux removes this problem since the entire state is accessible by the entire component without much difficulty.

Redux is a universal development tool and can be used in conjunction with various libraries and frameworks. The same article will discuss the use of Redux in React applications.

1. Install Redux and get started


Whether you use Yarn or Npm, run one of these commands to install Redux:

# NPM
npm install redux

# Yarn
yarn add redux 

Most likely you are using the src folder in which your code base is stored. Files associated with redux are usually stored in a separate folder. To do this, I use the / src / store folder, which stores everything related to Redux and the application storage. You can name it differently or put it in another place.

Create a basic structure for the repository. It should look something like this: Of course, here I used primitive names for files, this is done for clarity. In this project, files should not be called like that.

.store
β”œβ”€β”€ actionCreators
β”‚ β”œβ”€β”€ action_1.js
β”‚ └── action_2.js
β”œβ”€β”€ actions
β”‚ β”œβ”€β”€ action_1.js
β”‚ └── action_2.js
β”œβ”€β”€ reducers
β”‚ β”œβ”€β”€ reducer_1.js
β”‚ β”œβ”€β”€ reducer_2.js
β”‚ └── rootReducer.js
β”œβ”€β”€ initialState.js
└── store.js



2. Redux


2.1 createStore


When you created the basic structure for working with the Redux repository, it's time to understand how you can interact with it.

The global application storage is created in a separate file, which is usually called store.js:

//   store.js
import { createStore } from 'redux';

const store = createStore(reducer);

export default store;

2.2 reducer ()


reducer is a pure function that will be responsible for updating the state. Here the logic is implemented in accordance with which the store fields will be updated.

This is what the basic reducer function looks like:

function reducer(state, action) {
    switch(action.type) {
        case ACTION_1: return { value: action.value_1 };
        case ACTION_2: return { value: action.value_2 };
        
        default: return state;
    }
}

The function takes the value of the current state and the event object (action). An event object contains two properties: the type of the event (action.type) and the value of the event (action.value).

For example, if you need to process the onChange event for an input field, then the event object may look like this:

{
    type: "ACTION_1",
    value: "   "
}

Some events may not need to convey any meaning. For example, when processing an onClick event, we can signal that an event has occurred, no more data is required, and how the logic embedded directly in the component itself should respond to it and partially in the reducer how it will respond. But in all cases it is necessary to determine the type of event. The reducer asks: what happened? actio.type is equal to "ACTION_1" yeah, then event number 1 happened. Next, you need to process it and update the state somehow. What the reducer will return and will be a new state.

ACTION_1 and ACTION_2 are event constants. Differently Actions. We will talk about them further 2.5 Actions .

As you may have guessed, a store can store a complex data structure consisting of a set of independent properties. Updating one property will leave other properties untouched. So from the example above, when event number one (ACTION_1) occurs, field number one (value_1) in the store is updated while field number two (value_2) remains untouched. In general, the mechanism is similar to the this.setState () method.

2.3 dispatch ()


To update the store, you must call the dispatch () method. It is called on the store object that you create in store.js. This object is called store so the update of the state in my case looks like this:

store.dispatch({ type: ACTION_1, value_1: "Some text" });

Onchange is an event constant which will be discussed later (see Actions ).

This function will call the reducer function, which will handle the event and update the corresponding storage fields.

2.4 actionCreator ()


Actually, passing an event object directly to dispatch () is a sign of bad tone. To do this, use a function called actionCreator. She does exactly what is expected. Creates an event! The call to this function must be passed as an argument to dispatch and passed to the actionCreator the necessary value (value). The base actionCreator is as follows:

function action_1(value) {
    return { 
        type: ACTION_1,
        value_1: value
    };
}

export default action_1;

Thus, the dispatch call should look like this:

store.dispatch(action_1("Some value"));

Using actionCreator, the code becomes cleaner.

2.5 Actions


actions are constants describing the event. Usually this is just a line with the name describing the event. For example, the constant describing event number one will look like this:

const ACTION_1 = "ACTION_1";

export default ACTION_1;

Again, in the project, you should name the constants according to the event that it describes: onClick, createUserSesion, deleteItem, addItem, etc. The main thing is to make it clear. Note that I did not write import anywhere, so be sure to import your constants before using them. Because it’s also customary to break constants into separate files by storing them in a special folder. Although some store them in a single file called actionTypes.js. Such a decision cannot be called not correct, but not ideal either.

2.6 getState ()


Using dispatch () they updated it, but now how to look at the new store value? There is no need to invent anything; there is a getState () method. It, like the dispatch method, is called on an instance of the store object. Therefore, for my example, the call

store.getState()

will return the value of the storage fields. For example, to see the value of the value_1 field, you will need to call

store.getState().value_1

2.7 subscribe ()


But how do you know when the state is updated? There is a subscribe () method for this. It is also called on the store instance. This method accepts a function that will be called each time the store is updated. He, as it were, β€œsigns” the function transferred to him for updating. For example, the following code will display a new store value on the console with each update (each time dispatch () is called).

store.subscribe(() => console.info(store.getState()))

This method returns the unsubscribe () function. Which allows you to "unsubscribe from the update." For example, if a component is removed from the DOM, you should unsubscribe its methods from updating in componentWillUnmount (). This lifecycle method is called when the component is unmounted and this is exactly the place where you should unsubscribe from the update. Simply put in a destructor.

2.8 combineReducers ()


combineReducers () allows you to combine several reducers into one.

If the logic for updating components is rather complicated and / or it is necessary to handle a large number of different types of events, then the root reducer may become too cumbersome. The best solution would be to split it into several separate reducers, each of which is responsible for processing only one type of event and updating a specific field.

Attention!
When you break the base reducer into several, the name of each of them should correspond to the field that it updates in the store.
For example, if a reducer updates field number one, then it might look like this:

function value_1(state, action) {
    switch(action.type) {
        case ACTION_1: return action.value_1;
        
        default: return state;
    }
}

export default value_1;

The name of the reducer (value_1) indicates which property it will update in the store. If you rename it to value_2 then it will update value_2. Therefore, keep this in mind!

When using a single reducer we show which field we want to update:

 case ACTION_1: return { value_1: action.value_1 };

But when you split your reducers you just need to return a new value:

case ACTION_1: return action.value_1;

Since it is not required to indicate which of the fields the reducer updates, because its name is the field that it updates.

2.9 initialState


initialState is an object representing the initial state of the store. It is the second optional argument to the createStore () method. With the creation of the repository, you can immediately declare the initial state for its fields. This object is desirable to create, even in cases where the announcement of the initial state is not required. Because this object helps to look at the structure of the repository and the name of its fields. A typical initialState object looks like this:

const initialState = {
    date_1: "value...",
    date_2: "value..."
};

export default initialState;

In some cases (when the component immediately uses the value from the store), its declaration may become mandatory, otherwise you will get an error: TypeError: Cannot read property 'value_1' of undefined.

Also, reducers should always return the current state by default. For example, if you use a single reducer, then the last value in switch should look like this:

default: return store;

If you divide reducers into independent functions, then it should return the value of the property for which it is responsible:

default: return store.value_1;

Also, if you do not pass the initialState object to createStore, you can return it from the reducer. In both cases, the initial state for the store will be initialized.

3. React-redux


It would seem that we have everything to use Redux. But in fact, using it without the React-redux package in React applications does not look very nice.

3.1 Provider


To use store in a component, you need to pass it to props:

ReactDOM.render(<Main store={store} />, document.getElementById('root'));

And then use in the component: this.props.state. To do this, react-redux provides the Provider method:

ReactDOM.render(
    <Provider store={store}>
        <Main />
    </Provider>, 
document.getElementById('root'));

Thus, the connect method will be able to use store. Otherwise, you get an error: Error: Could not find "store" in the context of "Connect (Main)". Either wrap the root component in a, or pass a custom React context provider to and the corresponding React context consumer to Connect (Main) in connect options.

You can also pass store directly to a component without wrapping it in a Provider and this will work. But better still use Provider.

3.2 mapStateToProps ()


This method is called whenever the store is updated and it is it that passes the necessary properties from store to the component. For example, a component must respond and update the UI every time the field number one (value_1) is updated. He does not need to react to updating other fields. If you are not using React-redux, you would have to use the subscribe () method to find out about the update and then somehow check whether the field number one was updated or not. In general, it is easy to understand that such code will look too dirty and redundant. Using mapStateToProps (), you can clearly determine which fields interest the component. And what fields should he react to.

Returning to the example above, if component one needs to get field number one (value_1) then mapStateToProps for it will look like this:

function (state) {
    return {
        value_1: state.value_1
    };
}
After inside the component, we can access the value_1 field through this.props.value_1. And every time this field is updated, the component will be rendered again.

You can create a separate folder in / src / store to store files, each of which will contain the mapStateToProps function for all your components. Or (as I did) use a single function that returns the mapStateToProps function for each component. Personally, I like this approach. Such a function is as follows:

function mapStateToProps(component) {
    switch(component) {
        case "Component_1": {
            return function (state) {
                return {
                    value_1: state.value_1
                };
            }
        }
        case "Component_2": {
            return function(state) {
                return {
                    value_2: state.value_2
                };
            }
        }
        default: return undefined;
    }
}

export default mapStateToProps;

This function takes an argument string with the name of the component as an argument and returns a mapStateToProps function which returns an object with the property from store necessary for this component. This function can be called mapStateToPropsGenerator ().

3.3 mapDispatchToProps ()


This function passes methods to the component to update the required store field. In order not to call dispatch directly from the component, you will use this method in order to pass to the props method a call to which will result in a dispatch call and updating the corresponding field. Just now it will look more elegant, and the code more understandable and clean.

For example, component number one should be able to update field number one from store. Then mapDispatchToProps for it will look like this:

function (dispatch) {
    return {
        changeValue_1: bindActionCreators(action_1, dispatch)
    };
};

Now to update the value_1 property, you will call changeValue_1 () through this.props.changeValue_1 (value). Without calling dispatch directly through this.props.store.dispatch (action_1 (value)).

bindActionCreators should be imported from redux. It allows you to wrap the dispatch and actionCreator functions in a single object. You may not use bindActionCreators, but then the code will look redundant. You should be old to implement some kind of functionality so that the code looks simple and miniature. Therefore, nothing more should be written.

Only clean and understandable code. The bindActionCreators (actionCreator, dispatch) method accepts two required parameters: the actionCreator function, which we have already discussed and dispatch. Returning a method for changing store fields.

As with mapStateToProps, I use the generator function to return the mapDispatchToProps function for each component:

import { bindActionCreators } from 'redux';
import action_1 from './actionCreators/action_1';
import action_2 from './actionCreators/action_2';

function mapDispatchToProps(component) { 
    switch(component) {
        case "Component_1": return function(dispatch) {
            return {
                change_value_1: bindActionCreators(action_1, dispatch)
            };
        };
        case "Component_2": return function(dispatch) {
            return {
                change_value_2: bindActionCreators(action_2, dispatch)
            };
        };
        default: return undefined;
    }
}

export default mapDispatchToProps;

3.4 connect ()


Well and now the climax! That without which all this will not work. This is the connect function.
It is she who connects mapStateToProps and mapDispatchToProps with the component and passes the necessary fields and methods to it. It returns a new wrapper component for your component. I do not know how to properly name such a component, because the React-redux documentation itself does not describe it. Personally, I add the _w ending for the wrapper components. Like _w = wrap Component. The component connection in this case looks like this:

const COMPONENT_1_W = connect(mapStateToProps("Component_1"), mapDispatchToProps("Component_1"))(Component_1);

And now in ReactDOM.render () you do not pass your component, but the one that the connect function returns.

If the component does not need to pass mapStateToProps or mapDispatchToProps to it, pass undefined or null to it.

All Articles