diff --git a/spikes/list/react-infinite/.babelrc b/spikes/list/react-infinite/.babelrc new file mode 100644 index 00000000..88341a1d --- /dev/null +++ b/spikes/list/react-infinite/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceMaps": "both", + "presets": [ + "react", + "es2015" + ], + "plugins": [ + "react-hot-loader/babel", + "add-module-exports", + "syntax-async-functions", + ["transform-object-rest-spread", { + "useBuiltIns": true + }] + ] +} diff --git a/spikes/list/react-infinite/package.json b/spikes/list/react-infinite/package.json new file mode 100644 index 00000000..fff5aa4e --- /dev/null +++ b/spikes/list/react-infinite/package.json @@ -0,0 +1,34 @@ +{ + "name": "react-infinite-spike", + "private": true, + "license": "private", + "scripts": { + "start": "webpack-dev-server --config webpack/index.js" + }, + "dependencies": { + "build-array": "^1.0.0", + "delay": "^1.3.1", + "lodash.debounce": "^4.0.8", + "react": "^15.4.0", + "react-dom": "^15.4.0", + "react-hot-loader": "^3.0.0-beta.6", + "react-infinite": "^0.10.0", + "react-redux": "^4.4.6", + "redux": "^3.6.0", + "redux-logger": "^2.7.4", + "redux-promise-middleware": "^4.1.0", + "redux-thunk": "^2.1.0" + }, + "devDependencies": { + "babel-core": "^6.18.2", + "babel-eslint": "^7.1.1", + "babel-loader": "^6.2.7", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-syntax-async-functions": "^6.13.0", + "babel-plugin-transform-object-rest-spread": "^6.19.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "webpack": "^2.1.0-beta.27", + "webpack-dev-server": "^1.16.2" + } +} diff --git a/spikes/list/react-infinite/readme.md b/spikes/list/react-infinite/readme.md new file mode 100644 index 00000000..fc25cb0e --- /dev/null +++ b/spikes/list/react-infinite/readme.md @@ -0,0 +1,6 @@ +# react-infinite + + - not fetching when scrolling to the end of a big list + - it's keeping the location when we filter (kinda) + - tried with 100000 rows and it scrolled flawlessly, but it struggled filtering (I assume that the filter itself it the reason) + - because the calculations are based on a fixed container height, we might need to figure out how to handle a responsive ui where the height changes \ No newline at end of file diff --git a/spikes/list/react-infinite/src/actions.js b/spikes/list/react-infinite/src/actions.js new file mode 100644 index 00000000..a72ba725 --- /dev/null +++ b/spikes/list/react-infinite/src/actions.js @@ -0,0 +1,88 @@ +const buildArray = require('build-array'); +const delay = require('delay'); + +const actions = { + 'FETCH_FULFILLED': (state, action) => { + return { + ...state, + fetching: false, + items: (state.items || []).concat(action.payload) + }; + }, + 'FETCH_PENDING': (state, action) => { + return { + ...state, + fetching: true + }; + }, + 'FILTER_PENDING': (state, action) => { + return { + ...state, + fetching: true + }; + }, + 'FILTER_FULFILLED': (state, action) => { + return { + ...state, + fetching: false, + filtered: action.payload.length !== state.items.length + ? action.payload + : null + }; + }, +}; + +const fetch = () => (dispatch, getState) => { + const { + filtered + } = getState(); + + if (filtered) { + return; + } + + return dispatch({ + type: 'FETCH', + payload: delay(500).then(() => { + const { + items = [] + } = getState(); + + return buildArray(100000).map((v, i) => { + const id = items.length + i; + + return { + id, + title: `test ${id}` + }; + }); + }) + }); +}; + +const filter = (payload) => (dispatch, getState) => { + const regexp = new RegExp(payload); + + return dispatch({ + type: 'FILTER', + payload: delay(500).then(() => { + const { + items = [] + } = getState(); + + return items.filter((item) => { + return regexp.test(item.title); + }).sort((a, b) => a.id - b.id); + }) + }); +}; + +module.exports = (state, action) => { + return actions[action.type] + ? actions[action.type](state, action) + : state; +}; + +module.exports.fetch = fetch; +module.exports.filter = filter; + diff --git a/spikes/list/react-infinite/src/index.js b/spikes/list/react-infinite/src/index.js new file mode 100644 index 00000000..ad8139d9 --- /dev/null +++ b/spikes/list/react-infinite/src/index.js @@ -0,0 +1,18 @@ +const Store = require('./store'); +const ReactDOM = require('react-dom'); +const React = require('react'); + +const render = () => { + const Root = require('./root'); + + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +render(); + +if (module.hot) { + module.hot.accept('./root', render); +} diff --git a/spikes/list/react-infinite/src/list.js b/spikes/list/react-infinite/src/list.js new file mode 100644 index 00000000..e5e6f151 --- /dev/null +++ b/spikes/list/react-infinite/src/list.js @@ -0,0 +1,76 @@ +const debounce = require('lodash.debounce'); +const ReactRedux = require('react-redux'); +const Infinite = require('react-infinite'); +const actions = require('./actions'); +const React = require('react'); + +const { + connect +} = ReactRedux; + +const { + fetch, + filter +} = actions; + +const mapStateToProps = (state) => { + return state; +}; + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + fetch: () => { + return dispatch(fetch()); + }, + filter: (payload) => { + return dispatch(filter(payload)); + } + } +}; + +const List = ({ + items = [], + filtered, + input = '', + fetching = false, + fetch, + filter +}) => { + const _items = (filtered || items).map((item) => { + return ( +
+ {item.title} +
+ ); + }); + + const _loading = ( +
+ Loading... +
+ ); + + const _filter = debounce(filter, 100); + const onChange = (ev) => _filter(ev.target.value); + + return ( +
+ + + {_items} + +
+ ); +}; + +module.exports = connect( + mapStateToProps, + mapDispatchToProps, +)(List); diff --git a/spikes/list/react-infinite/src/root.js b/spikes/list/react-infinite/src/root.js new file mode 100644 index 00000000..ac846b6f --- /dev/null +++ b/spikes/list/react-infinite/src/root.js @@ -0,0 +1,24 @@ +const ReactRedux = require('react-redux'); +const ReactHotLoader = require('react-hot-loader'); +const React = require('react'); +const List = require('./list'); + +const { + AppContainer +} = ReactHotLoader; + +const { + Provider +} = ReactRedux; + +module.exports = ({ + store +}) => { + return ( + + + + + + ); +}; diff --git a/spikes/list/react-infinite/src/store.js b/spikes/list/react-infinite/src/store.js new file mode 100644 index 00000000..a844c912 --- /dev/null +++ b/spikes/list/react-infinite/src/store.js @@ -0,0 +1,19 @@ +const createLogger = require('redux-logger'); +const promiseMiddleware = require('redux-promise-middleware').default; +const thunk = require('redux-thunk').default; +const redux = require('redux'); +const reducer = require('./actions'); + +const { + createStore, + compose, + applyMiddleware +} = redux; + +module.exports = (state = Object.freeze({})) => { + return createStore(reducer, state, applyMiddleware( + createLogger(), + promiseMiddleware(), + thunk + )); +}; \ No newline at end of file diff --git a/spikes/list/react-infinite/static/.gitignore b/spikes/list/react-infinite/static/.gitignore new file mode 100644 index 00000000..d895d0f1 --- /dev/null +++ b/spikes/list/react-infinite/static/.gitignore @@ -0,0 +1,7 @@ +* +!.gitignore +!.gitkeep +!index.html + +js/* +!js/.gitkeep diff --git a/spikes/list/react-infinite/static/index.html b/spikes/list/react-infinite/static/index.html new file mode 100644 index 00000000..4b7d9e57 --- /dev/null +++ b/spikes/list/react-infinite/static/index.html @@ -0,0 +1,10 @@ + + + + Infinite List + + +
+ + + diff --git a/spikes/list/react-infinite/webpack/base.js b/spikes/list/react-infinite/webpack/base.js new file mode 100644 index 00000000..20849c9b --- /dev/null +++ b/spikes/list/react-infinite/webpack/base.js @@ -0,0 +1,30 @@ +const webpack = require('webpack'); +const path = require('path'); + +const plugins = { + 'no-errors-plugin': new webpack.NoErrorsPlugin(), +}; + +exports.config = { + context: path.join(__dirname, '../'), + output: { + path: path.join(__dirname, '../static'), + publicPath: '/', + filename: '[name].js' + }, + plugins: [ + new webpack.NoErrorsPlugin() + ], + module: { + loaders: [{ + test: /js?$/, + exclude: /node_modules/, + include: [ + path.join(__dirname, '../src') + ], + loader: 'babel-loader' + }] + } +}; + +exports.plugins = plugins; diff --git a/spikes/list/react-infinite/webpack/index.js b/spikes/list/react-infinite/webpack/index.js new file mode 100644 index 00000000..51776065 --- /dev/null +++ b/spikes/list/react-infinite/webpack/index.js @@ -0,0 +1,29 @@ +const base = require('./base.js'); +const webpack = require('webpack'); +const path = require('path'); + +const devServer = { + contentBase: [ + path.join(__dirname, '../static/') + ], + hot: true, + compress: true, + lazy: false, + historyApiFallback: { + index: './index.html' + } +}; + +module.exports = Object.assign(base.config, { + entry: [ + 'react-hot-loader/patch', + 'webpack-dev-server/client?http://localhost:8080', + 'webpack/hot/only-dev-server', + './src/index.js' + ], + plugins: base.config.plugins.concat([ + new webpack.HotModuleReplacementPlugin() + ]), + devtool: 'source-map', + devServer +});