├── .gitignore ├── README.md ├── build ├── index.html └── index.js ├── package.json ├── src ├── actions │ └── actions.js ├── components │ ├── Profile │ │ ├── Profile.js │ │ └── Profile.less │ ├── Search │ │ ├── Search.js │ │ └── Search.less │ └── index.js ├── containers │ ├── App.js │ ├── App.less │ └── test.less ├── index.html ├── index.js └── reducers │ └── reducers.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | doc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-async-redux 2 | http://jiavan.com/2016/08/26/Redux%E4%B8%AD%E9%97%B4%E4%BB%B6%E4%B8%8E%E5%BC%82%E6%AD%A5Action/ 3 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-redux", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf build/*", 8 | "dev": "cp src/index.html ./build && webpack-dev-server --progress --color --hot --content-base ./build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "react": "^15.3.1", 15 | "react-dom": "^15.3.1", 16 | "react-redux": "^4.4.5", 17 | "redux": "^3.5.2", 18 | "redux-logger": "^2.6.1", 19 | "redux-thunk": "^2.1.0" 20 | }, 21 | "devDependencies": { 22 | "babel-loader": "^6.2.5", 23 | "babel-polyfill": "^6.13.0", 24 | "babel-preset-es2015": "^6.13.2", 25 | "babel-preset-react": "^6.11.1", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "css-loader": "^0.24.0", 28 | "less": "^2.7.1", 29 | "less-loader": "^2.2.3", 30 | "open-browser-webpack-plugin": "^0.0.2", 31 | "style-loader": "^0.13.1", 32 | "webpack": "^1.13.2", 33 | "webpack-dev-server": "^1.15.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/actions/actions.js: -------------------------------------------------------------------------------- 1 | export const GET_INFO = 'GET_INFO'; 2 | export function getInfo(username) { 3 | return { 4 | type: GET_INFO, 5 | username, 6 | }; 7 | } 8 | 9 | export const FETCHING_DATA = 'FETCHING_DATA'; 10 | export function fetchingData(fetching) { 11 | return { 12 | type: FETCHING_DATA, 13 | fetching, 14 | }; 15 | } 16 | 17 | export const RECEIVE_USER_DATA = 'RECEIVE_USER_DATA'; 18 | export function receiveUserData(profile) { 19 | return { 20 | type: RECEIVE_USER_DATA, 21 | profile, 22 | }; 23 | } 24 | 25 | export function fetchUserInfo(username) { 26 | return function (dispatch) { 27 | dispatch(fetchingData(true)); 28 | return fetch(`https://api.github.com/users/${username}`) 29 | .then(response => { 30 | console.log(response); 31 | return response.json(); 32 | }) 33 | .then(json => { 34 | console.log(json); 35 | return json; 36 | }) 37 | .then((json) => { 38 | dispatch(receiveUserData(json)) 39 | }) 40 | .then(() => dispatch(fetchingData(false))) 41 | 42 | 43 | // let req = new XMLHttpRequest(); 44 | // req.open('get', `https://www.v2ex.com/api/members/show.json?username=${username}`); 45 | // req.onload = function load() { 46 | // console.log(req.response); 47 | // } 48 | // req.onerror = function error() { 49 | // console.log('error'); 50 | // } 51 | // req.send(null); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import './Profile.less'; 3 | 4 | export default class Profile extends Component { 5 | 6 | render() { 7 | let profile = this.props.profile; 8 | return ( 9 |
10 |
11 | 12 |

{profile.name}

13 |
14 |
15 | 25 |
26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Profile/Profile.less: -------------------------------------------------------------------------------- 1 | .profile { 2 | width: 500px; 3 | height: 300px; 4 | margin: auto; 5 | } 6 | .profile .avatar { 7 | width: 200px; 8 | margin-top: 20px; 9 | float: left; 10 | } 11 | .profile img { 12 | width: 200px; 13 | height: 200px; 14 | display: block; 15 | border-radius: 10px; 16 | } 17 | .profile .avatar .name { 18 | font-size: 30px; 19 | font-weight: 500; 20 | margin-top: 10px; 21 | } 22 | .profile .avatar .bio { 23 | font-size: 25px; 24 | margin-top: 5px; 25 | } 26 | .profile .introduce { 27 | margin: 20px; 28 | width: 250px; 29 | float: left; 30 | } 31 | .profile .introduce li { 32 | list-style-type: none; 33 | overflow: hidden; 34 | padding-top: 12px; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import './Search.less'; 3 | 4 | export default class Search extends Component { 5 | 6 | handleClick() { 7 | const node = this.refs.username; 8 | const text = node.value.trim(); 9 | this.props.fetchUserInfo(text); 10 | node.value = ''; 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 | 17 | 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Search/Search.less: -------------------------------------------------------------------------------- 1 | .search-box { 2 | width: 400px; 3 | text-align: center; 4 | margin: 100px auto auto; 5 | } 6 | .search-box input { 7 | width: 300px; 8 | height: 30px; 9 | border-radius: 4px; 10 | background: #fff; 11 | border: 1px solid gray; 12 | } 13 | .search-box button { 14 | height: 30px; 15 | width: 80px; 16 | border-radius: 4px; 17 | margin-left: 10px; 18 | background: #007fff; 19 | color: #fff; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export Search from './Search/Search.js'; 2 | export Profile from './Profile/Profile.js'; 3 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Search, Profile } from '../components'; 4 | import { fetchUserInfo, getInfo } from '../actions/actions.js'; 5 | import './App.less'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | profile: state.profile, 10 | isFetchingData: state.isFetchingData, 11 | }; 12 | } 13 | 14 | function mapDispatchToProps(dispatch) { 15 | return { 16 | fetchUserInfo: (username) => dispatch(fetchUserInfo(username)) 17 | }; 18 | } 19 | 20 | class App extends Component { 21 | render() { 22 | const { fetchUserInfo, profile, isFetchingData } = this.props; 23 | return ( 24 |
25 | 26 | {'name' in profile ? : ''} 27 |
28 | ); 29 | } 30 | } 31 | 32 | export default connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(App); 36 | -------------------------------------------------------------------------------- /src/containers/App.less: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | border: 0; 5 | } 6 | body { 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 8 | } 9 | div { 10 | // border: 1px solid red; 11 | } 12 | .container { 13 | position: relative; 14 | overflow: hidden; 15 | margin: auto; 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/test.less: -------------------------------------------------------------------------------- 1 | .boxShadow(@x: 0, @y: 0, @blur: 1px, @color: #fff) { 2 | -webkit-box-shadow: @arguments; 3 | -ms-box-shadow: @arguments; 4 | -moz-box-shadow: @arguments; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import thunkMiddleware from 'redux-thunk'; 6 | import createLogger from 'redux-logger'; 7 | import rootReducer from './reducers/reducers.js'; 8 | import App from './containers/App.js'; 9 | 10 | const loggerMiddleware = createLogger(); 11 | const store = createStore( 12 | rootReducer, 13 | compose( 14 | applyMiddleware( 15 | thunkMiddleware, 16 | loggerMiddleware, 17 | ), 18 | window.devToolsExtension ? window.devToolsExtension() : f => f 19 | ) 20 | ); 21 | 22 | let mountRoot = document.getElementById('app'); 23 | ReactDOM.render( 24 | 25 | 26 | , 27 | mountRoot 28 | ); 29 | -------------------------------------------------------------------------------- /src/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { GET_INFO, FETCHING_DATA, RECEIVE_USER_DATA, fetchingData, receiveUserData, getInfo } from '../actions/actions.js'; 2 | import { combineReducers } from 'redux'; 3 | 4 | const initProfile = { 5 | name: 'jiavan', 6 | avatar_url: 'https://avatars.githubusercontent.com/u/6786013?v=3', 7 | bio: 'this is bio', 8 | }; 9 | 10 | function profile(state = {}, action) { 11 | switch (action.type) { 12 | case GET_INFO: 13 | return Object.assign({}, state, { 14 | username: action.username, 15 | }); 16 | case RECEIVE_USER_DATA: 17 | return Object.assign({}, state, action.profile); 18 | default: return state; 19 | } 20 | } 21 | 22 | function isFetchingData(state = false, action) { 23 | switch (action.type) { 24 | case FETCHING_DATA: 25 | return action.fetching; 26 | default: return state; 27 | } 28 | } 29 | 30 | function username(state = '', action) { 31 | switch (action.type) { 32 | case GET_INFO: 33 | return state = action.username; 34 | default: return state; 35 | } 36 | } 37 | 38 | const rootReducer = combineReducers({ 39 | isFetchingData, 40 | username, 41 | profile, 42 | }); 43 | 44 | export default rootReducer; 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var OpenBrowserPlugin = require('open-browser-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | index: [ 7 | 'webpack/hot/dev-server', 8 | 'webpack-dev-server/client?http://localhost:8080', 9 | './src/index.js', 10 | ] 11 | }, 12 | output: { 13 | path: './build', 14 | filename: '[name].js', 15 | }, 16 | // devtool: 'source-map', 17 | module: { 18 | loaders: [{ 19 | test: /\.js$/, 20 | loader: 'babel', 21 | query: { 22 | presets: ['es2015', 'stage-0', 'react'], 23 | }, 24 | }, { 25 | test: /\.less$/, 26 | loader: 'style!css!less', 27 | }], 28 | }, 29 | plugins: [ 30 | new webpack.HotModuleReplacementPlugin(), 31 | new webpack.NoErrorsPlugin(), 32 | new OpenBrowserPlugin({ url: 'http://localhost:8080' }), 33 | ] 34 | }; 35 | --------------------------------------------------------------------------------