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

There are different ways to structure your app.

Generally for small applications

- 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/

Redux Container Design Pattern

- index.ios.js - index.android.js - Tests/ - App/ - Components/ - Config/ - Containers/ - I18n/ - Reducers/ - index.js - Store/ - index.js - Navigation - Themes/ - Assets/ - Services/

Have a look at these links:

Putting tests in the same folder.

Easier to write and modify tests when the application goes big.

Components/ CustomTextInput.js CustomTextInput.test.js



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.

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.

Create a new folder Actions

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

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';

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 }; }

Import Actions in index.js

export * from './CustomTextInputActions';

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.

Create a new folder Reducers

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 } }

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.

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 } };

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

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

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);

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.

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

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;

NOTE: This example is only to provide a better understanding, I wouldn't recommend using this approach for such a simple task.

Conclusion

Thank you for reading this post — I hope you found this helpful. You can find me on GitHub, LinkedIn and CodeMentor. If you have any questions, feel free to reach out to me!

More posts: