├── .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 |
9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
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 | 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 | Spinner 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; --------------------------------------------------------------------------------