Skip to content

Commit 6955516

Browse files
committed
add redux structure
1 parent 9986cd4 commit 6955516

File tree

13 files changed

+400
-38
lines changed

13 files changed

+400
-38
lines changed

.babelrc

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"presets": [
3+
"env",
4+
"next/babel"
5+
],
6+
"plugins": [
7+
["module-resolver", {
8+
"root": ["./src"]
9+
}]
10+
]
11+
}

package.json

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
{
22
"dependencies": {
3+
"axios": "^0.17.1",
4+
"babel-plugin-module-resolver": "^3.0.0",
5+
"babel-preset-env": "^1.6.1",
36
"compression": "^1.7.1",
47
"express": "^4.16.2",
5-
"next": "^4.1.2",
8+
"humps": "^2.0.1",
9+
"immutable": "^3.8.2",
10+
"next": "4.2.0",
11+
"next-redux-wrapper": "^1.3.5",
612
"next-routes": "^1.1.0",
7-
"react": "^16.0.0",
8-
"react-dom": "^16.0.0"
13+
"prop-type": "^0.0.1",
14+
"react": "16.2.0",
15+
"react-dom": "16.2.0",
16+
"react-redux": "^5.0.6",
17+
"redux": "^3.7.2",
18+
"redux-logger": "^3.0.6",
19+
"redux-thunk": "^2.2.0"
920
},
1021
"name": "nextjs-real-world",
1122
"version": "1.0.0",

pages/index.js

+3-19
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,4 @@
1-
import { Link } from '../routes'
1+
import withReduxStore from 'store/createStore'
2+
import SearchRepoContainer from 'containers/SearchRepoContainer'
23

3-
export default () => (
4-
<div className='hello'>
5-
next.js real world
6-
<div>
7-
<Link route='/about'>About</Link>
8-
</div>
9-
<style jsx>{`
10-
.hello {
11-
font: 20px Helvetica, Arial, sans-serif;
12-
background: #f8f8f8;
13-
color: #b0b0b0;
14-
padding: 2em;
15-
height: 100vh;
16-
text-align: center;
17-
}
18-
`}</style>
19-
</div>
20-
)
4+
export default withReduxStore(SearchRepoContainer)

routes.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
const nextRoutes = require('next-routes')
22
const routes = module.exports = nextRoutes()
33

4-
routes.add('about', '/about')
4+
const APP_ROUTES = [{
5+
page: 'index',
6+
pattern: '/'
7+
}, {
8+
page: 'about',
9+
pattern: '/about'
10+
}]
11+
12+
APP_ROUTES.forEach(route => routes.add(route))

src/actions/repos.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
import github from 'libs/github'
3+
4+
export const GET_TOP_REPOS = Symbol('GET_TOP_REPOS')
5+
export const GET_TOP_REPOS_SUCCESS = Symbol('GET_TOP_REPOS_SUCCESS')
6+
7+
export function getTopRepos({ lang }) {
8+
return dispatch => {
9+
10+
dispatch({
11+
type: GET_TOP_REPOS
12+
})
13+
14+
return github.getTopRepos({ lang }).then(res => {
15+
dispatch(onGetTopRepo(lang, res))
16+
})
17+
}
18+
}
19+
20+
function onGetTopRepo(lang, payload) {
21+
return {
22+
type: GET_TOP_REPOS_SUCCESS,
23+
lang,
24+
payload
25+
}
26+
}

src/components/SeachResults.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { Fragment } from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
const REPO_COUNT = 10
5+
const SearchResults = ({ repos }) => {
6+
return (
7+
<Fragment>
8+
<h2>Top { REPO_COUNT } { repos.get('lang') } repos</h2>
9+
<small>{ repos.get('totalCount').toLocaleString() } found</small>
10+
<ul>
11+
{
12+
repos.get('items').take(REPO_COUNT).map(item => (
13+
<li key={item.get('id')}>
14+
<a href={item.get('url')} target='_blank'>
15+
{ item.get('name') }
16+
</a>
17+
</li>
18+
))
19+
}
20+
</ul>
21+
</Fragment>
22+
)
23+
}
24+
25+
SearchResults.propTypes = {
26+
repos: PropTypes.instanceOf(Map).isRequired,
27+
}
28+
29+
export default SearchResults

src/config.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
env: process.env.NODE_ENV,
3+
mode: process.env.MODE,
4+
githubApiEndpoint: process.env.GITHUB_API_ENDPOINT,
5+
}

src/containers/SearchRepoContainer.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { Component } from 'react'
2+
import PropTypes from 'prop-types'
3+
import { Map } from 'immutable'
4+
import { connect } from 'react-redux'
5+
6+
import config from 'config'
7+
import { getTopRepos } from 'actions/repos'
8+
import SearchResults from 'components/SeachResults'
9+
10+
class SearchRepoContainer extends Component {
11+
12+
static async getInitialProps ({ store, query }) {
13+
14+
let lang = query.lang || 'javascript'
15+
await store.dispatch(getTopRepos({ lang }))
16+
}
17+
18+
componentDidMount() {
19+
let { getTopRepos } = this.props
20+
getTopRepos({ lang: 'ruby' })
21+
}
22+
23+
render() {
24+
let { repos } = this.props
25+
return (
26+
<SearchResults repos={repos} />
27+
)
28+
}
29+
30+
}
31+
32+
function mapStateToProps (state) {
33+
return {
34+
repos: state.repos
35+
}
36+
}
37+
38+
SearchRepoContainer.propTypes = {
39+
repos: PropTypes.instanceOf(Map).isRequired,
40+
getTopRepos: PropTypes.func.isRequired
41+
}
42+
43+
export { SearchRepoContainer }
44+
export default connect(mapStateToProps, {
45+
getTopRepos
46+
})(SearchRepoContainer)

src/libs/github.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import axios from 'axios'
2+
import humps from 'humps'
3+
4+
const ENDPOINT = 'https://api.github.com'
5+
6+
const github = {
7+
getTopRepos({ lang = 'javascript' }) {
8+
let path = `${ENDPOINT}/search/repositories?q=language:${lang}&sort=stars&order=desc`
9+
return axios.get(path).then(res => {
10+
return humps.camelizeKeys(res.data)
11+
})
12+
}
13+
}
14+
15+
export default github

src/reducers/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { combineReducers } from 'redux'
2+
3+
import repos from './repos'
4+
5+
export default combineReducers({
6+
repos
7+
})

src/reducers/repos.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Immutable from 'immutable'
2+
import * as ActionType from 'actions/repos'
3+
4+
export const initialState = Immutable.fromJS({
5+
isLoading: false,
6+
lang: ''
7+
})
8+
9+
export default function(state = initialState, action) {
10+
switch (action.type) {
11+
12+
case ActionType.GET_TOP_REPOS:
13+
return state.set('isLoading', true)
14+
15+
case ActionType.GET_TOP_REPOS_SUCCESS:
16+
return state.merge(
17+
Object.assign({}, action.payload, {
18+
isLoading: false,
19+
lang: action.lang
20+
})
21+
)
22+
23+
default:
24+
return state
25+
}
26+
}

src/store/createStore.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Immutable from 'immutable'
2+
import thunkMiddleware from 'redux-thunk'
3+
import { createLogger } from 'redux-logger'
4+
import withRedux from 'next-redux-wrapper'
5+
import { createStore, applyMiddleware, compose } from 'redux'
6+
7+
import config from 'config'
8+
import rootReducer from '../reducers'
9+
10+
function createMiddlewares({ isServer }) {
11+
12+
let middlewares = [
13+
thunkMiddleware,
14+
]
15+
16+
if(config.env === 'development' && typeof window !== 'undefined') {
17+
middlewares.push(createLogger({
18+
level: 'info',
19+
collapsed: true,
20+
stateTransformer: (state) => {
21+
let newState = {}
22+
23+
for (let i of Object.keys(state)) {
24+
if (Immutable.Iterable.isIterable(state[i])) {
25+
newState[i] = state[i].toJS()
26+
} else {
27+
newState[i] = state[i]
28+
}
29+
}
30+
31+
return newState
32+
}
33+
}))
34+
}
35+
36+
return middlewares
37+
}
38+
39+
function immutableChildren(obj) {
40+
let state = {}
41+
Object.keys(obj).map((key) => {
42+
state[key] = Immutable.fromJS(obj[key])
43+
})
44+
return state
45+
}
46+
47+
export const initStore = (initialState = {}, context) => {
48+
49+
let { isServer } = context
50+
let middlewares = createMiddlewares({ isServer })
51+
let state = immutableChildren(initialState)
52+
53+
return createStore(
54+
rootReducer,
55+
state,
56+
compose(applyMiddleware(...middlewares))
57+
)
58+
}
59+
60+
export default (comp) => withRedux(initStore)(comp)

0 commit comments

Comments
 (0)