Redux, Store, Actions, Reducers and logger: Get Started and a little further

Published Aug 23, 2017Last updated Nov 20, 2017
Redux, Store, Actions, Reducers and logger: Get Started and a little further

Implementation and Setup to get started with redux, actions, reducers and Logger inside any react.js or react-native application.

Content

Store and Provider

You can either follow this post OR directly test the final app from my github repo here.

Let's start by installing packages.

npm install --save redux react-redux redux-logger redux-thunk

Every single component inside our application will have access to the store by using the connect helper from the react-redux library, to do this you'll have to follow these steps:

  • Create an instance of the redux store.
  • Create a provider tag from react-redux library.
  • Then render that provider tag passing in the store as a prop, then any child component to this provider tag will have access to this store through the context system inside of react.
  • Create a store (index.js) in a separate folder.
// store/index.js 

import { createStore, compose, applyMiddleware } from ‘redux’;
import thunk from ‘redux-thunk’;
const store = createStore({
    reducers,
    {}, // default state of the application
    compose(
        applyMiddleware(thunk)
    )
})
export default store;
  • Create default or dummy reducer by creating a new folder Reducers/
  • Create index.js and add a dummy reducer to get started.
import { combineReducers } from 'redux';
export default combineReducers({
    temp: () => {return {}}
})

Reducer must return an object or a string or a number and never return undefined.

import reducers from '../Reducers';
  • import the store in the src/App.js
// In src/App.js
import { Provider } from 'react-redux'; 
import store from './Store';
import CustomTextInput from './Components/CustomTextInput';


<Provider store={store}>
   <CustomTextInput/>
</Provider>

Why action creators?

We want to minimize the responsibilities of anyone of our components. We want to make a component as simple and as dumb as possible, so all our compenent is going to do is show the UI. Whenever our Component is going to do some logical work we would call the action creator.

User types something -> call action creator with new text ->
action creator returns an action -> action sent to all reducers ->
reducers calculates new app state -> state sent to all components ->
component rerender with new state -> wait for new change -> repeat.

Structure

I like to structure my apps this way.

- index.ios.js
- index.android.js
- Tests/
- src/
  - App.js # Entry point
  - Components/
      - CustomTextInput.js  
  - Actions/
      - CustomTextInputActions.js
      - index.js
      - types.js
  - Reducers/
      - index.js
  - Store/
      - index.js
  - Navigation/
  - Constants/
  - Screens/
  - Assets/
  - Services/

Actions and Reducers in 14 Steps

Let's say we want to update text in TextInput via actions and reducers, so how is the entire process going to look like?

Our Goal.

Screen Shot 2017-08-23 at 10.46.14 AM.png

Step-1

Create a event handler onChangeText and attach a callback this.onTextChange and define that function in the same class.

export default class CustomTextInput extends Component {
  onTextChange = (text) => {
  // Step-12
  }
  render() {
    return (
      <View
        style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}
      >
        <TextInput
          onChangeText={(text) => this.onTextChange(text)}
          placeholder="Search"
          style={{ height: 80, width: 200, fontSize: 30 }}
        />
      </View>
    );
  }
}

Using the above syntax spares us to type this.onTextChange.bind(this) in other words it knows which this we are talking about.

Step-2

Create a new folder Actions

Step-3

Create a new file inside actions folder Actions/index.js

Step-4

Create a file Actions/types.js in actions folder and in that file we create variables to store action types.

One file for all the action types.

export const TEXT_CHANGED = 'text_changed';

Step-5

Create a new Actions file for handeling our CustomTextInput Component's actions Actions/CustomTextInputActions.js

import {
  TEXT_CHANGED
} from './types';

export const textChanged = (text) => {
  return {
    type: TEXT_CHANGED,
    payload: text
  };
}

Step-6

Import Actions in index.js

export * from './CustomTextInputActions';

Step-7

Import action creator in the file which has our TextInput component and import the connect helper from react-redux

import { connect } from 'react-redux';
import textChanged from '../Actions';

class CustomTextInput extends Component {
 ... bla bla bla
}

export default connect(null, textChanged)(CustomTextInput);

Where are we right now?

user types something -> calls event handler -> calls action creator with new text
-> action creator returns an action -> sends that actions to all the reducers

We haven't created a reducer yet and we need one to catch that action and really do something about it, so let's move to next step.

Step-8

Create a new folder Reducers

Step-9

Create a new file inside the reducers folder TextReducer.js to handle that action.

const INITIAL_STATE = {
  text: ''
};


export default ( state=INITIAL_STATE , action ) => {
  /* We always have the exact same format for every reducer we write, we always
  have a switch statement and depending on the action it will decide what to 
  do with it.
  
  we can never return undefined from a reducer, every time our reducers are call
  we should have a default state.
  */
  switch (action.type) {
    case:
       // in step 11
      
    default:
      // Return the initial state when no action types match.
      return state
  }
}

Step-10

Create a new file inside the reducers folder index.js

import { combineReducers } from 'redux';
import TextReducer from './TextReducer';

export default combineReducers({
  // the keys here are going to be the property of state that we are producing.
  text_reducer: TextReducer
  
});

Now that we have created a reducer and action creator, we need to make sure that our reducer watches for a action of appropriate type.

Step-11

Import that action type into our reducer. TextReducer.js and add a case for it.

import {
  TEXT_CHANGED
} from '../Actions/types';

/* Remember that TEXT_CHANGED should be defined and must have a value otherwise it
will be undefined and no error would popup and in the reducer we will have a
case of undefined

case undefined:
   return ...

which is not what we want.
*/

const INITIAL_STATE = {
  text: ''
};

export default ( state=INITIAL_STATE , action ) => {
  switch (action.type) {
    case TEXT_CHANGED:
       /*
          slice of state (that the reducer last published)  +  action
                         |
                    into the reducer
                         | 
              returns a new slice of state
       
       After our reducer runs, redux looks at the old value of the state and the
       new one. `is newState === oldState?` (matches the object) we must return a
       new object. (have to take care of immutable objects)
       
       Make a new object, take all the properties from the existing state object
       and throw that into our new object then define the property `text`, give it
       a value of action.payload and put it one top of whatever properties we had
       on the old state object.
       
       Now, since old state object already has a text property so, it will be
       overwritten with a new value.
       */
       
       return {...state, text: action.payload}
      
    default:
      /*
      We will just return the state. Return the initial state when nothing changes
      hence no re-rendering.
      */
      return state
  }
};

Step-12

Update our callback method and call the action creator so that it could update the state.

onTextChange = (text) => {
    this.props.textChanged(text)
}

Step-13

Define a mapStateToProps function in the file containing Component class.

This will be called with out global application state and we should return just the property we care about from that state object.

const mapStateToProps = state => {
  return {
    text: state.text_reducer.text
  }

}

// Pass it as the first argument to our connect function.

export default connect(mapStateToProps, textChanged)(CustomTextInput);

Step-14

Pass the value into the component

value={this.props.text}

Now each time you update text in TextInput it calls an Action responsible for updating the state and hence the text in the TextInput.

Redux-logger

To test actions being triggered in the development environment, I use redux-logger

Screen Shot 2017-08-23 at 10.32.53 AM.png

import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import reducers from '../Reducers';

const middleWare = [];

middleWare.push(thunk)

const loggerMiddleware = createLogger({
  predicate: () => process.env.NODE_ENV === 'development',
});
middleWare.push(loggerMiddleware)

const store = createStore(
  reducers,
  {},
  compose(
    applyMiddleware(...middleWare)
  )
);

export default store;

Conslusion

Thank you for reading — I hope you found this post helpful. If you have any questions, feel free to reach out to me! More posts, Gists
This is me on GitHub, Twitter. Let's connect on LinkedIn.

Discover and read more posts from Kuldeep
get started
Enjoy this post?

Leave a like and comment for Kuldeep

12