ruk·si

♻️ Redux

Updated at 2017-06-09 07:42

Redux is a predictable state container for JavaScript apps.

With Redux you have to:

  • Describe application state as plain objects (state objects).
  • Describe changes in the system as plain objects (actions).
  • Describe the logic for handling changes as pure functions (reducers).

Redux is good for these cases:

  • you want to persist whole application state to a local storage
  • you want to send whole application state as a single request
  • you want to serialize step-by-step details from user behavior
  • you want to send a lot of actionable events (actions) over network
  • you want undo functionality or need to see state history for another reason
  • you want to provide multiple UIs but keep the business logic the same
  • you frequently need to change state from deep within a complex application

Never use Redux with React if you are a newcomer. Try them out separately.

You have a single immutable state object store. The whole application state is stored in a single Redux store. This is an important difference between Flux where you have multiple stores. You can split your data handling logic with reducer composition covered later.

{
    todos: [],
    visibilityFilter: 'SHOW_ALL'
}

Store has three main functionalities:

  • createStore() creates the store, where you can optionally pass the initial state
  • getState() access current state
  • dispatch(action) update state via an action
  • subscribe(listener) listen for state changes; and remove listener using the function returned by subscribe

You are not allowed to modify that store on your own. You must send actions that creates a new store based on the previous one. type is the only required property of actions, all the rest are up to you.

{type: 'ADD_TODO', id:0, text: 'hello'}
{type: 'COMPLETE_TODO', id: 0}
{type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_COMPLETED'}

In a real application, make sure to use constants for your action types.

export const ADD_TODO = 'ADD_TODO'

// ...

import {ADD_TODO} from '../action-types'

Reducer define the logic what happens on actions. Reducers must be pure functions that receives state object with action and return new state object. Reducers can't have code side effects, can't modify the passed parameters and cant call non-pure functions like Date.not(). One storage can have multiple reducers.

counter = (state = 0, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state
    }
}

Working example with reducer, state, store and actions:

// reducer that receives previous state and an action
counter = (state = 0, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state
    }
}

// attaching reducer to a new store and checking the initial state
var store = Redux.createStore(counter);
console.log(store.getState());      // 0

// listen for state changes and send actions
store.subscribe(() => console.log(store.getState()))
store.dispatch({type: 'INCREMENT'}) // 1
store.dispatch({type: 'INCREMENT'}) // 2
store.dispatch({type: 'DECREMENT'}) // 1

You want to split your reducer into multiple functions later. The shape of the state object will be what you pass to combineReducers, but each reducer gets only one part of the total state.

counter = (state = 0, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state
    }
}

todos = (state = [], action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return state.concat([action.text])
        default:
            return state
    }
}

const reducers = Redux.combineReducers({todos: todos, counter: counter})
const store = Redux.createStore(reducers);
console.log(store.getState());      // {todos: [], counter: 0}

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

store.dispatch({type: 'INCREMENT'}) // {todos: [], counter: 1}
store.dispatch({type: 'INCREMENT'}) // {todos: [], counter: 2}
store.dispatch({type: 'DECREMENT'}) // {todos: [], counter: 1}

store.dispatch({type: 'ADD_TODO', text: 'Buy milk'})
                                    // {todos: ['Buy milk'], counter: 1}

If you can use ES6, neat trick is use import *. This will "open" up the object for the combineReducers call.

import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

Action creators are functions that create actions. They make creating actions reusable, portable and easier to test. Never dispatch in an action creator, that is a Flux thing.

const addTodo = (text) => {
    return {type: 'ADD_TODO', text};
}

Bound action creators also have a dispatch in them.

const boundAddTodo = (text) => store.dispatch(addTodo(text));
boundAddTodo('Buy milk');

Never mutate the state passed to reducers. Use Object.assign({}, state, ...) to copy it or otherwise create a new variable.

return Object.assign({}, state, {
    key: 'value'
});

Data flow is always unidirectional in Redux. It's a lot easier to reason around if you have deep component structure relying on the same state or a lot of asynchronous actions to be taken.

  1. You call store.dispatch(action)
  2. Reducers of your store are called
  3. Root reducer, usually combineReducers() combines results of reducers
  4. All store.subscribe(listener) registered listeners are called

Using Redux with React

Think of your React components as either presentational or container.

Presentational components don't have dependencies to rest of your app. So no touching the Redux store or loading data and have only UI related state.

Container components manage the data. They subscribe to Redux state and dispatch Redux actions. React Redux library connect() will usually handle creating these for you.

To use connect(), you need to define a special function mapStateToProps. It tells how to transform the current Redux store state into the props.

const getVisibleTodos = (todos, filter) => {
    switch (filter) {
        case 'SHOW_ALL':
            return todos
        case 'SHOW_COMPLETED':
            return todos.filter(t => t.completed)
        case 'SHOW_ACTIVE':
            return todos.filter(t => !t.completed)
    }
}


const mapStateToProps = (state) => {
    return {
        todos: getVisibleTodos(state.todos, state.visibilityFilter)
    }
}

You can define a function mapDispatchToProps() to dispatch actions.

const mapDispatchToProps = (dispatch) => {
    return {
        onTodoClick: (id) => {
            dispatch(toggleTodo(id))
        }
    }
}

Then we connect() the component with these two functions.

import { connect } from 'react-redux'

const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList)

export default VisibleTodoList

Use React Redux component <Provider> to pass Redux store automatically to all children.

import { Provider } from 'react-redux'

let store = createStore(todoApp)

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

Sources