├── .env
├── .gitignore
├── README.md
├── package.json
├── public
├── api
│ ├── authors.json
│ └── books.json
├── dispatch.png
├── favicon.ico
├── index.html
├── middleware.png
├── presentation.html
├── remark-latest.min.js
└── spinner.gif
├── src
├── actions
│ ├── authors.js
│ ├── books.js
│ └── ui.js
├── components
│ ├── App.js
│ ├── Authors.js
│ ├── Book.js
│ ├── Books.js
│ ├── Footer.js
│ ├── ThemeManager.js
│ └── common
│ │ ├── List.js
│ │ ├── PanelTitle.js
│ │ └── Spinner.js
├── consts
│ └── action-types.js
├── index.css
├── index.js
├── lib
│ └── schema.js
├── middleware
│ ├── api.js
│ ├── log.js
│ ├── multi.js
│ └── throttle.js
├── reducers
│ ├── authors.js
│ ├── books.js
│ ├── root.js
│ └── ui.js
└── store.js
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | NODE_PATH=src/
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Sample Middleware project for the React Next 2017 talk by [Boris Dinkevich](https://twitter.com/BorisDinkevich)
2 |
3 | # Starting
4 |
5 | yarn
6 | yarn start
7 |
8 | > On windows you might need to run ```set NODE_ENV=src``` before running ```yarn start```
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-next-2017",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "mimic": "^2.0.2",
7 | "normalizr": "^3.2.3",
8 | "react": "^15.6.1",
9 | "react-dom": "^15.6.1",
10 | "react-redux": "^5.0.5",
11 | "redux": "^3.7.2",
12 | "redux-actions": "^2.2.1",
13 | "redux-thunk": "^2.2.0",
14 | "seamless-immutable": "^7.1.2"
15 | },
16 | "devDependencies": {
17 | "react-scripts": "1.0.10"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test --env=jsdom",
23 | "eject": "react-scripts eject"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/public/api/authors.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 100,
4 | "name": "Kipi the cat"
5 | },
6 | {
7 | "id": 101,
8 | "name": "Clint Eastwood"
9 | }
10 | ]
--------------------------------------------------------------------------------
/public/api/books.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "title": "Harry Potter",
5 | "author": {
6 | "id": 11,
7 | "name": "J. K. Rowling"
8 | }
9 | },
10 | {
11 | "id": 2,
12 | "title": "The Chronicles of Narnia",
13 | "author": {
14 | "id": 12,
15 | "name": "C. S. Lewis"
16 | }
17 | },
18 | {
19 | "id": 3,
20 | "title": "Foundation",
21 | "author": {
22 | "id": 13,
23 | "name": "Isaac Asimov"
24 | }
25 | },
26 | {
27 | "id": 4,
28 | "title": "The Hobbit",
29 | "author": {
30 | "id": 14,
31 | "name": "J.R.R. Tolkien"
32 | }
33 | },
34 | {
35 | "id": 5,
36 | "title": "The Lord of the Rings",
37 | "author": {
38 | "id": 14,
39 | "name": "J.R.R. Tolkien"
40 | }
41 | }
42 | ]
43 |
--------------------------------------------------------------------------------
/public/dispatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/500tech/middleware-lecture/ace1f12a4bcb0e3e2d34172007ea78ff8f55a151/public/dispatch.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/500tech/middleware-lecture/ace1f12a4bcb0e3e2d34172007ea78ff8f55a151/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ReactNext 2017 - Middleware demo
8 |
9 |
10 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/middleware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/500tech/middleware-lecture/ace1f12a4bcb0e3e2d34172007ea78ff8f55a151/public/middleware.png
--------------------------------------------------------------------------------
/public/presentation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Title
5 |
6 |
7 |
16 |
17 |
18 |
80 |
82 |
85 |
86 |
--------------------------------------------------------------------------------
/public/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/500tech/middleware-lecture/ace1f12a4bcb0e3e2d34172007ea78ff8f55a151/public/spinner.gif
--------------------------------------------------------------------------------
/src/actions/authors.js:
--------------------------------------------------------------------------------
1 | import * as actions from 'consts/action-types';
2 | import * as schema from 'lib/schema';
3 |
4 | export const setAuthors = (payload) => ({
5 | type: actions.SET_AUTHORS,
6 | payload
7 | });
8 |
9 | export const fetchAuthors2 = () => (dispatch) => {
10 | fetch('api/authors.json')
11 | .then(response => response.json())
12 | .then(data => dispatch(setAuthors(data)));
13 | };
14 |
15 | export const fetchAuthors = () => ({
16 | type: actions.API,
17 | payload: {
18 | url: 'api/authors.json',
19 | success: ({ entities }) => setAuthors(entities.authors),
20 | schema: [schema.authors],
21 | label: 'authors'
22 | },
23 | meta: {
24 | throttle: 2000
25 | }
26 | });
--------------------------------------------------------------------------------
/src/actions/books.js:
--------------------------------------------------------------------------------
1 | import * as actions from 'consts/action-types';
2 | import { setAuthors } from './authors';
3 | import * as schema from 'lib/schema';
4 |
5 | export const setBooks = (payload) => ({
6 | type: actions.SET_BOOKS,
7 | payload
8 | });
9 |
10 | export const fetchBooks2 = () => (dispatch) => {
11 | fetch('api/books.json')
12 | .then(response => response.json())
13 | .then(data => dispatch(setBooks(data)));
14 | };
15 |
16 | export const fetchBooks = () => ({
17 | type: actions.API,
18 | payload: {
19 | url: 'api/books.json',
20 | schema: [schema.books],
21 | success: ({ entities }) => [
22 | setAuthors(entities.authors),
23 | setBooks(entities.books)
24 | ],
25 | label: 'books'
26 | }
27 | });
28 |
29 | export const updateBook = (id, newName) => ({
30 | type: actions.API,
31 | payload: {
32 | url: `api/books/${ id }`,
33 | method: 'PUT',
34 | data: {
35 | name: newName
36 | }
37 | }
38 | });
--------------------------------------------------------------------------------
/src/actions/ui.js:
--------------------------------------------------------------------------------
1 | import * as actions from 'consts/action-types';
2 |
3 | export const switchTheme = () => ({
4 | type: actions.SWITCH_THEME,
5 | meta: {
6 | throttle: 1000,
7 | analytics: 'Theme Change'
8 | }
9 | });
10 |
11 | export const startNetwork = (payload = 'global') => ({
12 | type: actions.START_NETWORK,
13 | payload
14 | });
15 |
16 | export const endNetwork = (payload = 'global') => ({
17 | type: actions.END_NETWORK,
18 | payload
19 | });
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Books from './Books';
3 | import Authors from './Authors';
4 | import Footer from './Footer';
5 |
6 | const App = () => (
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default App;
--------------------------------------------------------------------------------
/src/components/Authors.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { fetchAuthors } from 'actions/authors';
5 | import PanelTitle from './common/PanelTitle';
6 | import List from 'components/common/List';
7 | import { getRequests } from 'reducers/ui';
8 |
9 | const Authors = ({ authors, fetchAuthors, pending }) => (
10 |
11 |
12 |
13 |
{ item.name } }
16 | />
17 |
18 | );
19 |
20 | Authors.propTypes = {
21 | authors: PropTypes.array,
22 | fetchAuthors: PropTypes.func.isRequired,
23 | pending: PropTypes.bool.isRequired
24 | };
25 |
26 | const mapStateToProps = state => ({
27 | authors: Object.keys(state.authors).map(id => state.authors[id]),
28 | pending: getRequests(state, 'authors') > 0
29 | });
30 |
31 | export default connect(mapStateToProps, { fetchAuthors })(Authors);
--------------------------------------------------------------------------------
/src/components/Book.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | const Book = ({ book, author }) => (
6 |
7 | { book.title }
8 | { author.name }
9 |
10 | );
11 |
12 | Book.propTypes = {
13 | book: PropTypes.object.isRequired,
14 | author: PropTypes.object.isRequired
15 | };
16 |
17 | const mapStateToProps = (state, ownProps) => ({
18 | author: state.authors[ownProps.book.author]
19 | });
20 |
21 | export default connect(mapStateToProps)(Book);
--------------------------------------------------------------------------------
/src/components/Books.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { fetchBooks } from 'actions/books';
5 | import PanelTitle from './common/PanelTitle';
6 | import Book from './Book';
7 | import List from 'components/common/List';
8 | import { getRequests } from 'reducers/ui';
9 |
10 | const Books = ({ books, fetchBooks, pending }) => (
11 |
12 |
13 |
14 |
}
17 | />
18 |
19 | );
20 |
21 | Books.propTypes = {
22 | books: PropTypes.array,
23 | fetchBooks: PropTypes.func.isRequired,
24 | pending: PropTypes.bool.isRequired
25 | };
26 |
27 | const mapStateToProps = state => ({
28 | books: Object.keys(state.books).map(id => state.books[id]),
29 | pending: getRequests(state, 'books') > 0
30 | });
31 |
32 | export default connect(mapStateToProps, { fetchBooks })(Books);
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ThemeManager from './ThemeManager';
3 |
4 | const Footer = () => (
5 |
9 | );
10 |
11 | Footer.propTypes = {
12 | };
13 |
14 | export default Footer;
--------------------------------------------------------------------------------
/src/components/ThemeManager.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { switchTheme } from 'actions/ui';
4 | import { connect } from 'react-redux';
5 |
6 | const ThemeManager = ({ switchTheme }) => (
7 |
8 |
9 |
10 | );
11 |
12 | ThemeManager.propTypes = {
13 | switchTheme: PropTypes.func.isRequired
14 | };
15 |
16 | export default connect(null, { switchTheme })(ThemeManager);
--------------------------------------------------------------------------------
/src/components/common/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Spinner from './Spinner';
4 |
5 | const List = ({ list, render, pending = false }) => {
6 | if (pending) {
7 | return
8 | }
9 |
10 | return (
11 |
12 | {
13 | list && list.length > 0
14 | ? list.map(render)
15 | : - No items
16 | }
17 |
18 | )
19 | };
20 |
21 | List.propTypes = {
22 | list: PropTypes.array.isRequired,
23 | render: PropTypes.func.isRequired,
24 | pending: PropTypes.bool
25 | };
26 |
27 | export default List;
--------------------------------------------------------------------------------
/src/components/common/PanelTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { getColor } from 'reducers/ui';
5 |
6 | const PanelTitle = ({ title, callback, color }) => (
7 |
8 | { title }
9 | {
10 | callback
11 | ?
12 | : null
13 | }
14 |
15 | );
16 |
17 | PanelTitle.propTypes = {
18 | title: PropTypes.string.isRequired,
19 | callback: PropTypes.func,
20 | color: PropTypes.string.isRequired
21 | };
22 |
23 | const mapStateToProps = state => ({
24 | color: getColor(state)
25 | });
26 |
27 | export default connect(mapStateToProps)(PanelTitle);
--------------------------------------------------------------------------------
/src/components/common/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Spinner = () => (
4 |
5 |

6 |
7 | );
8 |
9 | export default Spinner;
--------------------------------------------------------------------------------
/src/consts/action-types.js:
--------------------------------------------------------------------------------
1 | export const API = 'API';
2 | export const SET_AUTHORS = 'SET_AUTHORS';
3 | export const SET_BOOKS = 'SET_BOOKS';
4 | export const START_NETWORK = 'START_NETWORK';
5 | export const END_NETWORK = 'END_NETWORK';
6 | export const SWITCH_THEME = 'SWITCH_THEME';
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
7 | .title {
8 | background-color: #e30d40;
9 | color: white;
10 | padding: 3px 10px;
11 | position: relative;
12 | }
13 |
14 | .title button {
15 | position: absolute;
16 | right: 5px;
17 | border: none;
18 | border-radius: 3px;
19 | background-color: white;
20 | padding: 3px 5px;
21 | margin: 0;
22 | cursor: pointer;
23 | }
24 |
25 | .title button:hover {
26 | background-color: #ccc;
27 | }
28 |
29 | footer {
30 | position: absolute;
31 | bottom: 30px;
32 | font-size: 10px;
33 | text-align: center;
34 | width: 100%;
35 | }
36 |
37 | .panel ul {
38 | padding-left: 20px;
39 | }
40 |
41 | .panel ul li {
42 | list-style: none;
43 | padding: 3px 8px;
44 | margin-right: 10%;
45 | border-bottom: 1px dotted #eee;
46 | }
47 |
48 | .panel ul li:hover {
49 | background-color: #eee;
50 | cursor: pointer;
51 | }
52 |
53 | .panel ul li .author {
54 | margin: 3px 10px;
55 | text-transform: uppercase;
56 | font-size: 9px;
57 | font-style: normal;
58 | }
59 |
60 | .panel ul li.empty {
61 | font-size: 11px;
62 | border: none;
63 | }
64 |
65 | .spinner {
66 | margin: auto;
67 | text-align: center;
68 | }
69 |
70 | .theme button {
71 | border: 2px solid #666;
72 | border-radius: 3px;
73 | background-color: white;
74 | font-size: 8px;
75 | margin: 10px;
76 | cursor: pointer;
77 | }
78 |
79 | .theme button:hover {
80 | background-color: #eee;
81 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import 'index.css';
4 | import { Provider } from 'react-redux';
5 | import store from 'store';
6 | import 'mimic';
7 |
8 | import App from 'components/App';
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/src/lib/schema.js:
--------------------------------------------------------------------------------
1 | import { schema } from 'normalizr';
2 |
3 | export const authors = new schema.Entity('authors');
4 | export const books = new schema.Entity('books', { author: authors });
5 |
--------------------------------------------------------------------------------
/src/middleware/api.js:
--------------------------------------------------------------------------------
1 | import * as actions from 'consts/action-types';
2 | import { normalize } from 'normalizr';
3 | import { startNetwork, endNetwork } from 'actions/ui';
4 |
5 | const api = ({ dispatch, getState }) => next => action => {
6 |
7 | if (action.type !== actions.API) {
8 | return next(action);
9 | }
10 |
11 | const { url, success, schema, label } = action.payload;
12 |
13 | dispatch(startNetwork(label));
14 |
15 | fetch(url)
16 | .then(response => response.json())
17 | .then(data => {
18 | if (schema) {
19 | data = normalize(data, schema);
20 | }
21 |
22 | dispatch(success(data));
23 |
24 | dispatch(endNetwork(label));
25 | })
26 | .catch(error => {
27 | console.error(error);
28 | dispatch(endNetwork(label))
29 | })
30 | };
31 |
32 | export default api;
33 |
--------------------------------------------------------------------------------
/src/middleware/log.js:
--------------------------------------------------------------------------------
1 | const log = ({ getState, dispatch }) => next => action => {
2 |
3 | console.log('ACTION: ' + action.type, action);
4 |
5 | next(action);
6 | };
7 |
8 | export default log;
--------------------------------------------------------------------------------
/src/middleware/multi.js:
--------------------------------------------------------------------------------
1 | const multi = ({ dispatch }) => next => action => {
2 | if (!Array.isArray(action)) {
3 | return next(action);
4 | }
5 |
6 | action.forEach(item => dispatch(item));
7 | };
8 |
9 | export default multi;
--------------------------------------------------------------------------------
/src/middleware/throttle.js:
--------------------------------------------------------------------------------
1 | const throttled = {};
2 |
3 | const middleware = () => next => action => {
4 | const time = action.meta && action.meta.throttle;
5 |
6 | if (!time) {
7 | return next(action);
8 | }
9 |
10 | // Just ignore the action if its already throttled
11 | if (throttled[action.type]) {
12 | return;
13 | }
14 |
15 | throttled[action.type] = true;
16 |
17 | setTimeout(
18 | () => throttled[action.type] = false,
19 | time
20 | );
21 |
22 | return next(action);
23 | };
24 |
25 | export default middleware;
26 |
--------------------------------------------------------------------------------
/src/reducers/authors.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import Immutable from 'seamless-immutable';
3 | import * as actions from 'consts/action-types';
4 |
5 | const initialState = Immutable({});
6 |
7 | export default handleActions({
8 | [actions.SET_AUTHORS]: (authors, { payload }) => authors.merge(payload)
9 | }, initialState);
--------------------------------------------------------------------------------
/src/reducers/books.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import Immutable from 'seamless-immutable';
3 | import * as actions from 'consts/action-types';
4 |
5 | const initialState = Immutable({});
6 |
7 | export default handleActions({
8 | [actions.SET_BOOKS]: (books, { payload }) => books.merge(Immutable(payload))
9 | }, initialState);
--------------------------------------------------------------------------------
/src/reducers/root.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import authors from './authors';
3 | import books from './books';
4 | import ui from './ui';
5 |
6 | export default combineReducers({
7 | authors,
8 | books,
9 | ui
10 | });
--------------------------------------------------------------------------------
/src/reducers/ui.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import Immutable from 'seamless-immutable';
3 | import * as actions from 'consts/action-types';
4 |
5 | const COLORS = ['#e30d40', '#0d75e3', '#e35f0d', '#3fe30d'];
6 |
7 | const initialState = Immutable({
8 | requests: {},
9 | theme: 0
10 | });
11 |
12 | export default handleActions({
13 | [actions.SWITCH_THEME]: (ui) => ui.update('theme', curr => (curr + 1) % COLORS.length),
14 | [actions.START_NETWORK]: (ui, { payload = 'global' }) => ui.updateIn(['requests', payload], counter => (counter || 0) + 1),
15 | [actions.END_NETWORK]: (ui, { payload = 'global' }) => ui.updateIn(['requests', payload], counter => (counter || 0) - 1)
16 | }, initialState);
17 |
18 | export const getRequests = (state, label = 'global') => state.ui.getIn(['requests', label]) || 0;
19 | export const getColor = (state) => COLORS[state.ui.theme];
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import reducer from 'reducers/root';
3 | import ReduxThunk from 'redux-thunk'
4 | import multi from 'middleware/multi';
5 | import throttle from 'middleware/throttle';
6 | import log from 'middleware/log';
7 | import api from 'middleware/api';
8 |
9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
10 |
11 | const store = createStore(reducer, composeEnhancers(applyMiddleware(
12 | ReduxThunk,
13 | multi,
14 | throttle,
15 | log,
16 | api
17 | )));
18 |
19 | window.store = store;
20 |
21 | export default store;
--------------------------------------------------------------------------------