Skip to content

Migration to Redux (Part 1)

Pavel Rodionov edited this page May 4, 2018 · 1 revision

Motivation

Most of the motivation is taken from the Redux official page. As the app is going to grow in the future, managing the state of data will get ever more complicated.

Redux attempts to make state mutations predictable and it would be easier to migrate the project to Redux now while it is still at the very beginning.

A large list of Resources on Redux

Basic concepts

Actions

  • Actions are payloads of information that send data from your application to your store
  • Actions are the only source of information for the store. You send them to the store using
store.dispatch()
  • Actions must have a type property that indicates the type of action being performed.

A basic Flux Standard Action:

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'
  }
}
  • Types should typically be defined as string constants
  • Action creators - functions that create actions.

Reducers

  • Rducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.
  • The reducer is a pure function that takes the previous state and an action, and return the next state.
(previousState, action) => newState

Store

  • Holds application state;
  • Allows access to state via getState();
  • Allows state to be updated via dispatch(action);
  • Register listener via subscribe(listener);
  • Handles unregistering of listeners via the function returned by subscribe(listener)

Migration

The process of migration was based on the youtube video Redux Crash Course With React

1. Install following packages:

npm i redux react-redux react-thunk

a few words about these packages: react-redux - library that binds two together react-thunk - middleware for redux

2. in App.js file

import { Provider } from "react-redux"
import store from "./store"

then we wrap everything we return in render()

return (
  <Provider store={store}
    ...
  <Provider />
)

<Provider /> is the higher-order component provided by React-Redux that lets you bind Redux to React

A higher-order component(HOC) is an advanced technique in React for reusing component logic. HOCs are not part of React API, per se. They are a pattern that emerges from React's compositional nature.

Concretely, a higher-order component is a function that takes a component and returns a new component (whereas a normal component transforms props into UI).

3. create store.js file

we import store in App.js.

So lets create one

`createStore(reducer, [preloadedState], [enhancer]) - creates a Redux store that holds the complete state tree of your app. There should be a single store in your app.

Arguments

  1. reducer(Function): a reducing function that returns the next state tree, given the current state tree and an action to handle
  2. [preloadedState](any): the initial state.
  3. [enhancer](Function): the store enhancer.

this is what we have in our store.js file

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const initialState = {};
const middleware = [thunk];
const store = createStore(
  rootReducer,
  initialState,
  applyMiddleware(...middleware)
);

export default store;

lets have a look at what we import: import { createStore, applyMiddleware } from "redux"

applyMiddleware Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store's dispatch method for fun and profit. The most common use case for middleware is to support asynchronous actions without much boilerplate code or a dependency on a library like Rx. For example, redux-thunk lets the action creators invert control by dispatching functions. They would receive dispatch as an argument and may call it asynchronously. Such functions are called thunks

Arguments

  • ...middleware(arguments): Functions that conform to the Redux middleware API. Each middleware receives Store's dispatch and getState functions as named arguments, and returns a function. That function will be given the next middleware's dispatch method, and is expected to return a function of action calling next(action) with a potentially different argument, or at a different time, or at a different time, or maybe not calling it at all. The last middleware in the chain will receive the real store's dispatch method as the next parameter, thus ending the chain. So, the middleware signature is ({ getState, dispatch }) => next => action.

Returns

(Function) A store enhancer that applies the given middleware. The store enhancer signature is createStore => createStore but the easiest way to apply it is to pass it to createStore() as the last enhancer argument.

This is hardly the most straightforward explanation. In our case the only middleware we pass to createStore is thunk

import thunk from "redux-thunk"
...
const middlware = [thunk];
const store = createStore(
  rootReducer,
  initialState,
  applyMiddleware(...middleware)
);

The redux-thunk` docs are not very descriptive. This what I have found online

By default, Redux action creators don't support asynchronous actions like fetching data, so here is where we utilise Redux Thunk. Thunk allows you to write action creators that return a function instead of an action.

This is important for us, as our application fetch data from the Guardian API and backend.

Finally, we import rootReducer from ./reducers/index.js and we have a look at it in the next stage.

4. in src create reducers folder

inside this folder, lets first create index.js file

import { combineReducers } from "redux";
import guardianFetchReducer from "./guardianFetchReducer";

export default combineReducers({
  articles: guardianFetchReducer
});

the guardianFetchReducer is written and discussed in the following stages. Lets focus on combineReducers to which we pass guardianFetchReducer and which is imported in the store.js as rootReducer

From Redux docs

combineReducers as your app grows more complex, you will want to split your reducing functions into separate functions, each managing independent parts of the state.

The combineReducers helper function turns an object whose values are different reducing functions into a single reducing function you can pass to createStore

The resulting reducer calls every child reducer, and gathers their results into a single state object.

Arguments

  1. reducers (Object): an object whose values correspond to different reducing functions that need to be combined into one.

Returns

(Function): a reducer that invokes every reducer inside the reducers object, and constructs a state object with the same shape.

in our case we only pass the following object {articles : guardianFetchReducer}.

just before we look at the guardianFetchReducer, some notes on any reducer passed to combineReducers.

  • for any action that is not recognised, it must return the state given to it as the first argument
  • it must never return undefined. It is too easy to do this by mistake via an early return statement, so combineReducers throws if you do that instead of letting the error manifest itself somewhere else.
  • if the state given to it is undefined, it must return the initial state for this specific reducer.

5. inside the reducers folder lets create guardianFetchReducer.js file

import { FETCH_GUARDIAN } from "../actions/types";

const initialState = {
  articles: []
};

export default function(state = initialState, action) {
  switch (action.type) {
    case FETCH_GUARDIAN:
      return {
        ...state,
        articles: action.payload
      };
    default:
      return state;
  }
}

We will deal with FETCH_GUARDIAN type in the next step.

For now, lets recall that reducer is a pure function that takes the previous state and an action, and returns the next state. This is exactly what happens here.

This function complies with the rules mentioned above

returns previous state if it doesn't recognised the action

...
   default:   
     return state;
...

never returns undefined if the state given to it is undefined, it must return the initial state for this specific reducer

const initialState = {
  articles: []
};

export default function(state = initialState, action) {
...

Lets have a closer look at what we return

    case FETCH_GUARDIAN:
      return {
        ...state,
        articles: action.payload
      };

first of all we want to return a current state and we do it using spread operator ...state and we add articles: action.payload (why action.payload will become clear when we talk about actions later.

6. lets create actions folder in src

the very first file we create there is types.js. This file is just defining a couple of constants.

export const FETCH_GUARDIAN = "FETCH_GUARDIAN";

this const is imported in step 5.

7. action creation

inside actions folder we create guardianFetchAction.js file.

import { FETCH_GUARDIAN } from "./types";
import { guardianRequest } from "../utils/fetchGuard";

export function fetchGuardian() {
  return function(dispatch) {
    guardianRequest().then(articles =>
      dispatch({
        type: FETCH_GUARDIAN,
        payload: articles.response.results
      })
    );
  };
}

here we also import "FETCH_GUARDIAN" from "./type" which is just action type. guardianRequest` is a function that fetch data from the Guardian API (adds dev key to it).

The guardianRequest function used to be part of componentDidMount() in MultppleArticles component. Now we took it away from there and it became part of the actioncreator. We also deleted the state articles from the component as now it's going to be handled by store

Finally, when we get the data from guardianRequest we dispatch it to the reducer

...
.then(articles =>
      dispatch({
        type: FETCH_GUARDIAN,
        payload: articles.response.results
      })
    );
...

Our reducer already has case for `FETCH_GUARDIAN'

    case FETCH_GUARDIAN:
      return {
        ...state,
        articles: action.payload
      };

8. Making changes to MulptipleArticles component

after we deleted componentDidMount in the previous step. Here are the additions we have to make to our component.

...
import { connect } from "react-redux";
import { fetchGuardian } from "../actions/guardianFetchAction";
...
class MultipleArticles extends Component {
...
  componentWillMount() {
    this.props.fetchGuardian();
  }
...
}

const mapStateToProps = state => ({
  articles: state.articles.articles
});

export default connect(mapStateToProps, { fetchGuardian })(MultipleArticles);

The full description for connect could be found here

The connect has two sets of parenthesis and the second one has the component MultipleArticles

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connects a React component to a Redux store. connect is a facade around connectAdvanced, providing a convenient API for the most common use cases.

It does not modify the component class passed to it; instead, it returns a new, connected component class for you to use.

Arguments

  • [mapStateToProps(state, [ownProps]): stateProps](Function): if this argument is specified, the new component will subscribe to Redux store updates. This means that any time the store is updated, mapStateProps will be called. The results of mapStateProps must be a plain object, which will be merged into the component's props. If you don't want to subscribe to store updates, pass null or undefined in place of mapStateToProps.

If your mapStateToProps function is declared as taking two parameters, it will be called with the store state as the first parameter and the props passed to the connected component as the second parameter, and will also be re-invoked whenever the connected component receives new props as determined by shallow equality comparisons. (The second parameter is normally referred to as ownProps by convention.)

In our case

const mapStateToProps = state => ({
  articles: state.articles.articles
});

the first .articles after state is from our root reducer, and second .articles is from our guardianFetchReducer

I have a couple of questions with this step:

  • in componentWillMount we call this.props.fetchGuardian(). At what stage, `fetchGuardian()' has became part of props.
  • when we connect(mapStateToProps, { fetchGuardian }), what happens when we pass { fetchGuardian }

So, I deleted { fetchGuardian } in connect and immediately I received an error: _TypeError: this.props.FetchGuardian is not a function`.

Having a closer look at the docs for the second argument that we pass to conect is mapDispatchToProps

[mapDispatchToProps(dispatch, [ownProps]):dispatchProps](_Object_ or _Function_): if an object is passed, each function inside it is assumed to be a Redux action creator An object with the same function names, but with every action creator wrapped into a dispatch` call so they may be invoked directly, will be merged into the component's props.

Sound ligit!

If a function is passed, it will be given dispatch as the first parameter. It is up to you to return an abject that somehow uses dispatch to bind action creators in your own way.

If your mapDispatchToProps function is declared as taking two parameters, it will be called with dispatch as the first parameter and the props passed to the connected component as the second parameter, and will be re-invoked whenever the connected component receives new props. (The second parameter is normally referred to as ownProps by convention.)

If you do not supply your own mapDispatchToProps function or object full of action creators, the default mapDispatchToProps implementation just injects dispatch into your components props.

??? The sentence about injects dispatch into your components props needs firther clarification.

Here, it is worth looking at one more time at Redux (docs)[https://redux.js.org/api-reference/store] for dispatch(action)

Dispatches an action. This is the only way to trigger a state change.

The store's reducing function will be called with the current getState() result and the given action synchronously. Its return value will be considered the next state. It will be returned from getState() from now on , and the change listeners will immediately be notified.

???

finally, in your component, if you used this.state. for articles it should be replaced with this.props.articles, as mapStateToProps made articles available in component as part of props

9.

Finally, it is recommended to add PropTypes to the component

import PropTypes from "prop-types";
...
Posts.propTypes = {
  fetchGuardian: PropTypes.func.isRequired,
  articles: PropTypes.array.isRequired
};

I will make a separate wiki about it.