Skip to content

Instantly share code, notes, and snippets.

@xvaldetaro
Last active January 28, 2017 01:01
Show Gist options
  • Save xvaldetaro/b3024f9f947aa008b65933dba61ba938 to your computer and use it in GitHub Desktop.
Save xvaldetaro/b3024f9f947aa008b65933dba61ba938 to your computer and use it in GitHub Desktop.
Proposal for a schema for redux

Schema for redux

The idea is to create a schema declaration for redux apps and generate ALL the boilerplate with flow typing. The schema will define the entire hierarchy of the store and the reducers.

What I am trying to achieve here.

  • Greatly reduce boilerplate. Only say your actions and how the state derives from them.
  • Enforce flow typing on everything. Since we have explicit type declaration for the actions, we can generate actions, action creators, reducers and selectors all with the correct types.

I am borrowing the idea from our php repo, which uses gencode heavily to great success.

The following is an artificial boilerplate of what a module's redux code would look like. We have definitions for actions, creators, reducer and selectors.

Note that the reducer handles some foreign actions that are not declared here and the selector will be used in a global selector, which will be available to the application.

foo module's redux code:
// foo/redux.js

// ------ Actions ------
const Actions = keyMirror({
  SIMPLE_ACTION: null, // e.g. SET_LOADING
  COMPLEX_ACTION: null, // SHOW_NEXT_USER_PAGE
  // ...
});

// ------ Creators ------
const FooActionCreators = {
  createSimpleAction: (simpleFlag: string) => {
    return {
      type: Actions.SIMPLE_ACTION,
      simpleFlag,
    };
  },

  createComplexAction: (shapeParam: MyShape, boolParam: boolean) => {
    return {
      type: Actions.COMPLEX_ACTION,
      shapeParam,
      boolParam,
    };
  },
};

const state = Immutable.Record(/*...*/);

const INITIAL_STATE = {
  simpleFlag: 'some specific initial state';
  complexActionField1: null,
  complexActionField2: null,
  bar: '';
  // ...
};

const _myComplexAction2Handler = (state, action) => {
  let field1, field2;
  // ... do stuff
  return state.merge({complexActionField1: field1, complexActionField2: field2});
}

// ------ Reducer ------
const fooReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case Actions.SIMPLE_ACTION: // Simple direct application reducer
      return state.set('simpleFlag', action.simpleFlag);
    case Actions.COMPLEX_ACTION: // Complex reducer, computes stuff and changes multiple fields
      return _myComplexAction2Handler(state, action);
    case 'GLOBAL_ACTION_Y': // Reduce on global actions
      return state.set('bar', action.param1.field1);
    case 'GLOBAL_ACTION_X': // Reduce on global actions
      // do whatever
    // ... more cases
  }
}

// ------ Selectors ------

// A selector that does some logic to derive stuff from multiple fields
const selectComplexActionCombinedFields = (localState) => {
  const {complexActionField1, complexActionField2} = localState;
  return myFunc(complexActionField1, complexActionField2);
}

module.exports = {
  fooReducer,
  FooActionCreators,
  selectComplexActionCombinedFields,
};
Now let's look at the relevant parts of the root redux code for this application:
// -----------------------
// rootRedux.js
const {fooReducer} = require('./foo/FooRedux.js')

const Actions = keyMirror({
  GLOBAL_ACTION_X: null,
  GLOBAL_ACTION_Y: null,
  // ...
});

const reducer = combineReducers({
  foo: FooReducer,
  // ...
});

// More complex global selector
const globalCurrentUsers = (globalState) => {
  //from our foo reducer
  const currentUserIDs = selectCurrentUserIDs(globalState.localState);

  // From another reducer
  const usersByID = globalState.usersByID;


  const currentUserListForDisplay = currentUserIDs.map(id => usersByID.get(id));
  return currentUserListForDisplay;
}

// Global Selector needs to wrap local reducer
const globalSelectsimpleFlag = (globalState) => {
  return selectsimpleFlag(globalState.localState);
}

Proposal

IMO, there is a lot of boilerplate there. Most of which, is unnecessary and could be inferred/generated, such as:

  • Reducers that just directly set a field in the state.
  • the enum that defines the actions
  • the action creators
  • the big switch
  • the selector
  • the Immutable record declaration
  • the global selectors

I think that by having a compact schema declarations such as the one below, we can generate all the parts that I mentioned above with full flow typing.

How it would work

Once you decide to use this schemagen tool in your app, it will be the only source of truth. All the code that you will import in your project will be generated by it.

It will generate some top level files with all the reducers so you can pass to createStore, as well as the global selectors and the complete list of actions.

As you will notice below, some reducers are delegated to some external file, which will not be generated. You might put all the complex reducing logic there. All simple reducers that just directly set stuff in the state will be generated.

Top level root Schema
// RootStore.schema.js
const fooStoreSchema = require('FooStore.schema.js');
// ... other sub store requires

module.exports = {
  Actions: {
    GLOBAL_ACTION_X: 'string', // change route for example
    // ... other global actions
  },
  State: {
    route: {
      type: 'string',
      on: 'GLOBAL_ACTION_X',
      direct: true, // Simply sets the field, no need to write a reducer
    },
    foo: 'fooStoreSchema',
  }
};
foo module's schema
// FooStore.schema.js
module.exports = {
  Actions: {
    SIMPLE_ACTION: {
      simpleFlag: 'string', // You can name parameters even for single param actions
    },
    COMPLEX_ACTION: {
      shapeParam: {
        type: 'MyShape', // Flow defined type
        from: './types/MyTypes.js', // Where the flow type is defined
        globalSelector: 'getFooShareField', // Name of the global selector
      },
      boolParam: 'boolean', // just say the type directly
    },
    // ... other actions
  },
  State: {
    simpleFlag: 'SIMPLE_ACTION', // Directly set the single param of an action
    complexActionField1: string, // Doesn't specify handler because it will be manual
    complexActionField2: { // Also no handler
      type: 'MyOtherShape',
      from: './types/MyTypes.js',
    },
    bar: { // Handle a global action and inline set state
      on: 'parent.GLOBAL_ACTION_Y',
      // inline function receives an object with the params of the action
      inline: (params) => params.param1.field1,
    },
    // ... Other state fields
  },
  ManualHandlers: { // You code them and pass [file, function]
    COMPLEX_ACTION: ['./handlers.js', 'handleComplexAction'],
    // Manually mutate state based on some global action
    'parent.GLOBAL_ACTION_X': ['./handlers.js', 'handleGlobalAction'],
    // ... Other manual handlers
  },
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment