Redux

Eine Übersicht für Redux (Weiterentwicklung von Flux).

Links:

TODO Anschauen redux-ducks - Code-Konvention, wie man Actions, Action creator und Reducer definiert.

TODO Anschauen: redux-devtools-extension - Extension für Chrome und Firefox.

TODO Action creators

TODO Anschauen: bindActionCreators

TODO Anschauen: Lib redux-thunk integriert AJAX calls in Redux

TODO Anschauen: redux-saga

TODO State normalisieren

Quelle: Blog-Artikel 10 Redux tips to scale your dev team

// Nicht normalisiert
const state = {
  users: [
    { id: 1, name: "Bat", groups: [{ name: "admin" }, { name: "regular" }] },
    { id: 2, name: "Vince", groups: { name: "admin" } },
    { id: 1, name: "Romain", groups: { name: "normal" } }
  ]
}

// Normalisiert
const state = {
  users: {
    1: { id: 1, name: "Bat", groups: [1, 2] },
    2: { id: 2, name: "Vince", groups: [1] },
    3: { id: 3, name: "Romain", groups: [2] }
  },
  groups: {
    1: { id: 1, name: "admin" },
    2: { id: 2, name: "regular" }
  }
}

Für einfacheren Zugriff aus normalisierten State Selektoren verwenden:

const getGroupsById = (state, ids) => ids.map(id => state.groups[id])
const getUsersWithGroupsById = (state, ids) => ids.map(id => ({...state.users[id], groups: getGroupsById(state.users[id].groups)}))

TODO Anschauen: redux-logger - Logging-Middleware für Redux

Was ist Redux?

TODO Formulieren

flux
  Core concepts
    One way data flow
      Views send actions to dispatcher
      Actions are only data, no logic
      Dispatcher sends action to store
      Store changes data
      Views update
    No event chaining
      Hauptunterschied zu MVC. Der Hauptgrund, warum Facebook Flux als Ersatz für MVC entwickelt hat
    Entire application state is resolved in store before views update
    Data manipulation only happens in one place
    Offizielle FB Flux Lib
      Nicht benutzen! OK zum rumspielen, ist aber mehr eine Art Proof-of-Concept zum Konzept-Paper
      Benötigt viel Boilerplate-Code
    ca. 20 Alternativen mit ähnlichem Konzept
  Redux
      Beliebteste Flux-Implementierung
      Leicht geändertes Konzept zur originalen Flux-Architektur
        Siehe [Video](https://www.youtube.com/watch?v=p8tqhf5qKOI?t=14m37s)
        Dispatcher ist Teil des Store
        Es gibt nur einen Store (state tree)
        Store verwendet Reducers
        Reducer ändern die Daten (Im originalen Flux macht dies der Store)
          Dadurch ist Daten-Änderungs-Logik gut gekapselt
          Store wird sehr einfach und hält nur noch die Daten und macht Event-Handling
        Views/Actions wie Original-Flux

Redux verwenden

src/app/state/actions.js anlegen:

export const myAction = (param) => ({
    type: 'myAction',
    param
})

src/app/state/reducers.js anlegen:

import { combineReducers } from 'redux'

/*
 * App state:
 *
 *  {
 *      navigation: {
 *          sidebar: 'enter-routing'
 *      }
 *  }
 */

const navigation = (state, action) => {
    state = state || {
        sidebar: 'my-sidebar'
    }

    switch (action.type) {
        case 'myAction':
            return {
                // ...
            }
        default:
            return state
    }
}

export default combineReducers({
    navigation
})

src/app/state/store.js anlegen:

import { createStore } from 'redux'
import mainReducer from './reducers'

export default createStore(mainReducer)

Actions dispatchen:

import { myAction } from 'app/state/actions'
store.dispatch(myAction(myParamValue))

Auf Store-Änderungen hören (Low-level ohne state-Selection):

let unsubscribe = store.subscribe(() => {
    // Called every time after an action
})
unsubscribe()

Auf Store-Änderungen hören (mit state-Selection):

let unsubscribe = observeStore(
    store,
    state => state.navigation, // Select state
    navigation => {
        // Called every time if `state.navigation` has changed
    })

unsubscribe()

Dazugehörige Util-Methode observeStore:

/**
 * Observes a react store with state selection
 *
 * @param store the react store
 * @param select {function} selects the wanted part of the state.
 *        Signature: (state) => substate
 * @param onChange {function} Called when the selected sub state has changed.
 *        Signature: (substate) => void
 * @returns {function} Unsubsriber - call it to unsubscribe.
 *          Signature: () => void
 */
// Source see: https://github.com/reactjs/redux/issues/303#issuecomment-125184409
export function observeStore(store, select, onChange) {
    let currentState;

    function handleChange() {
        let nextState = select(store.getState());
        if (nextState !== currentState) {
            currentState = nextState;
            onChange(currentState);
        }
    }

    let unsubscribe = store.subscribe(handleChange);
    handleChange();
    return unsubscribe;
}

Redux mit React

Aus Tutorial Using Redux with React:

From the very beginning, we need to stress that Redux has no relation to React. You can write Redux apps with React, Angular, Ember, jQuery, or vanilla JavaScript.

That said, Redux works especially well with libraries like React and Deku because they let you describe UI as a function of state, and Redux emits state updates in response to actions.

Links:

Konzepte

Quelle: Tutorial Using Redux with React

Trennung von "Presentational Components" und "Container Components":

Presentational Component Container Component
Aufgabe Aussehen (Markup, Styles) Funktionsweise (Daten laden, State-Updates)
Kennt Redux nein ja
Daten lesen Liest Daten aus Props Hört auf Redux-State
Daten ändern Ruft Callback aus Props auf Dispatcht Redux-Actions
Werden geschrieben von Hand generiert von react-redux (durch connect)

Presentational Components sind also ganze normale React-Komponenten ohne Abhängigkeit zu Redux. Es sind die Container Components, die die Verbindung zu Redux herstellen.

Verwendung

Redux und react-redux lokal installieren:

npm install --save redux react-redux

Wie oben beschrieben src/state/actions.js, src/state/reducers.js und src/state/store.js anlegen.

Damit die Container auf den Store zugreifen können, wrappt man die gesamte Applikation mit einem Provider - dieser stellt den Store per React Context zur Verfügung:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'

import store from 'app/state/store'
import Main from 'app/ui/main/Main'

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

Beispiel für Container:

import { connect } from 'react-redux'
import MyView from 'components/MyView'  // MyView is the presentational component to hook up

export default connect(
    // Map state to props
    (state) => {
        return {
            myProp: state.someValue
        }
    },
    // Map action creators to props (optional)
    {
        onBla: (params) => createMyAction(params),
        onBlubb: createMyAction
    }
)(MyView)

Mischform aus Presentational Component und Container ist auch möglich:

import React from 'react'
import { connect } from 'react-redux'

import MySidebar from 'component/MySidebar'

function render(props) {
    var sidebarView = null;
    if (props.sidebar == 'my-sidebar') {
        sidebarView = (<MySidebar />)
    }

    return (
        <div className="MyView">
            {sidebarView}
        </div>
    )
}

export default connect(
    // Map state to props
    (state) => {
        return {
            sidebar: state.navigation.sidebar
        }
    },
    // Map action creators to props (optional)
    {
        onBla: (params) => createMyAction(params),
        onBlubb: createMyAction
    }
)(render)