Redux in Next

Nordnet Academy

Agenda

  • Redux Global Store
  • How to disconnect Component tree structure from Store/state structure.
  • Support multiple states of same component type.
  • Redux Code splitting
  • Bonus: Reselect

Redux Global Store

Redux

  • State of the app
  • Changes to state is
    • requested by actions.
    • applied by reducers.
  • State is one big object

Redux continued

State should be

  • normalized
  • loosely coupled
    with presentation

Action (-creators)

  • Is global, needs to be unique
  • Should be as specific as possible
  • Must contain all relevant info
function toggleVisibility(id){
    return {
        type: 'MyWidget / Toggle Visibility',
        widget: id,
    };
}

Reducers

  • Mapped to only handle its local state
const DEFAULT_STATE ={};

function visibilityReducer(state = DEFAULT_STATE, action) {
    switch (action.type) {
        case 'MyWidget / Toggle Visibility':
            return {
                ...state,
                [action.widget]: !state[action.widget]
            };
        default:
            return state;
    }
}

function rootReducer = {
    // Remember the key visible here.
    visible: visibilityReducer,
};

Map*ToProps

Connects redux state with components

import connect from 'react-redux';
import toggleVisibility from './actions';
import visibilitySelector from './reducers';

const MyComponent = (props) => (
    <div>
        { props.isVisible ? <span>VISIBLE!</span> : null }
        <button onClick={props.toggle}>Toggle</button>
    </div>
);

const mapStateToProps = (state, ownProps) => ({
    // Still remember visible from before? This is it.
    isVisible: state.visible[ownProps.widgetId],
});

const mapDispatchToProps = (dispatch, ownProps) => ({
    toggle: () => dispatch(toggleVisibility(ownProps.widgetId));
});

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);


// ....
const Outer = () =>(
    <div>
        <MyComponent widgetId="a" />
        <MyComponent widgetId="b" />
    </div>
);

Selectors

  • Disconnects state and component.
  • Takes one state and finds subparts.
  • "Chain" them to go deep.
function visibilitySelector(state) {
    // Still remembering the key visible?
    return state.visible;
}

function isWidgetVisibleSelector(state, id) {
    return visibilitySelector(state)[id] || false;
}

New MapStateToProps

Disconnecting components from state.

const mapStateToProps = (state, ownProps) => ({
    // Getting rid of the visible key!
    isVisible: isWidgetVisibleSelector(state, ownProps.widgetId),
});

STATE_NAMESPACE

const STATE_NAMESPACE = 'visible'; // Only reference to the word visible.

const DEFAULT_STATE ={};

function visibilityReducer(state = DEFAULT_STATE, action) {
   // ...
}

export default function rootReducer = {
   [STATE_NAMESPACE]: visibilityReducer,
};

 // The above and below are coupled 1:1

function visibilitySelector(state) {
    return state[STATE_NAMESPACE] || DEFAULT_STATE;
}

export function isWidgetVisibleSelector(state, id) {
    return visibilitySelector(state)[id] || false;
}

Redux tip: export selectors with related reducers. They let you decouple views and action creators from state shape.

- Dan Abramov

Code splitting

Code splitting

  • Reducers must be attached to the store
  • Must use store.replaceReducer
  • ReducerRegistry handles this
// route/MyComponent/index.js

// ...
const { reducer } = require('./MyComponent');
reducerRegistry.register(reducer);
// ...

Nested Widgets

  • Nested widgets must expose reducers as a an object with a namespace.
  • Parent widgets must import and re-export the reducers upwards.
// ChildComponent/index.js

import reducerA, {
    STATE_NAMESPACE
} from './reducers';

const reducer = {
    [STATE_NAMESPACE]: reducerA
};

export reducer;
// ParentComponent/index.js

import {
    reducer as childReducer
} from 'ChildComponent';

import myReducer, {
    STATE_NAMESPACE
} from './reducers';

const reducer = Object.assign({
    [STATE_NAMESPACE]: myReducer, 
}, childReducer);

export reducer;

Not the error you are looking for!

  • We can only have 1 reducerRegistry
  • Styleguideist have weird (app) lifecycles
  • ReducerRegistry is a singleton which survives hot-reloads.
  • The store.replaceReducer still works.

Can only set the listener for a ReducerRegistry once.

The End...

... Encore, encore, encore!

What is it good for?

Absolutely nothin' ?

Reselect

const state = {
  accounts: [
      {id: 'a', totalValue: 100},
      {id: 'b', totalValue: 10},
      {id: 'c', totalValue: 50},
  ],
  selectedAccounts: [
      'a',
      'c'
  ]
}
import { createSelector } from 'reselect';

function getAllAccounts(state) {
    return state.accounts;
}

function getSelectedIds(state) {
    return state.selectedAccounts;
}

const getSelectedAccounts = createSelector(
[getAllAccounts, getSelectedIds],
(allAccounts, selectedIds) => {
    return allAccounts.filter(
        (acc) => selectedIds.includes(acc.id)
    );
});
const selected = getSelectedAccounts(state);

console.log(selected);

/*
[
    {id: 'a', totalValue: 100},
    {id: 'c', totalValue: 50},
]
*/

Reselect continued

const state = {
  accounts: [
      {id: 'a', totalValue: 100},
      {id: 'b', totalValue: 10},
      {id: 'c', totalValue: 50},
  ],
  selectedAccounts: [
      'a',
      'c'
  ]
}
import { createSelector } from 'reselect';
import { getSelectedAccounts } from 'selectors';

const getSelectedTotal = createSelector(
    [getSelectedAccounts],
    (selectedAccounts) => {
        return selectedAccounts.reduce(
            (result, acc) => result + acc.totalValue, 0)
    }
);
const grandTotal = getSelectedTotal(state);

console.log(grandTotal); // 150

Redux in Next

By nordnetacademy

Redux in Next

  • 791