Перейти к содержанию

Пример: Reddit API

Это полный исходный код примера выборки заголовков Reddit, который мы строили в продвинутом руководстве.

Точка Входа

index.js
1
2
3
4
5
6
7
import 'babel-polyfill';

import React from 'react';
import { render } from 'react-dom';
import Root from './containers/Root';

render(<Root />, document.getElementById('root'));

Генераторы экшенов и константы

actions.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import fetch from 'cross-fetch';

export const REQUEST_POSTS = 'REQUEST_POSTS';
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT';
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT';

export function selectSubreddit(subreddit) {
    return {
        type: SELECT_SUBREDDIT,
        subreddit,
    };
}

export function invalidateSubreddit(subreddit) {
    return {
        type: INVALIDATE_SUBREDDIT,
        subreddit,
    };
}

function requestPosts(subreddit) {
    return {
        type: REQUEST_POSTS,
        subreddit,
    };
}

function receivePosts(subreddit, json) {
    return {
        type: RECEIVE_POSTS,
        subreddit,
        posts: json.data.children.map(
            (child) => child.data
        ),
        receivedAt: Date.now(),
    };
}

function fetchPosts(subreddit) {
    return (dispatch) => {
        dispatch(requestPosts(subreddit));
        return fetch(
            `https://www.reddit.com/r/${subreddit}.json`
        )
            .then((response) => response.json())
            .then((json) =>
                dispatch(receivePosts(subreddit, json))
            );
    };
}

function shouldFetchPosts(state, subreddit) {
    const posts = state.postsBySubreddit[subreddit];
    if (!posts) {
        return true;
    } else if (posts.isFetching) {
        return false;
    } else {
        return posts.didInvalidate;
    }
}

export function fetchPostsIfNeeded(subreddit) {
    return (dispatch, getState) => {
        if (shouldFetchPosts(getState(), subreddit)) {
            return dispatch(fetchPosts(subreddit));
        }
    };
}

Редьюсеры

reducers.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { combineReducers } from 'redux';
import {
    SELECT_SUBREDDIT,
    INVALIDATE_SUBREDDIT,
    REQUEST_POSTS,
    RECEIVE_POSTS,
} from './actions';

function selectedSubreddit(state = 'reactjs', action) {
    switch (action.type) {
        case SELECT_SUBREDDIT:
            return action.subreddit;
        default:
            return state;
    }
}

function posts(
    state = {
        isFetching: false,
        didInvalidate: false,
        items: [],
    },
    action
) {
    switch (action.type) {
        case INVALIDATE_SUBREDDIT:
            return Object.assign({}, state, {
                didInvalidate: true,
            });
        case REQUEST_POSTS:
            return Object.assign({}, state, {
                isFetching: true,
                didInvalidate: false,
            });
        case RECEIVE_POSTS:
            return Object.assign({}, state, {
                isFetching: false,
                didInvalidate: false,
                items: action.posts,
                lastUpdated: action.receivedAt,
            });
        default:
            return state;
    }
}

function postsBySubreddit(state = {}, action) {
    switch (action.type) {
        case INVALIDATE_SUBREDDIT:
        case RECEIVE_POSTS:
        case REQUEST_POSTS:
            return Object.assign({}, state, {
                [action.subreddit]: posts(
                    state[action.subreddit],
                    action
                ),
            });
        default:
            return state;
    }
}

const rootReducer = combineReducers({
    postsBySubreddit,
    selectedSubreddit,
});

export default rootReducer;

Стор

configureStore.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import rootReducer from './reducers';

const loggerMiddleware = createLogger();

export default function configureStore(preloadedState) {
    return createStore(
        rootReducer,
        preloadedState,
        applyMiddleware(thunkMiddleware, loggerMiddleware)
    );
}

Компоненты контейнеры

containers/Root.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from '../configureStore';
import AsyncApp from './AsyncApp';

const store = configureStore();

export default class Root extends Component {
    render() {
        return (
            <Provider store={store}>
                <AsyncApp />
            </Provider>
        );
    }
}
containers/AsyncApp.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
    selectSubreddit,
    fetchPostsIfNeeded,
    invalidateSubreddit,
} from '../actions';
import Picker from '../components/Picker';
import Posts from '../components/Posts';

class AsyncApp extends Component {
    constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.handleRefreshClick = this.handleRefreshClick.bind(
            this
        );
    }

    componentDidMount() {
        const { dispatch, selectedSubreddit } = this.props;
        dispatch(fetchPostsIfNeeded(selectedSubreddit));
    }

    componentDidUpdate(prevProps) {
        if (
            this.props.selectedSubreddit !==
            prevProps.selectedSubreddit
        ) {
            const {
                dispatch,
                selectedSubreddit,
            } = this.props;
            dispatch(fetchPostsIfNeeded(selectedSubreddit));
        }
    }

    handleChange(nextSubreddit) {
        this.props.dispatch(selectSubreddit(nextSubreddit));
        this.props.dispatch(
            fetchPostsIfNeeded(nextSubreddit)
        );
    }

    handleRefreshClick(e) {
        e.preventDefault();

        const { dispatch, selectedSubreddit } = this.props;
        dispatch(invalidateSubreddit(selectedSubreddit));
        dispatch(fetchPostsIfNeeded(selectedSubreddit));
    }

    render() {
        const {
            selectedSubreddit,
            posts,
            isFetching,
            lastUpdated,
        } = this.props;
        return (
            <div>
                <Picker
                    value={selectedSubreddit}
                    onChange={this.handleChange}
                    options={['reactjs', 'frontend']}
                />
                <p>
                    {lastUpdated && (
                        <span>
                            Last updated at{' '}
                            {new Date(
                                lastUpdated
                            ).toLocaleTimeString()}
                            .{' '}
                        </span>
                    )}
                    {!isFetching && (
                        <button
                            onClick={
                                this.handleRefreshClick
                            }
                        >
                            Refresh
                        </button>
                    )}
                </p>
                {isFetching && posts.length === 0 && (
                    <h2>Loading...</h2>
                )}
                {!isFetching && posts.length === 0 && (
                    <h2>Empty.</h2>
                )}
                {posts.length > 0 && (
                    <div
                        style={{
                            opacity: isFetching ? 0.5 : 1,
                        }}
                    >
                        <Posts posts={posts} />
                    </div>
                )}
            </div>
        );
    }
}

AsyncApp.propTypes = {
    selectedSubreddit: PropTypes.string.isRequired,
    posts: PropTypes.array.isRequired,
    isFetching: PropTypes.bool.isRequired,
    lastUpdated: PropTypes.number,
    dispatch: PropTypes.func.isRequired,
};

function mapStateToProps(state) {
    const { selectedSubreddit, postsBySubreddit } = state;
    const {
        isFetching,
        lastUpdated,
        items: posts,
    } = postsBySubreddit[selectedSubreddit] || {
        isFetching: true,
        items: [],
    };

    return {
        selectedSubreddit,
        posts,
        isFetching,
        lastUpdated,
    };
}

export default connect(mapStateToProps)(AsyncApp);

Компоненты представления

components/Picker.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Picker extends Component {
    render() {
        const { value, onChange, options } = this.props;

        return (
            <span>
                <h1>{value}</h1>
                <select
                    onChange={(e) =>
                        onChange(e.target.value)
                    }
                    value={value}
                >
                    {options.map((option) => (
                        <option value={option} key={option}>
                            {option}
                        </option>
                    ))}
                </select>
            </span>
        );
    }
}

Picker.propTypes = {
    options: PropTypes.arrayOf(PropTypes.string.isRequired)
        .isRequired,
    value: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
};
components/Posts.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Posts extends Component {
    render() {
        return (
            <ul>
                {this.props.posts.map((post, i) => (
                    <li key={i}>{post.title}</li>
                ))}
            </ul>
        );
    }
}

Posts.propTypes = {
    posts: PropTypes.array.isRequired,
};

Комментарии