React - Redux


https://github.com/reactjs/redux:

  • The whole state of your app is stored in an object tree inside a single store.
  • The only way to change the state tree is to emit an action, an object describing what happened.
  • To specify how the actions transform the state tree, you write pure reducers.

store

actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

{
  type: SET_COUNT,
  count: 5,
}

Action creators are functions that create actions.

function setCount (count) {
  return {
    type: SET_COUNT,
    count,
  };
}

reducers

The reducer is a pure function that takes the previous state and an action, and returns the next state.

(previousState, action) => newState

Things you should never do inside a reducer:

reducer composition

http://redux.js.org/docs/basics/Reducers.html#splitting-reducers

reducer composition (fundamental pattern of Redux) - delegating a slice of state to manage to child reducers.

NOTE: it’s possible to nest child reducers! say, child reducer to set user state field might call another child reducer to set user’s name.

every reducer on any level receives part of state it manages on this level and current action and should return the same part of state with merged changes.

without reducer composition:

with reducer composition:

classic style (without reducer composition):

const initialState = {
  count: 0,
};

export default function badges (state = initialState, action = {}) {
  switch (action.type) {
  case SET_COUNT:
    return {
      ...state,
      count: action.count,
    };
  default:
    return state;
  }
}

with reducer composition (but without using combineReducers):

function count (state = 0, action) {
  switch (action.type) {
  case SET_COUNT:
    return action.count;
  default:
    return state;
  }
}

export default function badges (state, action) {
  return {count: count(state.count, action)};
}

with reducer composition (using combineReducers):

import {combineReducers} from 'redux';

function count (state = 0, action) {
  switch (action.type) {
  case SET_COUNT:
    return action.count;
  default:
    return state;
  }
}

const badges = combineReducers({count});

export default badges;

react-redux

  1. https://github.com/reactjs/react-redux/blob/master/docs/api.md
  2. http://redux.js.org/docs/faq/ReactRedux.html
  3. http://rants.broonix.ca/getting-started-with-react-native-and-redux/
  4. http://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/
  5. https://goshakkk.name/redux-antipattern-mapstatetoprops/
  6. https://stackoverflow.com/a/40068198/3632318

using react-redux boils down to using just 2 things:

tips

don’t share state between child reducers

  1. https://stackoverflow.com/questions/35375810

say, you want to calculate total count and save it as a state field in store.

One of the golden rules of Redux is that you should try to avoid putting data into state if it can be calculated from another state, as it increases likelihood of getting data that is out-of-sync.

don’t dispatch in reducer

  1. https://stackoverflow.com/questions/36730793/dispatch-action-in-reducer

Dispatching an action within a reducer is an anti-pattern. Reducer should be without side effects simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.

don’t pass store to presentational components

  1. https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
  2. https://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components

only container components are aware of store and provide data from store to presentational and other container components.

that is why only container components should be connected to Redux store.

initialize state in reducers with ES6 default arguments

http://redux.js.org/docs/recipes/reducers/InitializingState.html#single-simple-reducer:

When Redux initializes it dispatches a “dummy” action to fill the state. This is exactly the case that “activates” the default argument.

function isModalVisible (state = false, action) {
  switch (action.type) {
  case SHOW_MODAL:
    return true;
  case HIDE_MODAL:
    return false;
  default:
    return state;
  }
}

export default combineReducers({isModalVisible});

or when not using reducer composition:

const initialState = {
  isModalVisible: false,
}

export default function memberships (state = initialState, action = {}) {
  switch (action) {
  case SHOW_MODAL:
    return {...state, isModalVisible: true};
  case HIDE_MODAL:
    return {...state, isModalVisible: false};
  default:
    return state;
  }
}

store object keyed by ID instead of array

  1. http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html
  2. https://medium.com/dailyjs/rewriting-javascript-converting-an-array-of-objects-to-an-object-ec579cafbfc7
// [{id: 2, name: 'foo'}, {id: 2, name: 'bar'}] ->
//  {2: {id: 1, name: 'foo'}, 2: {id: 2, name: 'bar'}}
const arrayToObject = (array) =>
  array.reduce((obj, item) => {
    obj[item.id] = item;
    return obj;
  }, {})

NOTE: original array sorting is lost in resulting object!

if you need to keep sorting opt for Map instead:

const arrayToMap = (array) =>
  array.reduce((map, item) => {
    map.set(item.id, item);
    return map;
  }, new Map());

also it might be more convenient to store both object keyed by ID (byId store key) and array of all items (all store key).

UPDATE

try to store object keyed by ID (byId) only - maintaining array of all items (all) is complex and error-prone (say, adding item with existing ID to byId just overwrites existing item while adding that item to all would result in 2 items with the same ID to coexist - unless this is explicitly checked).

normalize state

  1. https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

Note that a normalized state structure generally implies that more components are connected and each component is responsible for looking up its own data, as opposed to a few connected components looking up large amounts of data and passing all that data downwards.

As it turns out, having connected parent components simply pass item IDs to connected children is a good pattern for optimizing UI performance in a React Redux application.

don’t store refreshing flag in Redux store

this causes flickering of RefreshControl component during animation - store it in component state instead.

style guide

thunk actions

// pass `params` object when updating the whole model
export const requestUpdateGame = (id, params) => (
  (dispatch, getState, api) => {
    // ...
    return api.updateGame(token, id, params)
    // ...
  }
)

// pass specific attribute when updating that attribute
export const requestUpdateGameKind = (id, kind) => (
  (dispatch, getState, api) => {
    // ...
    return api.updateGame(token, id, {kind})
    // ...
  }
)

middleware

Redux Thunk

https://github.com/gaearon/redux-thunk#composition:

A thunk is a function that returns a function.

https://github.com/reactjs/redux/issues/1676#issuecomment-215413478

The return value of dispatch() when you dispatch a thunk is the return value of the inner function. This is why it’s useful to return a Promise (even though it is not strictly necessary)

that is dispatching a thunk returns whatever thunk itself returns - not necessarily Promise object (even though it’s highly recommended).

https://stackoverflow.com/a/35415559/3632318 (Dan Abramov):

This was the motivation for finding a way to “legitimize” this pattern of providing dispatch to a helper function, and help Redux “see” such asynchronous action creators as a special case of normal action creators rather than totally different functions.

handling rejected promises in thunks

in thunk:

export const requestPlayers = (teamId) => (
  (dispatch, getState, api) => {
    dispatch(startLoading());

    const {token} = getState().user.credentials;

    return api.getPlayers(token, teamId)
      .then(data => {
        dispatch(set(data.players));
        return data.players;
      })
      .catch(e => {
        dispatch(finishLoading());
        Log.info(e.message);
        throw e;
      });
  }
);

don’t forget to re-throw error in catch method body to return rejected promise.

in component:

// notify user about error
this.props.store
  .dispatch(teamsActions.requestPlayers(this.props.team.id))
  .catch(_e => AlertHelpers.serverError());

// or else silence error
this.props.store
  .dispatch(teamsActions.requestPlayers(this.props.team.id))
  .catch(_e => {});

in any case it’s required to add catch method call in component in order to avoid warning about unhandled promise rejection.

debugging

see React Native - Debugging.

troubleshooting

component is not re-rendered when it’s connected

see the section above about updating component when using react-redux.

in my case connected component GamerCheckedRow is passed gamer and callback to calculate if gamer is checked or not. when gamer is clicked, selected_user_ids state property of parent component is updated inside passed callback - not the gamer himself. but selected_user_ids state property is not passed as a property of GamerCheckedRow component so React thinks that props of GamerCheckedRow have not changed and doesn’t re-render it (it would re-render if, say, forceUpdate() would be called).

solution

there are 2 ways to solve this problem:

functions of connected component are not available from outside

  1. https://github.com/reactjs/react-redux/issues/475

say, when connected component is obtained via its ref property.

solution

connected component:

@connect(mapStateToProps, null, null, {withRef: true})
export default class MyComponent extends Component {
  // ...
}

calling its function from outside:

this.myComponent.getWrappedInstance().tryScrollToGame(games[0]);