Skip to content

Instantly share code, notes, and snippets.

@bstro
Last active May 21, 2018 12:45
Show Gist options
  • Save bstro/2366fc8fae5c99803f942baaef161aac to your computer and use it in GitHub Desktop.
Save bstro/2366fc8fae5c99803f942baaef161aac to your computer and use it in GitHub Desktop.

Revisions

  1. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    This is a simple pattern I follow that helps me maintain an easily accessible graph of all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in my browser's devtools.
    This is a simple pattern I follow that helps maintain an easily accessible graph of all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in the browser's console.

    ### selectorMiddleware.js
    computes selectors and sets the derived data on the window object, visible at `window.selectors`
  2. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion selectors.js
    Original file line number Diff line number Diff line change
    @@ -35,5 +35,5 @@ export const getArticle => createSelector(
    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }), // https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    ((articles, match) => articles[match.params.articleId]) // assuming I don't need to do any null checks for this example
    ((articles, match) => articles[match.params.articleId]) // leaving out null checks for brevity/clarity in this example.
    );
  3. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion selectorMiddleware.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    import selectors as * from '/selectors.js';
    import * as selectors from '/selectors.js';

    const selectorMiddleware = ({ getState }) => next => action => {
    const ret = next(action);
  4. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 11 additions and 6 deletions.
    17 changes: 11 additions & 6 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -8,23 +8,28 @@ export getProps = (_, props) => props;
    // me there's an explicit coupling introduced by using component props in a selector.

    export const getEntities => createSelector(getState, fp.get('entities'));

    export const getArticles => createSelector(getEntities, fp.get('articles'));

    export const getArticleIdFromProps => createSelector(getProps, fp.get('activeArticleId'));
    // ^ I also like to explicity indicate (in the selector name) when
    // a select is reliant on a prop in order to expose that coupling.
    // a select is reliant on a prop in order to expose that coupling
    // whenever a prop-based selector is composed with another.

    export const getArticle => createSelector(
    getArticleIdFromProps,
    getArticles,
    fp.get // lodash/fp makes point-free function application particularly easy here.
    );

    /*
    These selectors then get exposed on window.selectors in debug mode.
    The above selectors then get exposed on window.selectors in debug mode.
    Prop-based selectors are ignored, thus window.selectors.getEntities &
    window.selectors.getArticles will be computed, but getArticle and
    getArticleIdFromProps will not.
    window.selectors.getArticles will be computed and exposed on window.selectors,
    but getArticle and getArticleIdFromProps will not.
    A potential solution that I often use is to store route params in
    Redux, so we could compute `activeArticle` using a selector provided by react-router-redux:
    A potential solution that I often use is to store route params in Redux, so we
    could compute `activeArticle` using a selector provided by react-router-redux:
    */

    export const getActiveArticle = createSelector(
  5. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -30,7 +30,5 @@ export const getArticle => createSelector(
    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }), // https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    ((articles, match) => ({
    activeArticleId: articles[match.params.articleId] // assuming I don't need to do any null checks for this example…
    })
    ((articles, match) => articles[match.params.articleId]) // assuming I don't need to do any null checks for this example…
    );
  6. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,7 @@ This is a simple pattern I follow that helps me maintain an easily accessible gr
    computes selectors and sets the derived data on the window object, visible at `window.selectors`

    ### stateMiddleware.js
    simply exposes the redux state to the window object. nothing crazy here
    simply exposes the redux state to the window object at `window.state`. nothing crazy here

    ### configureStore.js
    apply the two middlewares
  7. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 2 additions and 4 deletions.
    6 changes: 2 additions & 4 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -29,10 +29,8 @@ export const getArticle => createSelector(

    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }),
    // ^ https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    createMatchSelector({ path: '/articles/:articleId' }), // https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    ((articles, match) => ({
    activeArticleId: articles[match.params.articleId]
    // ^ assuming I don't need to do any null checks for this example…
    activeArticleId: articles[match.params.articleId] // assuming I don't need to do any null checks for this example…
    })
    );
  8. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 11 additions and 11 deletions.
    22 changes: 11 additions & 11 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -25,14 +25,14 @@ export const getArticle => createSelector(
    A potential solution that I often use is to store route params in
    Redux, so we could compute `activeArticle` using a selector provided by react-router-redux:
    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }),
    // ^ https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    ((articles, match) => ({
    activeArticleId: articles[match.params.articleId]
    // ^ assuming I don't need to do any null checks for this example…
    })
    );
    */
    */

    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }),
    // ^ https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    ((articles, match) => ({
    activeArticleId: articles[match.params.articleId]
    // ^ assuming I don't need to do any null checks for this example…
    })
    );
  9. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion selectors.js
    Original file line number Diff line number Diff line change
    @@ -24,7 +24,7 @@ export const getArticle => createSelector(
    getArticleIdFromProps will not.
    A potential solution that I often use is to store route params in
    Redux, so we could compute `activeArticle` using a selector written by react-router-redux:
    Redux, so we could compute `activeArticle` using a selector provided by react-router-redux:
    export const getActiveArticle = createSelector(
    getArticles,
  10. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -20,8 +20,10 @@ export const getArticle => createSelector(
    /*
    These selectors then get exposed on window.selectors in debug mode.
    Prop-based selectors are ignored, thus window.selectors.getEntities &
    window.selectors.getArticles will be computed, but getArticle will not.
    A potential solution that i've used often is to store route params in
    window.selectors.getArticles will be computed, but getArticle and
    getArticleIdFromProps will not.
    A potential solution that I often use is to store route params in
    Redux, so we could compute `activeArticle` using a selector written by react-router-redux:
    export const getActiveArticle = createSelector(
  11. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion selectors.js
    Original file line number Diff line number Diff line change
    @@ -13,7 +13,7 @@ export const getArticleIdFromProps => createSelector(getProps, fp.get('activeArt
    // ^ I also like to explicity indicate (in the selector name) when
    // a select is reliant on a prop in order to expose that coupling.
    export const getArticle => createSelector(
    getActiveArticleId,
    getArticleIdFromProps,
    getArticles,
    fp.get // lodash/fp makes point-free function application particularly easy here.
    );
  12. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    This is a simple pattern I follow that helps me maintain a graph of (potentially) all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in my browser's devtools.
    This is a simple pattern I follow that helps me maintain an easily accessible graph of all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in my browser's devtools.

    ### selectorMiddleware.js
    computes selectors and sets the derived data on the window object, visible at `window.selectors`
  13. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    This is a simple pattern I follow that allows a me user to maintain a graph of (potentially) all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in my browser's devtools.
    This is a simple pattern I follow that helps me maintain a graph of (potentially) all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in my browser's devtools.

    ### selectorMiddleware.js
    computes selectors and sets the derived data on the window object, visible at `window.selectors`
  14. Brendan Stromberger revised this gist May 21, 2018. 2 changed files with 1 addition and 24 deletions.
    24 changes: 0 additions & 24 deletions createMatchSelector.js
    Original file line number Diff line number Diff line change
    @@ -1,24 +0,0 @@
    // this selector is referenced in selectors.js, I've copied and pasted it here verbatin from the react-router-redux repo.
    // https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js

    import { matchPath } from "react-router";

    export const getLocation = state => state.router.location;
    export const getAction = state => state.router.action;

    export const createMatchSelector = path => {
    let lastPathname = null;
    let lastMatch = null;
    return state => {
    const { pathname } = getLocation(state) || {};
    if (pathname === lastPathname) {
    return lastMatch;
    }
    lastPathname = pathname;
    const match = matchPath(pathname, path);
    if (!match || !lastMatch || match.url !== lastMatch.url) {
    lastMatch = match;
    }
    return lastMatch;
    };
    };
    1 change: 1 addition & 0 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -27,6 +27,7 @@ export const getArticle => createSelector(
    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }),
    // ^ https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js
    ((articles, match) => ({
    activeArticleId: articles[match.params.articleId]
    // ^ assuming I don't need to do any null checks for this example…
  15. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 24 additions and 0 deletions.
    24 changes: 24 additions & 0 deletions createMatchSelector.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,24 @@
    // this selector is referenced in selectors.js, I've copied and pasted it here verbatin from the react-router-redux repo.
    // https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/modules/selectors.js

    import { matchPath } from "react-router";

    export const getLocation = state => state.router.location;
    export const getAction = state => state.router.action;

    export const createMatchSelector = path => {
    let lastPathname = null;
    let lastMatch = null;
    return state => {
    const { pathname } = getLocation(state) || {};
    if (pathname === lastPathname) {
    return lastMatch;
    }
    lastPathname = pathname;
    const match = matchPath(pathname, path);
    if (!match || !lastMatch || match.url !== lastMatch.url) {
    lastMatch = match;
    }
    return lastMatch;
    };
    };
  16. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion selectors.js
    Original file line number Diff line number Diff line change
    @@ -13,8 +13,9 @@ export const getArticleIdFromProps => createSelector(getProps, fp.get('activeArt
    // ^ I also like to explicity indicate (in the selector name) when
    // a select is reliant on a prop in order to expose that coupling.
    export const getArticle => createSelector(
    getActiveArticleId,
    getArticles,
    getActiveArticleId
    fp.get // lodash/fp makes point-free function application particularly easy here.
    );
    /*
    These selectors then get exposed on window.selectors in debug mode.
  17. Brendan Stromberger revised this gist May 21, 2018. No changes.
  18. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 13 additions and 1 deletion.
    14 changes: 13 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1,13 @@
    This is a simple technique that allows a Redux user to maintain a graph of (potentially) all derived data flowing through a Redux application.
    This is a simple pattern I follow that allows a me user to maintain a graph of (potentially) all derived data flowing through a Redux application, and expose that derived data (as well as the Redux state itself) to the global window object in my browser's devtools.

    ### selectorMiddleware.js
    computes selectors and sets the derived data on the window object, visible at `window.selectors`

    ### stateMiddleware.js
    simply exposes the redux state to the window object. nothing crazy here

    ### configureStore.js
    apply the two middlewares

    ### selectors.js
    an example of how I write selectors.
  19. Brendan Stromberger revised this gist May 21, 2018. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion stateMiddleware.js
    Original file line number Diff line number Diff line change
    @@ -3,5 +3,4 @@ export default ({ getState }) => next => action => {
    if (process.env.NODE_ENV !== 'production') {
    window.state = getState();
    }
    return ret;
    };
  20. Brendan Stromberger revised this gist May 21, 2018. 3 changed files with 10 additions and 1 deletion.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    This is a simple technique that allows a Redux user to maintain a graph of (potentially) all derived data flowing through a Redux application.
    3 changes: 2 additions & 1 deletion configureStore.js
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,9 @@
    import selectorMiddleware from './selectorMiddleware.js';
    import stateMiddleware from './stateMiddleware.js';
    export default createStore(
    combineReducers({}),
    applyMiddleware(selectorMiddleware, logger, )
    applyMiddleware(selectorMiddleware, stateMiddleware, )
    );

    7 changes: 7 additions & 0 deletions stateMiddleware.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    export default ({ getState }) => next => action => {
    const ret = next(action);
    if (process.env.NODE_ENV !== 'production') {
    window.state = getState();
    }
    return ret;
    };
  21. Brendan Stromberger revised this gist May 21, 2018. 2 changed files with 34 additions and 3 deletions.
    13 changes: 11 additions & 2 deletions selectorMiddleware.js
    Original file line number Diff line number Diff line change
    @@ -2,16 +2,25 @@ import selectors as * from '/selectors.js';

    const selectorMiddleware = ({ getState }) => next => action => {
    const ret = next(action);

    // Don't run this code in prod.
    if (process.env.NODE_ENV !== 'production') {
    const selectorKeys = Object.keys(selectors);
    window.selectors = {};

    // Iterate through all the selectors and compute their state.
    for (let key of selectorKeys) {
    const selector = selectors[key];

    if (!fp.isFunction(selector)) continue;

    const value = selector(getState());
    if (fp.isNil(value)) continue;

    // This is a dumb way of filtering out prop-based selectors
    // (we can't compute them as they are coupled to the lifecycle of a component instance).
    if (fp.isNil(value)) continue;

    window.selectors[key] = value;
    }
    }
    return ret;
    };
    24 changes: 23 additions & 1 deletion selectors.js
    Original file line number Diff line number Diff line change
    @@ -9,4 +9,26 @@ export getProps = (_, props) => props;

    export const getEntities => createSelector(getState, fp.get('entities'));
    export const getArticles => createSelector(getEntities, fp.get('articles'));
    export const getArticleIdFromProps => createSelector(getProps, fp.get('activeArticleId'));
    // ^ I also like to explicity indicate (in the selector name) when
    // a select is reliant on a prop in order to expose that coupling.
    export const getArticle => createSelector(
    getArticles,
    getActiveArticleId
    );
    /*
    These selectors then get exposed on window.selectors in debug mode.
    Prop-based selectors are ignored, thus window.selectors.getEntities &
    window.selectors.getArticles will be computed, but getArticle will not.
    A potential solution that i've used often is to store route params in
    Redux, so we could compute `activeArticle` using a selector written by react-router-redux:
    export const getActiveArticle = createSelector(
    getArticles,
    createMatchSelector({ path: '/articles/:articleId' }),
    ((articles, match) => ({
    activeArticleId: articles[match.params.articleId]
    // ^ assuming I don't need to do any null checks for this example…
    })
    );
    */
  22. Brendan Stromberger created this gist May 20, 2018.
    8 changes: 8 additions & 0 deletions configureStore.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    import selectorMiddleware from './selectorMiddleware.js';
    export default createStore(
    combineReducers({}),
    applyMiddleware(selectorMiddleware, logger, )
    );

    17 changes: 17 additions & 0 deletions selectorMiddleware.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    import selectors as * from '/selectors.js';

    const selectorMiddleware = ({ getState }) => next => action => {
    const ret = next(action);
    if (process.env.NODE_ENV !== 'production') {
    const selectorKeys = Object.keys(selectors);
    window.selectors = {};
    for (let key of selectorKeys) {
    const selector = selectors[key];
    if (!fp.isFunction(selector)) continue;
    const value = selector(getState());
    if (fp.isNil(value)) continue;
    window.selectors[key] = value;
    }
    }
    return ret;
    };
    12 changes: 12 additions & 0 deletions selectors.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    import createSelector from 'reselect';
    import fp from 'lodash/fp';

    export getState = state => state;

    export getProps = (_, props) => props;
    // ^ just including this to mention I like to make prop access explicit, as it reminds
    // me there's an explicit coupling introduced by using component props in a selector.

    export const getEntities => createSelector(getState, fp.get('entities'));
    export const getArticles => createSelector(getEntities, fp.get('articles'));