├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── .eslintrc.js └── config.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── lector-react-redux │ ├── README.md │ ├── __tests__ │ │ └── index.js │ ├── lib │ │ └── index.js │ └── package.json ├── lector-redux │ ├── README.md │ ├── __tests__ │ │ └── index.js │ ├── lib │ │ └── index.js │ └── package.json └── lector │ ├── README.md │ ├── __tests__ │ ├── coroutine.js │ └── index.js │ ├── doc │ ├── react-redux-integration.md │ └── tutorial.md │ ├── lib │ └── index.js │ └── package.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | jest: true 8 | }, 9 | parserOptions: { 10 | ecmaVersion: 2017, 11 | sourceType: "module" 12 | }, 13 | plugins: ["prettier"], 14 | extends: ["eslint:recommended", "prettier"], 15 | rules: { 16 | "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 17 | "require-yield": "off", 18 | "prettier/prettier": "error" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | node_modules 3 | lerna-debug.log 4 | dist 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - node 5 | 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | - First version of the library. Basic `Reader` class and methods 12 | - Helper method `prop` to get a property of a reader 13 | - A tutorial. 14 | - Documentation about how to integrate with react and redux. 15 | - `Reader.coroutine` function 16 | 17 | [Unreleased]: https://github.com/davazp/lector/compare/v0.1.0...HEAD 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 David Vázquez Púa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lector 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/davazp/lector.svg?branch=master)](https://travis-ci.org/davazp/lector) 5 | [![npm version](https://badge.fury.io/js/lector.svg)](https://badge.fury.io/js/lector) 6 | 7 | *lector* is a library to deal with *readers*: computations with access 8 | to some read-only context. 9 | 10 | **This package is experimental and it is under active development. Expect backward-incompatible changes**. 11 | 12 | ## [Introduction to readers](./packages/lector/doc/tutorial.md) 13 | ## [Using lector with React and Redux](./packages/lector/doc/react-redux-integration.md) 14 | 15 | ## Overview 16 | 17 | You define *readers* by chaining them with other readers 18 | 19 | ```javascript 20 | import { ask, coroutine } from "lector"; 21 | 22 | const getVersion = ask.chain(context => context.version); 23 | 24 | const f = coroutine(function*() { 25 | const version = yield getVersion; 26 | 27 | if (version === 1) { 28 | console.log("hello"); 29 | } else { 30 | console.log("bye"); 31 | } 32 | 33 | return version; 34 | }); 35 | ``` 36 | 37 | The `ask` reader is a built-in reader that just returns the whole 38 | context. You can define a derived reader by calling `.chain`, which 39 | will be called with the return value of the previous reader. 40 | 41 | If you return another *Reader*, the resolved value of those will be 42 | passed to the next reader. 43 | 44 | Finally, you can provide the context to the function at the top of 45 | your stack: 46 | 47 | ```javascript 48 | f().run({version: 2}) 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "no-console": "off" 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /examples/config.js: -------------------------------------------------------------------------------- 1 | const { ask, coroutine } = require("lector"); 2 | 3 | // Basic context 4 | const config = ask.prop("config"); 5 | 6 | // Abstract selectors out of the general context 7 | const userPreferences = config.chain(c => { 8 | return Object.assign({ language: "en" }, c); 9 | }); 10 | 11 | const userLanguage = userPreferences.prop("language"); 12 | 13 | // You can also create functions with arguments 14 | const translate = key => { 15 | const i18n = { 16 | en: { hello: "hello", bye: "bye" }, 17 | es: { hello: "hola", bye: "adios" } 18 | }; 19 | return userLanguage.chain(lang => i18n[lang][key]); 20 | }; 21 | 22 | const main = coroutine(function*() { 23 | console.log(yield translate("hello"), "!"); 24 | console.log(yield translate("bye"), "!"); 25 | }); 26 | 27 | // User your functions by providing the context once 28 | 29 | const _context1 = { 30 | // No language set, should use the default 31 | }; 32 | const _context2 = { 33 | config: { language: "es" } 34 | }; 35 | 36 | const test = () => { 37 | main().run(_context1); 38 | main().run(_context2); 39 | }; 40 | 41 | test(); 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | collectCoverage: true, 4 | collectCoverageFrom: ["packages/**/*.js"], 5 | coveragePathIgnorePatterns: ["/node_modules/", "/dist/"], 6 | // NOTE: As of 20171205, this seems necessary or jest will find 7 | // duplicated package lector, both in packages/ as the toplevel 8 | // one. This seems a bug, as the main package has actually no name. 9 | modulePathIgnorePatterns: ["/package.json"] 10 | }; 11 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.5.1", 3 | "packages": ["packages/*"], 4 | "version": "0.2.1", 5 | "npmClient": "yarn", 6 | "useWorkspaces": true 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "lerna run build", 5 | "prepare": "yarn build", 6 | "lint": "eslint .", 7 | "test": "yarn lint && jest", 8 | "precommit": "lint-staged" 9 | }, 10 | "lint-staged": { 11 | "*.{js,json,css}": ["prettier --write", "git add"] 12 | }, 13 | "workspaces": ["packages/*"], 14 | "devDependencies": { 15 | "babel-cli": "^6.26.0", 16 | "babel-preset-env": "^1.6.1", 17 | "bluebird": "^3.5.1", 18 | "eslint": "^4.12.0", 19 | "eslint-config-prettier": "2.9.0", 20 | "eslint-plugin-prettier": "2.3.1", 21 | "husky": "^0.14.3", 22 | "jest": "^21.2.1", 23 | "lerna": "^2.5.1", 24 | "lint-staged": "^6.0.0", 25 | "prettier": "1.9.0", 26 | "react": "^16.2.0", 27 | "react-dom": "^16.2.0", 28 | "react-redux": "^5.0.6", 29 | "redux": "^3.7.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/lector-react-redux/README.md: -------------------------------------------------------------------------------- 1 | lector + React Redux 2 | ========================== 3 | 4 | ```shell 5 | npm install lector-react-redux 6 | ``` 7 | 8 | -------------------------------------------------------------------------------- /packages/lector-react-redux/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMServer from "react-dom/server"; 3 | 4 | import { createStore } from "redux"; 5 | import { Provider } from "react-redux"; 6 | 7 | import Reader from "lector"; 8 | import { getState } from "lector-redux"; 9 | import { connectReaders } from "../lib"; 10 | 11 | // Render Component within a Provider with a specific store and return 12 | // the rendered static markup. 13 | function renderTest(store, Component) { 14 | const element = React.createElement( 15 | Provider, 16 | { store }, 17 | React.createElement(Component, {}) 18 | ); 19 | return ReactDOMServer.renderToStaticMarkup(element); 20 | } 21 | 22 | test("connectReaders returns a function", () => { 23 | expect(connectReaders({})).toBeInstanceOf(Function); 24 | }); 25 | 26 | test("connectReaders won't change literal objects", () => { 27 | const store = createStore(x => x, {}); 28 | 29 | const enhance = connectReaders({ x: 10, y: 20 }); 30 | const Component = enhance(({ x }) => { 31 | return React.createElement("span", {}, x); 32 | }); 33 | 34 | const html = renderTest(store, Component); 35 | 36 | expect(html).toBe("10"); 37 | }); 38 | 39 | test("can call a function creating reader", () => { 40 | const store = createStore(x => x, {}); 41 | 42 | const f = () => Reader.of(5); 43 | 44 | const enhance = connectReaders({ f }); 45 | const Component = enhance(({ f }) => { 46 | return React.createElement("span", {}, f()); 47 | }); 48 | 49 | const html = renderTest(store, Component); 50 | 51 | expect(html).toBe("5"); 52 | }); 53 | 54 | test("connected readers can access the state", () => { 55 | const store = createStore(x => x, { x: 10 }); 56 | 57 | const f = () => getState.prop("x"); 58 | 59 | const enhance = connectReaders({ f }); 60 | const Component = enhance(({ f }) => { 61 | return React.createElement("span", {}, f()); 62 | }); 63 | 64 | const html = renderTest(store, Component); 65 | 66 | expect(html).toBe("10"); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/lector-react-redux/lib/index.js: -------------------------------------------------------------------------------- 1 | import { connectAdvanced } from "react-redux"; 2 | import Reader from "lector"; 3 | 4 | function mapValues(object, fn) { 5 | const result = {}; 6 | const keys = Object.keys(object); 7 | for (let i = 0; i < keys.length; i++) { 8 | const key = keys[i]; 9 | const value = object[key]; 10 | result[key] = fn(value); 11 | } 12 | return result; 13 | } 14 | 15 | const bindReaderFunction = (context, fn) => { 16 | return function() { 17 | const reader = Reader.of(fn.apply(null, arguments)); 18 | return reader.run(context); 19 | }; 20 | }; 21 | 22 | const readerSelectorsFactory = object => dispatch => state => { 23 | return mapValues(object, value => { 24 | const context = { state, dispatch }; 25 | if (value instanceof Function) { 26 | return bindReaderFunction(context, value); 27 | } else { 28 | return value; 29 | } 30 | }); 31 | }; 32 | 33 | function connectReaders(object) { 34 | return connectAdvanced(readerSelectorsFactory(object)); 35 | } 36 | 37 | export { connectReaders }; 38 | -------------------------------------------------------------------------------- /packages/lector-react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lector-react-redux", 3 | "version": "0.2.1", 4 | "main": "dist/index.js", 5 | "module": "lib/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "babel lib/ -d dist/", 9 | "prepare": "npm run build" 10 | }, 11 | "peerDependencies": { 12 | "lector": "^0.2.1", 13 | "lector-redux": "^0.2.1", 14 | "react-redux": "^5.0.6" 15 | }, 16 | "author": { 17 | "name": "David Vázquez Púa", 18 | "email": "davazp@gmail.com", 19 | "url": "https://github.com/davazp/" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/lector-redux/README.md: -------------------------------------------------------------------------------- 1 | lector-redux 2 | ================== 3 | 4 | ```shell 5 | npm install lector-redux 6 | ``` 7 | 8 | -------------------------------------------------------------------------------- /packages/lector-redux/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { getState } from "../lib/"; 3 | 4 | const initialState = { a: 10, b: 20 }; 5 | 6 | function reducer(state, action) { 7 | switch (action.type) { 8 | case "INCa": 9 | return Object.assign({}, state, { a: state.a + 1 }); 10 | case "DECa": 11 | return Object.assign({}, state, { a: state.a - 1 }); 12 | default: 13 | return state; 14 | } 15 | } 16 | 17 | test("getState reader resolves to the state", () => { 18 | const store = createStore(reducer, initialState); 19 | const context = { state: store.getState(), dispatch: store.dispatch }; 20 | return expect(getState.run(context)).toBe(initialState); 21 | }); 22 | 23 | // test("dispatch returns a reader that will dispatch the action", async () => { 24 | // const store = createStore(reducer, initialState); 25 | // await dispatch({ type: "INCa" }) 26 | // .then(() => getState) 27 | // .then(state => { 28 | // expect(state).toEqual({ a: 11, b: 20 }); 29 | // }) 30 | // .run(store); 31 | // }); 32 | -------------------------------------------------------------------------------- /packages/lector-redux/lib/index.js: -------------------------------------------------------------------------------- 1 | import { ask } from "lector"; 2 | 3 | const getState = ask.prop("state"); 4 | const getDispatch = ask.prop("dispatch"); 5 | 6 | function dispatch(action) { 7 | return getDispatch.then(dispatch => dispatch(action)); 8 | } 9 | 10 | export { getState, dispatch }; 11 | -------------------------------------------------------------------------------- /packages/lector-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lector-redux", 3 | "version": "0.2.1", 4 | "description": "Integrate lector with Redux", 5 | "main": "dist/index.js", 6 | "module": "lib/index.js", 7 | "license": "MIT", 8 | "keywords": ["lector", "redux", "context"], 9 | "scripts": { 10 | "build": "babel lib/ -d dist/", 11 | "prepare": "npm run build" 12 | }, 13 | "peerDependencies": { 14 | "lector": "^0.2.1", 15 | "redux": "^3.7.2" 16 | }, 17 | "author": { 18 | "name": "David Vázquez Púa", 19 | "email": "davazp@gmail.com", 20 | "url": "https://github.com/davazp/" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/lector/README.md: -------------------------------------------------------------------------------- 1 | lector 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/davazp/lector.svg?branch=master)](https://travis-ci.org/davazp/lector) 5 | 6 | lector is a library to deal with asynchronous *readers*: 7 | computations with access to some read-only context. 8 | 9 | **This package is experimental and it is under active development. Expect backward-incompatible changes**. 10 | 11 | ## [Introduction to lectors](./doc/tutorial.md) 12 | ## [Using lector with React and Redux](./doc/react-redux-integration.md) 13 | 14 | ## Installation 15 | 16 | You can install this package with 17 | 18 | ```shell 19 | npm install lector 20 | ``` 21 | 22 | ## Overview 23 | 24 | You define *readers* by chaining them with other readers 25 | 26 | ```javascript 27 | import { ask, coroutine } from "lector"; 28 | 29 | const getVersion = ask.chain(context => context.version); 30 | 31 | const f = coroutine(function*() { 32 | const version = yield getVersion; 33 | 34 | if (version === 1) { 35 | console.log("hello"); 36 | } else { 37 | console.log("bye"); 38 | } 39 | 40 | return version; 41 | }); 42 | ``` 43 | 44 | The `ask` reader is a built-in reader that just returns the whole 45 | context. You can define a derived reader by calling `.chain`, which 46 | will be called with the return value of the previous reader. 47 | 48 | If you return a *Promise* or another *Reader*, the resolved value of 49 | those will be passed to the next reader. 50 | 51 | Finally, you can provide the context to the function at the top of 52 | your stack: 53 | 54 | ```javascript 55 | f().run({version: 2}) 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/lector/__tests__/coroutine.js: -------------------------------------------------------------------------------- 1 | import Promise from "bluebird"; 2 | import Reader, { ask, coroutine } from "../lib"; 3 | 4 | test("it builds a proper reader", () => { 5 | const r = Reader.do(function*() { 6 | return 10; 7 | }); 8 | expect(r).toBeInstanceOf(Reader); 9 | }); 10 | 11 | test("reader will resolved to the returned value from the generator function function will be resolved automatically", async () => { 12 | const r = Reader.do(function*() { 13 | return Promise.resolve(20); 14 | }); 15 | const result = await r.run(); 16 | return expect(result).toBe(20); 17 | }); 18 | 19 | test("returned readers in the generator function will be resolved automatically", () => { 20 | const r = Reader.do(function*() { 21 | return Reader.of("lisp"); 22 | }); 23 | const result = r.run(); 24 | return expect(result).toBe("lisp"); 25 | }); 26 | 27 | test("yield returns the resolved value of the context", () => { 28 | const r = Reader.do(function*() { 29 | const ctx = yield ask; 30 | return ctx * ctx; 31 | }); 32 | const result = r.run(10); 33 | return expect(result).toBe(100); 34 | }); 35 | 36 | test("can yield a reader created with Reader.of", () => { 37 | const r = Reader.do(function*() { 38 | const value = yield Reader.of(3); 39 | return value; 40 | }); 41 | const result = r.run(); 42 | return expect(result).toBe(3); 43 | }); 44 | 45 | test("yield can integrate with promises", async () => { 46 | function getValue() { 47 | return Promise.resolve(10); 48 | } 49 | 50 | const r = Reader.do(function*() { 51 | const ctx = yield ask; 52 | return getValue().then(value => ctx * value); 53 | }); 54 | 55 | const result = await r.run(10); 56 | return expect(result).toBe(100); 57 | }); 58 | 59 | test("errors in the generator function will throw an exception", () => { 60 | const err = new Error("foo"); 61 | 62 | const r = Reader.do(function*() { 63 | throw err; 64 | }); 65 | 66 | return expect(r.run).toThrow(err); 67 | }); 68 | 69 | test("errors in the yielded readers will throw an exception", () => { 70 | const err = new Error("foo"); 71 | 72 | function f() { 73 | return new Reader(_ => { 74 | throw err; 75 | }); 76 | } 77 | 78 | const r = Reader.do(function*() { 79 | yield f(); 80 | }); 81 | 82 | return expect(r.run).toThrow(err); 83 | }); 84 | 85 | test("Reader.do should throw an error if the argument is not a function", () => { 86 | function f() { 87 | return Reader.do(3); 88 | } 89 | expect(f).toThrow(TypeError); 90 | }); 91 | 92 | test("coroutine converts a generator function into a function creating a reader", () => { 93 | const f = coroutine(function*(name) { 94 | const context = yield ask; 95 | return context[name]; 96 | }); 97 | 98 | expect(f).toBeInstanceOf(Function); 99 | 100 | const result = f("x").run({ x: 10 }); 101 | 102 | expect(result).toBe(10); 103 | }); 104 | 105 | test("coroutine should throw an error if the argument is not a function", () => { 106 | function f() { 107 | return coroutine(3); 108 | } 109 | expect(f).toThrow(TypeError); 110 | }); 111 | -------------------------------------------------------------------------------- /packages/lector/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import Promise from "bluebird"; 2 | import Reader, { ask } from "../lib"; 3 | 4 | test("identity reader returns the context", () => { 5 | const result = ask.run({ x: 10 }); 6 | expect(result).toEqual({ x: 10 }); 7 | }); 8 | 9 | test("readers compose properly", () => { 10 | const x = ask.chain(c => c.x); 11 | const y = x.chain(x => x.y); 12 | const result = y.run({ x: { y: 2 } }); 13 | expect(result).toBe(2); 14 | }); 15 | 16 | test("readers .chain method can return other readers", () => { 17 | const r = ask.chain(c => Reader.of(c * c)); 18 | const result = r.run(10); 19 | expect(result).toBe(100); 20 | }); 21 | 22 | test("readers .prop method can access a property from the previous result", () => { 23 | const r = ask.prop("x").prop("y"); 24 | const result = r.run({ x: { y: 42 } }); 25 | expect(result).toBe(42); 26 | }); 27 | 28 | test("integrates with promises", () => { 29 | function f() { 30 | return ask.chain(x => { 31 | return Promise.delay(50).return(x * x); 32 | }); 33 | } 34 | 35 | return f() 36 | .run(10) 37 | .then(result => { 38 | expect(result).toBe(100); 39 | }); 40 | }); 41 | 42 | test(".then will throw an error if the argument is not a function", () => { 43 | function f() { 44 | return ask.then(23); 45 | } 46 | expect(f).toThrow(TypeError); 47 | }); 48 | 49 | test(".then will throw an error when evaluated if the argument is not a promise", () => { 50 | const r = ask.then(() => 23); 51 | expect(r.run).toThrow(TypeError); 52 | }); 53 | 54 | test(".then will return a reader evaluating to chained promise", () => { 55 | function f() { 56 | return ask.prop("ready").then(x => x + 1); 57 | } 58 | return f() 59 | .run({ 60 | ready: Promise.resolve(42) 61 | }) 62 | .then(result => { 63 | expect(result).toBe(43); 64 | }); 65 | }); 66 | 67 | test("errors in the handlers will throw an exception", () => { 68 | const err = new Error("foo"); 69 | const r = ask.chain(() => { 70 | throw err; 71 | }); 72 | expect(r.run).toThrow(err); 73 | }); 74 | 75 | test("resolve nested readers properly", () => { 76 | function f1() { 77 | return ask.chain(_ => 10); 78 | } 79 | 80 | function f2() { 81 | return ask.chain(() => f1()); 82 | } 83 | 84 | function f3() { 85 | return ask.chain(() => f2()); 86 | } 87 | 88 | return expect(f3().run()).toBe(10); 89 | }); 90 | 91 | test("new Reader throws an exception if its argument is not a function", () => { 92 | function f() { 93 | return new Reader(3); 94 | } 95 | expect(f).toThrow(TypeError); 96 | }); 97 | 98 | test(".chain throws an exception if its argument is not a function", () => { 99 | function f() { 100 | return ask.chain(3); 101 | } 102 | expect(f).toThrow(TypeError); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/lector/doc/react-redux-integration.md: -------------------------------------------------------------------------------- 1 | Using lector with React & Redux 2 | ===================================== 3 | 4 | **This document is work-in-progress** 5 | 6 | Many web applications are built using [React](https://reactjs.org/) 7 | and [Redux](https://github.com/reactjs/react-redux), an approach that 8 | encourages a functional style. Some parts of the application will be 9 | completely pure and will not depend on the state, but many others 10 | will. 11 | 12 | This introduces the contextual problem: you will have to pass 13 | explicitly the state (or parts of the state) to the functions you 14 | need, and every function that directly or indirectly calls it. 15 | 16 | The integration of Redux for react 17 | ([react-redux](https://github.com/reactjs/react-redux/blob/master/docs/api.md#provider-store)) 18 | solves this problem for *React Components*, by making the redux store 19 | available as part of the 20 | React [context](https://reactjs.org/docs/context.html), so every 21 | subcomponent can access it without having to add a property for it. 22 | 23 | However, not all the code in your application is part of a React 24 | component. You could want to access the state or dispatch an action in 25 | your API layer, but `react-redux` can't help us in this 26 | case. `lector` provides the same implicit context functionality 27 | for non-react code. 28 | 29 | 30 | The `lector` library 31 | 32 | ```javascript 33 | // lector/react 34 | 35 | import { ask } from "lector"; 36 | 37 | // The main context is the store 38 | export const store = ask; 39 | 40 | // The dispatch function defers the action until 41 | // the reader is executed with a specific store. 42 | export const dispatch = action => { 43 | return ask.chain(store => { 44 | store.dispatch(action); 45 | }); 46 | }; 47 | 48 | // The state reader 49 | export const state = store.chain(store => store.getState()); 50 | ``` 51 | 52 | 53 | Your API layer 54 | ```javascript 55 | import { state, dispatch } from "lector-redux"; 56 | 57 | const debugMode = state.prop("debugMode"); 58 | 59 | const request = coroutine(function*(uri, options) { 60 | const debug = yield debugMode; 61 | 62 | const extraHeaders = debug ? { "X-DEBUG": "true" } : {}; 63 | 64 | const effectiveOptions = { 65 | ...options, 66 | headers: { 67 | ...options.headers, 68 | ...extraHeaders 69 | } 70 | }; 71 | 72 | return fetch(uri, options); 73 | }); 74 | 75 | export const fetchProduct = coroutine(async function*(id) { 76 | const product = await request(`/api/product/{id}`); 77 | yield dispatch({ type: UPDATE_PRODUCT, payload: product }); 78 | return product; 79 | }); 80 | ``` 81 | 82 | 83 | At the very end, you will have to pass the store somehow to your API 84 | layer, of course. In React, as we mentioned, your components already 85 | have access to the store thanks to `react-redux`. We can take benefit 86 | of this to integrate your API layer into your components 87 | 88 | ```javascript 89 | import { fetchProdut } from "./api"; 90 | import { withReaders } from "lectors/react-redux"; 91 | 92 | const RefreshProduct = ({ id, fetchProduct }) => { 93 | return