Should I store function references in Redux store?

2019-03-17 03:50发布

问题:

I'm trying to build keyboard shortcut support into my React/Redux app in an idiomatic React/Redux way. The way I am planning to do this is to have the following action creator and associated action:

registerShortcut(keyCode, actionCreatorFuncReference)

The reducer would then update a registeredShortcuts object in the redux store with a mapping of keyCodes to actionCreatorFuncReferences. Then my root component would listen for keyup and see if there is an associated keyCode registered and if so, then dispatch the mapped action via the action creator function reference.

However, this would be the first time I am storing function references in my Redux store. To date I've only had objects with keys with vanilla values (strings, ints, etc).

The Redux docs says "You should do your best to keep the state serializable. Don’t put anything inside it that you can’t easily turn into JSON.". Does this suggest it's a bad idea to store such function references in my Redux store? If so, what is a better way to accomplish what I'm trying to do in React/Redux?

An alternative approach is just to store the mapping of keyCodes and function references in the root react component itself, but that didn't feel very Redux-like since now application state is not in the Redux store.

回答1:

No, you should not store function references in the redux store. They are not serializable, and as you mentioned state should be serializable at all time. The most redux friendly approach I can think of is just to keep the map of hotkeys to their actionCreatorFuncNames.



回答2:

TL;DR: You don't. The store state must be serializable at all times (as Nathan answered). The Redux way is via enhancers, or the Redux-Observable way via dependencies.

Based on the Redux docs example, what you want is to pass the reference in your action(1), ignore it your reducer(2) and use it in your enhancer(3):

//... in your action:
const data={val:1},ref=()=>{};
const action = {type:'ACTION_WITH_REF', data:data:, ref: ref}; //(1)

//... in your reducer:
case 'ACTION_WITH_REF':
return {...state, data: action.data}

//... and in your enhancer:
import { createStore, applyMiddleware } from 'redux'
import reducers from './reducers'
export const myRefStore= {};
 
function refHandler({ getState }) {
  return next => action => {
    switch(action.type){ // this can be done more elegantly with redux-observable
     case 'ACTION_WITH_REF':
     myRefStore.aRef = action.ref // (3)
     break;
 }
    const returnValue = next(action)   
    return returnValue
  }
} 
const store = createStore(
  reducers,
  initialState,
  applyMiddleware(refHandler)
)

Note: As far as there are no side-effects in your enhancers, you are good to go. Be aware that you could have obtained the refs directly in the reducers, but such approach keeps the reference at the reducer-level and misses the point of combineReducers(). With an enhancer, you keep them all in one place(myRefStore). One final observation is that a redux store is not an any-data store but a state store, thus why we need to handle functions and other non-state related stuff in enhancers. You can leverage the enhancer backbone to Redux-Observable and inject myRefStore via dependencies.



回答3:

I'm new to redux, but the way I see it, you could pass the key code and an action type. Then a reducer could be listening for that action type and make changes accordingly.

Here is an example using the library Mousetrap:

// On your Container
function registerShortcut(element, dispatch, keyCode, actionType) {
  Mousetrap(element).bind(keyCode, function(e) {
    dispatch({
      type: actionType,
      payload: {
        keyCode: keyCode,
        event: e
      }
    });
  });
});

mapDispatchToProps = function(dispatch) {
  return {
    onMount: function(element) {
      registerShortcut(element, dispatch, ['command+f', 'ctrl+f'], 'OPEN_SEARCH');
    },
    onUnmount: function(element) {
      Mousetrap(element).unbind(['command+f', 'ctrl+f']);
    }
  };
};


// On your Component
componentDidMount() {
  onMount(ReactDOM.findDOMNode(this));
};

componentWillUnmount() {
  onUnmount(ReactDOM.findDOMNode(this));
};



// On your reducer
function reducer(oldState, action)  {
  if (action.type == 'OPEN_SEARCH') {
    //... make changes ...//
    return newState;
  }
  return oldState;
};

This way, keyboard shortcuts will dispatch an action. The reducer will make the changes necessary to the state. And finally, the application can re-render.



回答4:

I've had some really good results adding functions to my redux store. If your functions act as methods that apply logic to the data, it can be incredibly powerful throughout your application (plus JSON.stringify will strip out the functions anyhow so you can still serialize your store and save it to local storage or whatever.) Below I have a currentUser reducer that uses a class as it's initial state, and includes a hasPermissions method that can be called anywhere the currentUser is connected. This is especially nice because whenever I map currentUser to my props, I don't need to remember to import some kind of util that will parse the user to see if it has permissions to view something. The method is already on the user.

  class CurrentUser {
    constructor() {
      this.permissions = []
      this.roles = []
      this.admin = false
    }

    hasPermission = (permission) => {
      if (this.admin) { return true }
      if (Array.isArray(permission)) {
        return permission.reduce((prev, curr) => {
          if (!prev) { return false }
          if (this.permissions.indexOf(curr) > -1) { return true }
          return false
        }, true)
      }
      return this.permissions.indexOf(permission) > -1
    }
  }

  const initialState = new CurrentUser()

  const curretUserReducer = (state = initialState, action) => {
    const { type, payload } = action
    switch (type) {
      case ACTIONS.SET_CURRENT_USER: {
        const { user } = payload
        return Object.assign(new CurrentUser(), user)
      }
      case ACTIONS.CLEAR_CURRENT_USER: {
        return new CurrentUser()
      }
      case ACTIONS.UPDATE_CURRENT_USER: {
        const newCurrentUser = merge(state, payload.user)
        storage.set('currentUser', JSON.stringify(newCurrentUser))
        return newCurrentUser
      }
      default:
        return state
    }
  }

  export default curretUserReducer


标签: redux