Redux

Introduction and usage in 

Motivation

  • Managing state is hard
  • Managing state in a asynchronous world is even harder.
  • We are mixing mutation and asynchronicity.

Three Principles

  • Single Source of truth

  • State is Read Only

  • Mutation 

Single Source of Truth

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    }, 
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

State is Read Only

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})

Mutations written as pure functions

(state, action) => state

 

Also known as Reducers

The Basics

  • Actions

  • Reducers

  • Store

But first: Demo Time

Actions

const ADD_MESSAGE = 'ADD_MESSAGE';

{
    type: ADD_MESSAGE,
    text: "Hello World!",
    channel: '#redux'
}
{
    type: LIKE_MESSAGE,
    index: 5
}
{
    type: SET_CURRENT_CHANNEL,
    channel: '#redux'
}

Action Creators

function addMessage(text, channel) {
    return {
        type: ADD_MESSAGE,
        text,
        channel,
    }
}

dispatch(addMessage(text);
const boundAddMessage = (text) => dispatch(addMessage(text));


boundAddMessage(text);

Reducers

Designing the State Shape

{
    currentChannel: '#redux',
    messages: [
        {
            text: 'Hello World',
            likes: 8,
            channel: '#redux',
        },
        {
            text: 'Heisann, Hoppsann',
            likes: 73,
            channel: '#redux',
        },
        {
            text: 'Yo!',
            likes: 2,
            channel: '#random',
        },
        {
            text: 'This is epic',
            likes: 3,
            channel: '#random',
        }  
    ]
}

Reducers

(prevState, action) => newState

const initialState = {
    currentChannel: '#redux',
    channels: []
}

function miniWoop(state, action) {
    if (typeof state === 'undefined') {
        return initialState;
    }

    return state;
}
const initialState = {
    currentChannel: '#redux',
    channels: []
}

function miniWoop(state = initialState, action) {
    return state;
}
function miniWoop(state = initialState, action) {
    switch (action.type) {
        case SET_CURRENT_CHANNEL:
            return {
                ...state,
                currentChannel: action.channel,
            }
        default:
            return state;
    }
}
function miniWoop(state = initialState, action) {
    switch (action.type) {
        case SET_CURRENT_CHANNEL:
            return {
                ...state,
                currentChannel: action.channel,
            }
        case ADD_MESSAGE:
            return {
                ...state,
                messages: [
                    ...state.messages,
                    {
                        text: action.text,
                        likes: 0,
                        channel: action.channel,
                    }
                }
            }
        default:
            return state;
    }
}
function miniWoop(state = initialState, action) {
    switch (action.type) {
        case SET_CURRENT_CHANNEL:
            return {
                ...state,
                currentChannel: action.channel,
            }
        case ADD_MESSAGE:
            return {
                ...state,
                messages: [
                    ...state.messages,
                    {
                        text: action.text,
                        likes: 0,
                        channel: action.channel,
                    }
                ]
            }
        case LIKE_MESSAGE:
            return {
                ...state,
                messages: [
                    ...state.messages.slice(0, action.index),
                    {
                        ...state.messages[action.index],
                        likes: state.messages[action.index].likes + 1
                    },
                    ...state.messages.slice(action.index + 1),
                ]
            };
        default:
            return state;
    }
}
function messages(state = [], action) {
    switch (action.type) {
        case ADD_MESSAGE:
            return [
                ...state,
                {
                    text: action.text,
                    likes: 0,
                    channel: action.channel,
                }
            ]
        case LIKE_MESSAGE:
            return [
                ...state.slice(0, action.index),
                {
                    ...state[action.index],
                    likes: state[action.index].likes + 1
                },
                ...state.slice(action.index + 1),
            ];
        default
            return state;
    }
}
function miniWoop(state = initialState, action) {
    switch (action.type) {
        case SET_CURRENT_CHANNEL:
            return {
                ...state,
                currentChannel: action.channel,
            }
        case ADD_MESSAGE:
        case LIKE_MESSAGE:
            return {
                ...state,
                messages: messages(state.messages, action),
            };
        default:
            return state;
    }
}
function currentChannel(state = '#redux', action) {
    switch (action.type) {
        case SET_CURRENT_CHANNEL:
            return action.channel;
        default:
            return state;
    }
}
function miniWoop(state = {}, action) {
    return {
        currentChannel: currentChannel(state.currentChannel, action),
        messages: messages(state.messages, action),
    }
}
const miniWoop = combineReducers({
    currentChannel,
    messages,
});

Store

  • Holds application state
  • A single store in a Redux application
  • getState()
  • dispatch()
  • subscribe(listener)
import { createStore } from 'redux';
import { miniWoop } from './reducers';

let store = createStore(miniWoop);
console.log(store.getState());

// Every time the state changes, log it
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// Dispatch some actions
store.dispatch(addMessage('Hello World', '#redux'));
store.dispatch(addMessage('This is a message', '#redux'));
store.dispatch(addMessage('Woop, Woop!', '#redux'));
store.dispatch(addMessage('4!', '#random'));
store.dispatch(likeMessage(0));
store.dispatch(likeMessage(1));
store.dispatch(likeMessage(2));
store.dispatch(likeMessage(2));
store.dispatch(setCurrentChannel('#random'));

// Stop listening to state updates
unsubscribe();

DEMO TIME

Data Flow

  1. You call store.dispatch(action)
  2. Redux calls the root reducer function
    • The root reducer function might  combine multiple recuce functions.
  3. Redux saves the new state, and notifies all listeners.

Usage With React

  • react-redux
  • Smart and Dumb Components
const MessageList = ({ messages, dispatch }) => 
    <table border="1">
        <thead><tr><th>Likes</th><th></th><th>Text</th></tr></thead>
        <tbody>
            { messages.map( (message) => 
                <Message 
                    message={message}
                    key={message.index}
                    index={message.index}
                    dispatch={dispatch} />) }
        </tbody>
    </table>

Dumb Components

import { connect } from 'react-redux'

const MiniWoop = ({ dispatch, messages, currentChannel }) =>
    <div>
        <ChannelChooser dispatch={dispatch} currentChannel={currentChannel}/>
        <AddMessage dispatch={dispatch} currentChannel={currentChannel} />
        <MessageList dispatch={dispatch} messages={messages} />
    </div>

const ConnectedMiniWoop = connect(select)(MiniWoop);

Smart Components

const select = state => ({
    currentChannel: state.currentChannel,
    messages: state.messages
        .map((msg, index) => ({
                ...msg,
                index
        }))
        .filter(msg => 
            msg.channel === state.currentChannel),
});
import { Provider } from 'react-redux'

import store from '../store'

const TopLevel = (props) =>
    <Provider store={store}>
        <ConnectedMiniWoop />
    </Provider>

Provider

import { setCurrentChannel } from '../actions'

const ChannelChooser = ({ dispatch, currentChannel }) =>
    <div>
        { channels.map((channel) => 
            <Channel 
                channel={channel}
                isCurrent={currentChannel === channel}
                onClick={() => dispatch(setCurrentChannel(channel))} />)
        }
    </div>

const Channel = ({ channel, isCurrent, onClick }) => 
    <button onClick={onClick} className={ isCurrent ? 'active' : '' }>{ channel }</button>

Dispatching

Async

Or "The stuff that makes things complicated"

 

Solved by "Thunk Action Creators"

export function requestChannels() {
  return {
    type: MessageConstants.CHANNELS_REQUEST,
  };
}

export function receiveChannels(channels) {
  return {
    type: MessageConstants.CHANNELS_SUCCESS,
    channels: channels,
  };
}

export function fetchChannels() {
  return dispatch => {
    dispatch(requestChannels());
    return xhr.get(urls.channels)
      .then(channels => dispatch(receiveChannels(channels)));
  };
}

Usage in Real World

AKA Woop

{
    currentChannel: '#redux',
    messages: [
        {
            text: 'Hello World',
            likes: 8,
            channel: '#redux',
        },
        {
            text: 'Heisann, Hoppsann',
            likes: 73,
            channel: '#redux',
        },
        {
            text: 'Yo!',
            likes: 2,
            channel: '#random',
        },
        {
            text: 'This is epic',
            likes: 3,
            channel: '#random',
        }  
    ]
}
{
    currentChannel: '#redux',
    channels: [
        '#redux': {
            messages: [1,2]
        },
        '#redux': {
            messages: [3,4]
        }
    ]
    messages: {
        1: {
            text: 'Hello World',
            likes: 8,
        },
        2: {
            text: 'Heisann, Hoppsann',
            likes: 73,
        },
        3: {
            text: 'Yo!',
            likes: 2,
        },
        4: {
            text: 'This is epic',
            likes: 3,
        }  
    }
}

Normalizr

.
├── actionsCreators
│   ├── AccountActionCreators.js
│   └── MessageActionCreators.js
├── app.js
├── components
│   ├── App.js
│   ├── WoopApp.js
│   └── ...
├── constants
│   ├── AccountConstants.js
│   └── MessageConstants.js
├── notifier.js
├── old_stores
│   ├── AccountStore.js
│   └── MessageStore.js
├── reducers
│   ├── accounts.js
│   ├── index.js
│   └── messages.js
├── router.js
├── urls.js
├── utils
│   ├── ...
└── ws-events.js

Changed from Actions

to ActionCreators.

Needs cleanup

App.js is new - the Provider

WoopApp.js - now the smart Component.

Added many new constants.

Old Stores for Reference

The Reducers.

Should probably change to redux-router.

WebSocket changed to dispatch to store

Questions?

https://slides.com/sindreij/redux

https://github.com/sindreij/miniwoop

 

Redux documentation: http://redux.js.org/docs/introduction/index.html

Redux

By sindreij

Redux

  • 666