├── .gitignore ├── demos └── scoped-redux │ ├── src │ ├── router │ │ ├── index.js │ │ └── Router.js │ ├── shared │ │ ├── components │ │ │ ├── List │ │ │ │ ├── index.js │ │ │ │ └── List.js │ │ │ ├── Repo │ │ │ │ ├── index.js │ │ │ │ └── Repo.js │ │ │ ├── Profile │ │ │ │ ├── index.js │ │ │ │ └── Profile.js │ │ │ └── DevTools │ │ │ │ └── index.js │ │ ├── constants.js │ │ └── api │ │ │ └── index.js │ ├── features │ │ ├── repo │ │ │ ├── selectors.js │ │ │ ├── index.js │ │ │ ├── renderers │ │ │ │ ├── Empty.js │ │ │ │ └── Loading.js │ │ │ ├── actionTypes.js │ │ │ ├── actionCreators.js │ │ │ ├── reducer.js │ │ │ └── containers │ │ │ │ └── Repo.js │ │ ├── search │ │ │ ├── index.js │ │ │ └── renderers │ │ │ │ └── Search.js │ │ ├── error │ │ │ ├── selectors.js │ │ │ ├── actionTypes.js │ │ │ ├── index.js │ │ │ ├── actionCreators.js │ │ │ ├── reducer.js │ │ │ ├── containers │ │ │ │ └── Error.js │ │ │ └── renderers │ │ │ │ └── Error.js │ │ ├── profile │ │ │ ├── selectors.js │ │ │ ├── index.js │ │ │ ├── renderers │ │ │ │ ├── Title.js │ │ │ │ ├── Empty.js │ │ │ │ └── Loading.js │ │ │ ├── actionTypes.js │ │ │ ├── actionCreators.js │ │ │ ├── reducer.js │ │ │ └── containers │ │ │ │ └── Profile.js │ │ ├── stargazers │ │ │ ├── selectors.js │ │ │ ├── index.js │ │ │ ├── renderers │ │ │ │ └── Title.js │ │ │ ├── actionTypes.js │ │ │ ├── actionCreators.js │ │ │ ├── reducer.js │ │ │ └── containers │ │ │ │ └── Stargazers.js │ │ └── starredRepos │ │ │ ├── selectors.js │ │ │ ├── index.js │ │ │ ├── renderers │ │ │ └── Title.js │ │ │ ├── actionTypes.js │ │ │ ├── actionCreators.js │ │ │ ├── reducer.js │ │ │ └── containers │ │ │ └── StarredRepos.js │ ├── pages │ │ ├── repo │ │ │ ├── index.js │ │ │ ├── route.js │ │ │ └── containers │ │ │ │ └── RepoPage.js │ │ ├── root │ │ │ ├── index.js │ │ │ ├── route.js │ │ │ └── containers │ │ │ │ └── RootPage.js │ │ └── user │ │ │ ├── index.js │ │ │ ├── route.js │ │ │ └── containers │ │ │ └── UserPage.js │ ├── index.js │ └── store │ │ └── index.js │ ├── .gitattributes │ ├── readme.md │ ├── .gitignore │ ├── .prettierrc │ ├── public │ └── index.html │ └── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/router/index.js: -------------------------------------------------------------------------------- 1 | export { default as Router } from './Router' 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/List/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './List' 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/Repo/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Repo' 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/Profile/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Profile' 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectRepo = state => state.features.repo 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/search/index.js: -------------------------------------------------------------------------------- 1 | export { default as Search } from './renderers/Search' 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectError = state => state.features.error 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectProfile = state => state.features.profile 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/constants.js: -------------------------------------------------------------------------------- 1 | export const LOADING_STATES = ['initial', 'loading', 'loaded', 'error'] 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectStargazers = state => state.features.stargazers 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectStarredRepos = state => state.features.starredRepos 2 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const DISMISS = 'feature/error/DISMISS' 2 | export const HANDLE = 'feature/error/HANDLE' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/index.js: -------------------------------------------------------------------------------- 1 | export { default as Repo } from './containers/Repo' 2 | export { default as reducer } from './reducer' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/repo/index.js: -------------------------------------------------------------------------------- 1 | export { default as RepoPage } from './containers/RepoPage' 2 | export { default as route } from './route' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/root/index.js: -------------------------------------------------------------------------------- 1 | export { default as RootPage } from './containers/RootPage' 2 | export { default as route } from './route' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/user/index.js: -------------------------------------------------------------------------------- 1 | export { default as UserPage } from './containers/UserPage' 2 | export { default as route } from './route' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/index.js: -------------------------------------------------------------------------------- 1 | export { default as Profile } from './containers/Profile' 2 | export { default as reducer } from './reducer' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/renderers/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Title = () =>

User

4 | 5 | export default Title 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/index.js: -------------------------------------------------------------------------------- 1 | export { default as Stargazers } from './containers/Stargazers' 2 | export { default as reducer } from './reducer' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/renderers/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Title = () =>

Stargazers

4 | 5 | export default Title 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/index.js: -------------------------------------------------------------------------------- 1 | export { default as StarredRepos } from './containers/StarredRepos' 2 | export { default as reducer } from './reducer' 3 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/renderers/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Title = () =>

Starred repositories

4 | 5 | export default Title 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feature-driven-architecture 2 | Defining boundaries for a large application. 3 | 4 | I have restarted this effort under a separte org https://github.com/feature-driven-architecture/spec 5 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/index.js: -------------------------------------------------------------------------------- 1 | export { default as Error } from './containers/Error' 2 | export { default as reducer } from './reducer' 3 | export { handle, dismiss } from './actionCreators' 4 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/renderers/Empty.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Empty = () => ( 4 |

5 | Repo empty state. 6 |

7 | ) 8 | 9 | export default Empty 10 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/renderers/Empty.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Empty = () => ( 4 |

5 | Profile empty state. 6 |

7 | ) 8 | 9 | export default Empty 10 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD = 'features/repo/LOAD' 2 | export const HANDLE_RESPONSE = 'features/repo/HANDLE_RESPONSE' 3 | export const HANDLE_ERROR = 'features/repo/HANDLE_ERROR' 4 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/root/route.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RootPage from './containers/RootPage' 3 | import { Route } from 'react-router-dom' 4 | 5 | export default 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/readme.md: -------------------------------------------------------------------------------- 1 | ## Scoped Redux 2 | 3 | You can clone and run `npm i && npm start` or open it directly in [codesandbox](https://codesandbox.io/s/github/kof/feature-driven-architecture/tree/master/demos/scoped-redux). 4 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD = 'features/profile/LOAD' 2 | export const HANDLE_RESPONSE = 'features/profile/HANDLE_RESPONSE' 3 | export const HANDLE_ERROR = 'features/profile/HANDLE_ERROR' 4 | -------------------------------------------------------------------------------- /demos/scoped-redux/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/user/route.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import UserPage from './containers/UserPage' 3 | import { Route } from 'react-router-dom' 4 | 5 | export default 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/repo/route.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RepoPage from './containers/RepoPage' 3 | import { Route } from 'react-router-dom' 4 | 5 | export default 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "jsxBracketSameLine": false, 4 | "printWidth": 80, 5 | "semi": false, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "es5", 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { DISMISS, HANDLE } from './actionTypes' 2 | 3 | export const dismiss = () => dispatch => { 4 | dispatch({ type: DISMISS }) 5 | } 6 | 7 | export const handle = error => dispatch => { 8 | dispatch({ type: HANDLE, payload: error }) 9 | } 10 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD = 'features/stargazers/LOAD' 2 | export const LOAD_NEXT = 'features/stargazers/LOAD_NEXT' 3 | export const HANDLE_RESPONSE = 'features/stargazers/HANDLE_RESPONSE' 4 | export const HANDLE_NEXT_RESPONSE = 'features/stargazers/HANDLE_NEXT_RESPONSE' 5 | export const HANDLE_ERROR = 'features/stargazers/HANDLE_ERROR' 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD = 'features/starredRepos/LOAD' 2 | export const LOAD_NEXT = 'features/starredRepos/LOAD_NEXT' 3 | export const HANDLE_RESPONSE = 'features/starredRepos/HANDLE_RESPONSE' 4 | export const HANDLE_NEXT_RESPONSE = 'features/starredRepos/HANDLE_NEXT_RESPONSE' 5 | export const HANDLE_ERROR = 'features/starredRepos/HANDLE_ERROR' 6 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/renderers/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const propTypes = { 5 | login: PropTypes.string.isRequired, 6 | } 7 | 8 | const Loading = ({ login }) => ( 9 |

10 | {`Loading ${login}'s profile...`} 11 |

12 | ) 13 | 14 | Loading.propTypes = propTypes 15 | 16 | export default Loading 17 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/renderers/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const propTypes = { 5 | fullName: PropTypes.string.isRequired, 6 | } 7 | 8 | const Loading = ({ fullName }) => ( 9 |

10 | {`Loading ${fullName} details...`} 11 |

12 | ) 13 | 14 | Loading.propTypes = propTypes 15 | 16 | export default Loading 17 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/reducer.js: -------------------------------------------------------------------------------- 1 | import { DISMISS, HANDLE } from './actionTypes' 2 | 3 | const defaultState = {} 4 | 5 | const states = { 6 | [DISMISS]: () => defaultState, 7 | [HANDLE]: (state, payload) => ({ 8 | message: payload.message, 9 | }), 10 | } 11 | 12 | export default (state = defaultState, { type, payload } = {}) => 13 | states[type] ? states[type](state, payload) : state 14 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/DevTools/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/router/Router.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { BrowserRouter } from 'react-router-dom' 3 | import { route as rootRoute } from '../pages/root' 4 | import { route as repoRoute } from '../pages/repo' 5 | import { route as userRoute } from '../pages/user' 6 | 7 | const Router = () => ( 8 | 9 | 10 | {rootRoute} 11 | {userRoute} 12 | {repoRoute} 13 | 14 | 15 | ) 16 | 17 | export default Router 18 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { Router } from './router' 5 | import { configure as configureStore } from './store' 6 | import DevTools from './shared/components/DevTools' 7 | 8 | const store = configureStore() 9 | 10 | render( 11 | 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ) 19 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../shared/api' 2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes' 3 | 4 | export const load = ({ login }) => dispatch => { 5 | dispatch({ type: LOAD, payload: login }) 6 | 7 | return api 8 | .call(`users/${login}`) 9 | .then(({ result }) => { 10 | dispatch({ type: HANDLE_RESPONSE, payload: result }) 11 | }) 12 | .catch(error => { 13 | dispatch({ type: HANDLE_ERROR, payload: error }) 14 | throw error 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../shared/api' 2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes' 3 | 4 | export const load = ({ fullName }) => dispatch => { 5 | dispatch({ type: LOAD, payload: fullName }) 6 | 7 | return api 8 | .call(`repos/${fullName}`) 9 | .then(({ result }) => { 10 | dispatch({ type: HANDLE_RESPONSE, payload: result }) 11 | }) 12 | .catch(error => { 13 | dispatch({ type: HANDLE_ERROR, payload: error }) 14 | throw error 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/containers/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import ErrorRenderer from '../renderers/Error' 5 | import { dismiss } from '../actionCreators' 6 | import { selectError } from '../selectors' 7 | 8 | const propTypes = { 9 | message: PropTypes.string, 10 | } 11 | 12 | const Error = props => (props.message ? : null) 13 | 14 | Error.propTypes = propTypes 15 | 16 | export default connect( 17 | selectError, 18 | { 19 | onDismiss: dismiss, 20 | } 21 | )(Error) 22 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/error/renderers/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const propTypes = { 5 | message: PropTypes.string.isRequired, 6 | onDismiss: PropTypes.func.isRequired, 7 | } 8 | 9 | const Error = ({ message, onDismiss }) => ( 10 |

11 | {message}{' '} 12 | 20 |

21 | ) 22 | 23 | Error.propTypes = propTypes 24 | 25 | export default Error 26 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/Profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Profile = ({ login, avatarUrl, name }) => ( 6 |
7 | 8 | {login} 9 |

10 | {login} {name && ({name})} 11 |

12 | 13 |
14 | ) 15 | 16 | Profile.propTypes = { 17 | login: PropTypes.string.isRequired, 18 | avatarUrl: PropTypes.string.isRequired, 19 | name: PropTypes.string, 20 | } 21 | 22 | export default Profile 23 | -------------------------------------------------------------------------------- /demos/scoped-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/reducer.js: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick' 2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes' 3 | 4 | const defaultState = { 5 | user: null, 6 | status: 'initial', 7 | } 8 | 9 | const states = { 10 | [LOAD]: state => ({ 11 | ...defaultState, 12 | status: 'loading', 13 | }), 14 | [HANDLE_RESPONSE]: (state, user) => ({ 15 | ...state, 16 | user: pick(user, 'login', 'avatarUrl', 'name'), 17 | status: 'loaded', 18 | }), 19 | [HANDLE_ERROR]: state => ({ 20 | ...defaultState, 21 | status: 'error', 22 | }), 23 | } 24 | 25 | export default (state = defaultState, { type, payload } = {}) => 26 | states[type] ? states[type](state, payload) : state 27 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/reducer.js: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick' 2 | import { LOAD, HANDLE_RESPONSE, HANDLE_ERROR } from './actionTypes' 3 | 4 | const defaultState = { 5 | repo: null, 6 | owner: null, 7 | status: 'initial', 8 | } 9 | 10 | const states = { 11 | [LOAD]: state => ({ 12 | ...defaultState, 13 | status: 'loading', 14 | }), 15 | [HANDLE_RESPONSE]: (state, repo) => ({ 16 | ...state, 17 | repo: pick(repo, 'name', 'description'), 18 | owner: { 19 | login: repo.owner.login, 20 | }, 21 | status: 'loaded', 22 | }), 23 | [HANDLE_ERROR]: state => ({ 24 | ...defaultState, 25 | status: 'error', 26 | }), 27 | } 28 | 29 | export default (state = defaultState, { type, payload } = {}) => 30 | states[type] ? states[type](state, payload) : state 31 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/Repo/Repo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Repo = ({ repo, owner }) => { 6 | const { login } = owner 7 | const { name, description } = repo 8 | 9 | return ( 10 |
11 |

12 | {name} 13 | {' by '} 14 | {login} 15 |

16 | {description &&

{description}

} 17 |
18 | ) 19 | } 20 | 21 | Repo.propTypes = { 22 | repo: PropTypes.shape({ 23 | name: PropTypes.string.isRequired, 24 | description: PropTypes.string, 25 | }).isRequired, 26 | owner: PropTypes.shape({ 27 | login: PropTypes.string.isRequired, 28 | }).isRequired, 29 | } 30 | 31 | export default Repo 32 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/user/containers/UserPage.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { Profile } from '../../../features/profile' 5 | import { StarredRepos } from '../../../features/starredRepos' 6 | import { handle as handleError } from '../../../features/error' 7 | 8 | const propTypes = { 9 | login: PropTypes.string.isRequired, 10 | } 11 | 12 | const UserPage = ({ login, onError }) => ( 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | UserPage.propTypes = propTypes 20 | 21 | export default connect( 22 | (state, props) => ({ 23 | login: props.match.params.login, 24 | }), 25 | { 26 | onError: handleError, 27 | } 28 | )(UserPage) 29 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/repo/containers/RepoPage.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { Repo } from '../../../features/repo' 5 | import { Stargazers } from '../../../features/stargazers' 6 | import { handle as handleError } from '../../../features/error' 7 | 8 | const propTypes = { 9 | fullName: PropTypes.string.isRequired, 10 | } 11 | 12 | const RepoPage = ({ fullName, onError }) => ( 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | RepoPage.propTypes = propTypes 20 | 21 | export default connect( 22 | (state, props) => ({ 23 | fullName: `${props.match.params.login}/${props.match.params.repo}`, 24 | }), 25 | { 26 | onError: handleError, 27 | } 28 | )(RepoPage) 29 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../shared/api' 2 | import { 3 | LOAD, 4 | LOAD_NEXT, 5 | HANDLE_RESPONSE, 6 | HANDLE_NEXT_RESPONSE, 7 | HANDLE_ERROR, 8 | } from './actionTypes' 9 | 10 | const handleError = dispatch => error => { 11 | dispatch({ type: HANDLE_ERROR, payload: error }) 12 | throw error 13 | } 14 | 15 | export const load = ({ login }) => dispatch => { 16 | dispatch({ type: LOAD, payload: login }) 17 | return api 18 | .call(`users/${login}/starred`) 19 | .then(payload => { 20 | dispatch({ type: HANDLE_RESPONSE, payload }) 21 | }) 22 | .catch(handleError(dispatch)) 23 | } 24 | 25 | export const loadNext = ({ url }) => dispatch => { 26 | dispatch({ type: LOAD_NEXT }) 27 | return api 28 | .call(url) 29 | .then(payload => { 30 | dispatch({ type: HANDLE_NEXT_RESPONSE, payload }) 31 | }) 32 | .catch(handleError(dispatch)) 33 | } 34 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../shared/api' 2 | import { 3 | LOAD, 4 | LOAD_NEXT, 5 | HANDLE_RESPONSE, 6 | HANDLE_NEXT_RESPONSE, 7 | HANDLE_ERROR, 8 | } from './actionTypes' 9 | 10 | const handleError = dispatch => error => { 11 | dispatch({ type: HANDLE_ERROR, payload: error }) 12 | throw error 13 | } 14 | 15 | export const load = ({ fullName }) => dispatch => { 16 | dispatch({ type: LOAD, payload: fullName }) 17 | return api 18 | .call(`repos/${fullName}/stargazers`) 19 | .then(payload => { 20 | dispatch({ type: HANDLE_RESPONSE, payload }) 21 | }) 22 | .catch(handleError(dispatch)) 23 | } 24 | 25 | export const loadNext = ({ url }) => dispatch => { 26 | dispatch({ type: LOAD_NEXT }) 27 | return api 28 | .call(url) 29 | .then(payload => { 30 | dispatch({ type: HANDLE_NEXT_RESPONSE, payload }) 31 | }) 32 | .catch(handleError(dispatch)) 33 | } 34 | -------------------------------------------------------------------------------- /demos/scoped-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scoped-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^2.0.4", 7 | "redux-devtools": "^3.4.1", 8 | "redux-devtools-dock-monitor": "^1.1.3", 9 | "redux-devtools-log-monitor": "^1.4.0", 10 | "redux-logger": "^3.0.6" 11 | }, 12 | "dependencies": { 13 | "camelcase-keys": "^4.2.0", 14 | "lodash": "^4.17.5", 15 | "prop-types": "^15.6.1", 16 | "react": "^16.3.1", 17 | "react-dom": "^16.3.1", 18 | "react-redux": "^5.0.7", 19 | "react-router-dom": "^4.1.2", 20 | "redux": "^3.5.2", 21 | "redux-thunk": "^2.1.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "eject": "react-scripts eject", 27 | "test": "react-scripts test" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { createLogger } from 'redux-logger' 4 | import DevTools from '../shared/components/DevTools' 5 | import { reducer as errorReducer } from '../features/error' 6 | import { reducer as profileReducer } from '../features/profile' 7 | import { reducer as repoReducer } from '../features/repo' 8 | import { reducer as starredReposReducer } from '../features/starredRepos' 9 | import { reducer as stargazersReducer } from '../features/stargazers' 10 | 11 | const reducer = combineReducers({ 12 | features: combineReducers({ 13 | error: errorReducer, 14 | profile: profileReducer, 15 | repo: repoReducer, 16 | starredRepos: starredReposReducer, 17 | stargazers: stargazersReducer, 18 | }), 19 | pages: {}, 20 | }) 21 | 22 | export const configure = preloadedState => { 23 | const store = createStore( 24 | reducer, 25 | preloadedState, 26 | compose( 27 | applyMiddleware(thunk, createLogger()), 28 | DevTools.instrument() 29 | ) 30 | ) 31 | 32 | return store 33 | } 34 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/reducer.js: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick' 2 | import { 3 | LOAD, 4 | HANDLE_RESPONSE, 5 | HANDLE_NEXT_RESPONSE, 6 | HANDLE_ERROR, 7 | LOAD_NEXT, 8 | } from './actionTypes' 9 | 10 | const defaultState = { 11 | users: [], 12 | status: 'initial', 13 | } 14 | 15 | const mapResult = data => pick(data, 'login', 'avatarUrl') 16 | 17 | const states = { 18 | [LOAD]: state => ({ 19 | ...defaultState, 20 | status: 'loading', 21 | }), 22 | [LOAD_NEXT]: state => ({ 23 | ...state, 24 | status: 'loading', 25 | }), 26 | [HANDLE_RESPONSE]: (state, { result, ...rest }) => ({ 27 | ...state, 28 | ...rest, 29 | users: result.map(mapResult), 30 | status: 'loaded', 31 | }), 32 | [HANDLE_NEXT_RESPONSE]: (state, { result, ...rest }) => ({ 33 | ...state, 34 | ...rest, 35 | users: [...state.users, ...result.map(mapResult)], 36 | status: 'loaded', 37 | }), 38 | [HANDLE_ERROR]: state => ({ 39 | ...defaultState, 40 | status: 'error', 41 | }), 42 | } 43 | 44 | export default (state = defaultState, { type, payload } = {}) => 45 | states[type] ? states[type](state, payload) : state 46 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/pages/root/containers/RootPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { Search } from '../../../features/search' 5 | import { Error, dismiss as dismissError } from '../../../features/error' 6 | 7 | const propTypes = { 8 | search: PropTypes.string, 9 | onChangeSearch: PropTypes.func.isRequired, 10 | onSearch: PropTypes.func.isRequired, 11 | } 12 | 13 | class RootPageContainer extends Component { 14 | static propTypes = propTypes 15 | 16 | componentDidUpdate(prevProps) { 17 | if (prevProps.search !== this.props.search) { 18 | this.props.onChangeSearch() 19 | } 20 | } 21 | 22 | render() { 23 | const { search, onSearch } = this.props 24 | return ( 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | } 32 | 33 | export default connect( 34 | (state, { location, history }) => ({ 35 | search: location.pathname.substr(1), 36 | onSearch: ({ value }) => { 37 | history.push(`/${value}`) 38 | }, 39 | }), 40 | { 41 | onChangeSearch: dismissError, 42 | } 43 | )(RootPageContainer) 44 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/reducer.js: -------------------------------------------------------------------------------- 1 | import pick from 'lodash/pick' 2 | import { 3 | LOAD, 4 | HANDLE_RESPONSE, 5 | HANDLE_NEXT_RESPONSE, 6 | HANDLE_ERROR, 7 | LOAD_NEXT, 8 | } from './actionTypes' 9 | 10 | const defaultState = { 11 | starred: [], 12 | status: 'initial', 13 | } 14 | 15 | const mapResult = data => ({ 16 | repo: pick(data, 'name', 'fullName', 'description'), 17 | owner: { 18 | login: data.owner.login, 19 | }, 20 | }) 21 | 22 | const states = { 23 | [LOAD]: state => ({ 24 | ...defaultState, 25 | status: 'loading', 26 | }), 27 | [LOAD_NEXT]: state => ({ 28 | ...state, 29 | status: 'loading', 30 | }), 31 | [HANDLE_RESPONSE]: (state, { result, ...rest }) => ({ 32 | ...state, 33 | ...rest, 34 | starred: result.map(mapResult), 35 | status: 'loaded', 36 | }), 37 | [HANDLE_NEXT_RESPONSE]: (state, { result, ...rest }) => ({ 38 | ...state, 39 | ...rest, 40 | starred: [...state.starred, ...result.map(mapResult)], 41 | status: 'loaded', 42 | }), 43 | [HANDLE_ERROR]: state => ({ 44 | ...defaultState, 45 | status: 'error', 46 | }), 47 | } 48 | 49 | export default (state = defaultState, { type, payload } = {}) => 50 | states[type] ? states[type](state, payload) : state 51 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/api/index.js: -------------------------------------------------------------------------------- 1 | import camelizeKeys from 'camelcase-keys' 2 | 3 | const API_ROOT = 'https://api.github.com/' 4 | 5 | // Extracts the next page URL from Github API response. 6 | const getLink = (rel, response) => { 7 | const link = response.headers.get('link') 8 | if (!link) { 9 | return null 10 | } 11 | 12 | const nextLink = link.split(',').find(s => s.indexOf(`rel="${rel}"`) > -1) 13 | if (!nextLink) { 14 | return null 15 | } 16 | 17 | return nextLink 18 | .trim() 19 | .split(';')[0] 20 | .slice(1, -1) 21 | } 22 | 23 | // Fetches an API response and normalizes the result JSON according to schema. 24 | // This makes every API response have the same shape, regardless of how nested it was. 25 | export const call = endpoint => { 26 | const url = endpoint.indexOf(API_ROOT) !== 0 ? API_ROOT + endpoint : endpoint 27 | 28 | return fetch(url).then(response => 29 | response.json().then(json => { 30 | if (!response.ok) { 31 | return Promise.reject(json) 32 | } 33 | 34 | const result = camelizeKeys(json, { deep: true }) 35 | const nextPageUrl = getLink('next', response) 36 | const lastPageUrl = getLink('last', response) 37 | return { result, nextPageUrl, lastPageUrl } 38 | }) 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/repo/containers/Repo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { LOADING_STATES } from '../../../shared/constants' 5 | import Repo from '../../../shared/components/Repo' 6 | import * as actions from '../actionCreators' 7 | import { selectRepo } from '../selectors' 8 | import Loading from '../renderers/Loading' 9 | import Empty from '../renderers/Empty' 10 | 11 | const propTypes = { 12 | fullName: PropTypes.string.isRequired, 13 | status: PropTypes.oneOf(LOADING_STATES).isRequired, 14 | onLoad: PropTypes.func.isRequired, 15 | repo: PropTypes.shape(), 16 | owner: PropTypes.shape(), 17 | } 18 | 19 | class RepoContainer extends Component { 20 | static propTypes = propTypes 21 | 22 | componentDidMount() { 23 | this.load() 24 | } 25 | 26 | componentDidUpdate(prevProps) { 27 | this.load(prevProps.fullName) 28 | } 29 | 30 | load(prevFullName) { 31 | const { fullName, onLoad, onError, status } = this.props 32 | if (fullName && fullName !== prevFullName && status !== 'loading') { 33 | onLoad({ fullName }).catch(onError) 34 | } 35 | } 36 | 37 | render() { 38 | const { status, fullName, repo, owner } = this.props 39 | 40 | if (status === 'loading') { 41 | return 42 | } 43 | 44 | if (status === 'loaded') { 45 | return 46 | } 47 | 48 | return 49 | } 50 | } 51 | 52 | export default connect( 53 | selectRepo, 54 | { 55 | onLoad: actions.load, 56 | } 57 | )(RepoContainer) 58 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/profile/containers/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { LOADING_STATES } from '../../../shared/constants' 5 | import Profile from '../../../shared/components/Profile' 6 | import * as actions from '../actionCreators' 7 | import { selectProfile } from '../selectors' 8 | import Loading from '../renderers/Loading' 9 | import Empty from '../renderers/Empty' 10 | import Title from '../renderers/Title' 11 | 12 | const propTypes = { 13 | login: PropTypes.string.isRequired, 14 | status: PropTypes.oneOf(LOADING_STATES).isRequired, 15 | onLoad: PropTypes.func.isRequired, 16 | user: PropTypes.shape(), 17 | } 18 | 19 | class ProfileContainer extends Component { 20 | static propTypes = propTypes 21 | 22 | componentDidMount() { 23 | this.load() 24 | } 25 | 26 | componentDidUpdate(prevProps) { 27 | this.load(prevProps.login) 28 | } 29 | 30 | load(prevLogin) { 31 | const { login, onLoad, onError, status } = this.props 32 | if (login && login !== prevLogin && status !== 'loading') { 33 | onLoad({ login }).catch(onError) 34 | } 35 | } 36 | 37 | render() { 38 | const { status, login, user } = this.props 39 | 40 | return ( 41 | 42 | 43 | {status === 'loading' && <Loading login={login} />} 44 | {status === 'loaded' && <Profile {...user} />} 45 | {status === 'initial' && <Empty />} 46 | </Fragment> 47 | ) 48 | } 49 | } 50 | 51 | export default connect( 52 | selectProfile, 53 | { 54 | onLoad: actions.load, 55 | } 56 | )(ProfileContainer) 57 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/shared/components/List/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { LOADING_STATES } from '../../constants' 4 | 5 | const LoadMoreButton = ({ status, onClick }) => ( 6 | <button 7 | style={{ fontSize: '150%' }} 8 | onClick={onClick} 9 | disabled={status === 'loading'} 10 | > 11 | {status === 'loading' ? 'Loading...' : 'Load More'} 12 | </button> 13 | ) 14 | 15 | const Loading = ({ label }) => ( 16 | <h2> 17 | <i>{label}</i> 18 | </h2> 19 | ) 20 | 21 | const Empty = () => ( 22 | <h1> 23 | <i>Nothing here!</i> 24 | </h1> 25 | ) 26 | 27 | const List = props => { 28 | const { 29 | status, 30 | nextPageUrl, 31 | lastPageUrl, 32 | items, 33 | renderItem, 34 | loadingLabel, 35 | onLoadNext, 36 | } = props 37 | 38 | const isEmpty = items.length === 0 39 | const isLastPage = !nextPageUrl 40 | const isSinglePage = nextPageUrl === lastPageUrl 41 | 42 | if (isEmpty && status === 'loading') { 43 | return <Loading label={loadingLabel} /> 44 | } 45 | 46 | if (isEmpty && isLastPage) { 47 | return <Empty /> 48 | } 49 | 50 | return ( 51 | <div> 52 | {items.map(renderItem)} 53 | {!isSinglePage && 54 | !isLastPage && ( 55 | <LoadMoreButton 56 | status={status} 57 | onClick={() => { 58 | onLoadNext({ url: nextPageUrl }) 59 | }} 60 | /> 61 | )} 62 | </div> 63 | ) 64 | } 65 | 66 | List.propTypes = { 67 | loadingLabel: PropTypes.string.isRequired, 68 | renderItem: PropTypes.func.isRequired, 69 | items: PropTypes.array.isRequired, 70 | status: PropTypes.oneOf(LOADING_STATES).isRequired, 71 | onLoadNext: PropTypes.func.isRequired, 72 | nextPageUrl: PropTypes.string, 73 | } 74 | 75 | List.defaultProps = { 76 | status: 'loading', 77 | loadingLabel: 'Loading...', 78 | items: [], 79 | } 80 | 81 | export default List 82 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/search/renderers/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const propTypes = { 5 | value: PropTypes.string.isRequired, 6 | onSearch: PropTypes.func.isRequired, 7 | } 8 | 9 | const Input = ({ value, onSearch, onChange }) => ( 10 | <input 11 | size="45" 12 | value={value} 13 | onKeyUp={({ keyCode }) => { 14 | if (keyCode === 13) onSearch() 15 | }} 16 | onChange={e => { 17 | onChange({ value: e.target.value }) 18 | }} 19 | /> 20 | ) 21 | 22 | const Label = () => ( 23 | <label style={{ display: 'block' }}> 24 | Type a username or repo full name and hit 'Go': 25 | </label> 26 | ) 27 | 28 | const RepoHint = () => ( 29 | <p> 30 | Code on{' '} 31 | <a 32 | href="https://github.com/kof/feature-driven-architecture/" 33 | target="_blank" 34 | rel="noopener noreferrer" 35 | > 36 | Github 37 | </a> 38 | . 39 | </p> 40 | ) 41 | 42 | const DevToolsHint = () => ( 43 | <p>Move the DevTools with Ctrl+W or hide them with Ctrl+H.</p> 44 | ) 45 | 46 | export default class Search extends Component { 47 | static propTypes = propTypes 48 | 49 | constructor(props) { 50 | super(props) 51 | this.state = { value: props.value } 52 | } 53 | 54 | componentWillReceiveProps(nextProps) { 55 | if (nextProps.value !== this.state.value) { 56 | this.setState({ value: nextProps.value }) 57 | } 58 | } 59 | 60 | onChange = ({ value }) => { 61 | this.setState({ value }) 62 | } 63 | 64 | onSearch = () => { 65 | this.props.onSearch({ value: this.state.value }) 66 | } 67 | 68 | render() { 69 | return ( 70 | <Fragment> 71 | <Label /> 72 | <Input 73 | value={this.state.value} 74 | onChange={this.onChange} 75 | onSearch={this.onSearch} 76 | /> 77 | <button onClick={this.onSearch}>Go!</button> 78 | <RepoHint /> 79 | <DevToolsHint /> 80 | </Fragment> 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/stargazers/containers/Stargazers.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { LOADING_STATES } from '../../../shared/constants' 5 | import List from '../../../shared/components/List' 6 | import Profile from '../../../shared/components/Profile' 7 | import Title from '../renderers/Title' 8 | import * as actions from '../actionCreators' 9 | import { selectStargazers } from '../selectors' 10 | 11 | const propTypes = { 12 | fullName: PropTypes.string.isRequired, 13 | status: PropTypes.oneOf(LOADING_STATES).isRequired, 14 | onLoad: PropTypes.func.isRequired, 15 | onLoadNext: PropTypes.func.isRequired, 16 | nextPageUrl: PropTypes.string, 17 | lastPageUrl: PropTypes.string, 18 | users: PropTypes.array.isRequired, 19 | } 20 | 21 | class StargazersContainer extends Component { 22 | static propTypes = propTypes 23 | 24 | componentDidMount() { 25 | this.load() 26 | } 27 | 28 | componentDidUpdate(prevProps) { 29 | this.load(prevProps.fullName) 30 | } 31 | 32 | load(prevfullName) { 33 | const { fullName, onLoad, onError, status } = this.props 34 | 35 | if (fullName && fullName !== prevfullName && status !== 'loading') { 36 | onLoad({ fullName }).catch(onError) 37 | } 38 | } 39 | 40 | render() { 41 | const { 42 | fullName, 43 | users, 44 | onLoadNext, 45 | nextPageUrl, 46 | lastPageUrl, 47 | status, 48 | } = this.props 49 | 50 | return ( 51 | <Fragment> 52 | <Title /> 53 | <List 54 | renderItem={props => <Profile {...props} key={props.login} />} 55 | items={users} 56 | onLoadNext={onLoadNext} 57 | loadingLabel={`Loading ${fullName}'s stargazers...`} 58 | nextPageUrl={nextPageUrl} 59 | lastPageUrl={lastPageUrl} 60 | status={status} 61 | /> 62 | </Fragment> 63 | ) 64 | } 65 | } 66 | 67 | export default connect( 68 | selectStargazers, 69 | { 70 | onLoad: actions.load, 71 | onLoadNext: actions.loadNext, 72 | } 73 | )(StargazersContainer) 74 | -------------------------------------------------------------------------------- /demos/scoped-redux/src/features/starredRepos/containers/StarredRepos.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { LOADING_STATES } from '../../../shared/constants' 5 | import List from '../../../shared/components/List' 6 | import Repo from '../../../shared/components/Repo' 7 | import Title from '../renderers/Title' 8 | import * as actions from '../actionCreators' 9 | import { selectStarredRepos } from '../selectors' 10 | 11 | const propTypes = { 12 | login: PropTypes.string.isRequired, 13 | status: PropTypes.oneOf(LOADING_STATES).isRequired, 14 | onLoad: PropTypes.func.isRequired, 15 | onLoadNext: PropTypes.func.isRequired, 16 | nextPageUrl: PropTypes.string, 17 | lastPageUrl: PropTypes.string, 18 | starred: PropTypes.array.isRequired, 19 | } 20 | 21 | class StarredReposContainer extends Component { 22 | static propTypes = propTypes 23 | 24 | componentDidMount() { 25 | this.load() 26 | } 27 | 28 | componentDidUpdate(prevProps) { 29 | this.load(prevProps.login) 30 | } 31 | 32 | load = prevLogin => { 33 | const { login, onLoad, onError, status } = this.props 34 | 35 | if (login && login !== prevLogin && status !== 'loading') { 36 | onLoad({ login }).catch(onError) 37 | } 38 | } 39 | 40 | loadNext = options => { 41 | const { onLoadNext, onError } = this.props 42 | onLoadNext(options).catch(onError) 43 | } 44 | 45 | render() { 46 | const { 47 | login, 48 | starred, 49 | onLoadNext, 50 | nextPageUrl, 51 | lastPageUrl, 52 | status, 53 | } = this.props 54 | 55 | return ( 56 | <Fragment> 57 | <Title /> 58 | <List 59 | renderItem={props => <Repo {...props} key={props.repo.fullName} />} 60 | items={starred} 61 | onLoadNext={this.loadNext} 62 | loadingLabel={`Loading ${login}'s starred...`} 63 | nextPageUrl={nextPageUrl} 64 | lastPageUrl={lastPageUrl} 65 | status={status} 66 | /> 67 | </Fragment> 68 | ) 69 | } 70 | } 71 | 72 | export default connect( 73 | selectStarredRepos, 74 | { 75 | onLoad: actions.load, 76 | onLoadNext: actions.loadNext, 77 | } 78 | )(StarredReposContainer) 79 | --------------------------------------------------------------------------------