├── .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 |
16 | - email: {profile.email}
17 | - created: {profile.created_at}
18 | - location: {profile.location}
19 | - blog: {profile.blog}
20 | - followers: {profile.followers}
21 | - following: {profile.following}
22 | - id: {profile.id}
23 | - repos: {profile.public_repos}
24 |
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 |
--------------------------------------------------------------------------------