共计 14463 个字符,预计需要花费 37 分钟才能阅读完成。
Redux
Redux 是 JavaScript 状态容器,提供可预测化的状态治理。除了和 React 一起用外,还反对其它界面库。它体小精悍(只有 2kB,包含依赖)。
三大准则
繁多数据源
整个利用的 state 被贮存在一棵 object tree 中,并且这个 object tree 只存在于惟一一个 store 中。
console.log(store.getState())
/* 输入
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
State 是只读的
惟一扭转 state 的办法就是触发 action,action 是一个用于形容已产生事件的一般对象。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
应用纯函数来执行批改
为了形容 action 如何扭转 state tree,你须要编写 reducers。
function visibilityFilter(state = 'SHOW_ALL', action) {switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {if (index === action.index) {return Object.assign({}, todo, {completed: true})
}
return todo
})
default:
return state
}
}
import {combineReducers, createStore} from 'redux'
let reducer = combineReducers({visibilityFilter, todos})
let store = createStore(reducer)
Action
Action 是把数据从利用传到 store 的有效载荷。它是 store 数据的惟一起源。一般来说你会 通过 store.dispatch() 将 action 传到 store。
Action 实质上是 JavaScript 一般对象。咱们约定,action 内 必须应用一个字符串类型的 type
字段来示意将要执行的动作。
// Action 创立函数
export const ADD_TODO = 'ADD_TODO';
export function addTodo(text) {return { type: ADD_TODO, text}
}
// 发动 dispatch
dispatch(addTodo(text))
Action 创立函数也能够是异步非纯函数。
Reducer
Reducers 指定了 利用状态的变动如何响应 actions 并发送到 store 的,记住 actions 只是形容了有事件产生了这一事实,并没有形容利用如何更新 state。
reducer 就是一个纯函数,接管旧的 state 和 action,返回新的 state。
(previousState, action) => newState
永远不要 在 reducer 里做这些操作:
- 批改传入参数;
- 执行有副作用的操作,如 API 申请和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
只有传入参数雷同,返回计算失去的下一个 state 就肯定雷同。没有非凡状况、没有副作用,没有 API 申请、没有变量批改,单纯执行计算。
function todoApp(state = initialState, action) {switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {visibilityFilter: action.filter})
default:
return state
}
}
留神:
- 不要间接批改 state,而是返回新对象
- 在 default 状况下返回旧的 state。遇到未知的 action 时,肯定要返回旧的 state。
Store
Store 有以下职责:
- 维持利用的 state;
- 提供 getState() 办法获取 state;
- 提供 dispatch(action) 办法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数登记监听器。
import {createStore} from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)
Middleware
在这类框架中,middleware 是指能够被嵌入在框架接管申请到产生响应过程之中的代码。
它提供的是位于 action 被发动之后,达到 reducer 之前的扩大点。 你能够利用 Redux middleware 来进行日志记录、创立解体报告、调用异步接口或者路由等等。
middleware 最优良的个性就是能够被链式组合。你能够在一个我的项目中应用多个独立的第三方 middleware。
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware, // 容许咱们 dispatch() 函数
loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
)
)
数据流
严格的单向数据流 是 Redux 架构的设计外围。这意味着利用中所有的数据都遵循雷同的生命周期,遵循上面 4 个步骤:
- 调用 store.dispatch(action)。
- Redux store 调用传入的 reducer 函数。
- 根 reducer 应该把多个子 reducer 输入合并成一个繁多的 state 树。
- Redux store 保留了根 reducer 返回的残缺 state 树。
Side Effects: 异步网络申请 、 本地读取 localStorage/Cookie 等外界操作
总结
Redux 这种单向数据流的库有很显著的优缺点
可预测性
action 创立函数 和 reducer都是纯函数
state 和 action 是简略对象
state 能够应用 immutable 长久化数据
整套流程职责十分清晰, 数据可追踪可回溯, 能很好保障我的项目稳定性
可扩展性
通过 middleware 定制 action 的解决,通过 reducer enhancer 扩大 reducer 等等
治理麻烦
redux 的我的项目通常要分 reducer, action, saga, component 等等, 开发中须要来回切换
redux-saga
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,拜访浏览器缓存等)的 library,它的指标是让副作用治理更容易,执行更高效,测试更简略,在解决故障时更容易。
redux-saga 应用了 ES6 的 Generator 性能,让异步的流程更易于读取,写入和测试。
外围术语
Effect
一个 effect 就是一个 Plain Object JavaScript 对象,蕴含一些将被 saga middleware 执行的指令。
应用 redux-saga 提供的工厂函数来创立 effect。举个例子,你能够应用 call(myfunc, 'arg1', 'arg2')
批示 middleware 调用 myfunc('arg1', 'arg2')
并将后果返回给 yield effect 的那个 Generator。
Task
一个 task 就像是一个在后盾运行的过程。在基于 redux-saga 的应用程序中,能够同时运行多个 task。通过 fork
函数来创立 task:
function* saga() {
...
const task = yield fork(otherSaga, ...args)
...
}
阻塞调用 / 非阻塞调用
阻塞调用的意思是,Saga 在 yield Effect 之后会期待其执行后果返回,后果返回后才会复原执行 Generator 中的下一个指令。
非阻塞调用的意思是,Saga 会在 yield Effect 之后立刻复原执行。
function* saga() {yield take(ACTION) // 阻塞: 将期待 action
yield call(ApiFn, ...args) // 阻塞: 将期待 ApiFn (如果 ApiFn 返回一个 Promise 的话)
yield call(otherSaga, ...args) // 阻塞: 将期待 otherSaga 完结
yield put(...) // 阻塞: 将同步发动 action (应用 Promise.then)
const task = yield fork(otherSaga, ...args) // 非阻塞: 将不会期待 otherSaga
yield cancel(task) // 非阻塞: 将立刻复原执行
// or
yield join(task) // 阻塞: 将期待 task 完结
}
Watcher/Worker
指的是一种应用两个独自的 Saga 来组织控制流的形式。
- Watcher: 监听发动的 action 并在每次接管到 action 时
fork
一个 worker。 - Worker: 解决 action 并完结它。
function* watcher() {while(true) {const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {// ... do some stuff}
Saga 辅助函数
redux-saga 提供了一些辅助函数,包装了一些外部办法,用来在一些特定的 action 被发动到 Store 时派生工作。
让咱们通过常见的 AJAX 例子来演示一下。每次点击 Fetch 按钮时,咱们发动一个 FETCH_REQUESTED
的 action。咱们想通过启动一个从服务器获取一些数据的工作,来解决这个 action。
首先咱们创立一个将执行异步 action 的工作:
import {call, put} from 'redux-saga/effects'
export function* fetchData(action) {
try {
// 发动申请
const data = yield call(Api.fetchUser, action.payload.url);
// 创立 action
yield put({type: "FETCH_SUCCEEDED", data});
} catch (error) {yield put({type: "FETCH_FAILED", error});
}
}
而后在每次 FETCH_REQUESTED
action 被发动时启动下面的工作。
import {takeEvery} from 'redux-saga'
function* watchFetchData() {yield* takeEvery('FETCH_REQUESTED', fetchData)
}
还有很多不同作用的辅助函数
- takeEvery(pattern, saga, …args)
- takeEvery(channel, saga, …args)
- takeLatest(pattern, saga, ..args)
- takeLatest(channel, saga, ..args)
- takeLeading(pattern, saga, ..args)
- takeLeading(channel, saga, ..args)
- throttle(ms, pattern, saga, ..args)
申明式 Effects
在 redux-saga
的世界里,Sagas 都用 Generator 函数实现。咱们从 Generator 里 yield 纯 JavaScript 对象以表白 Saga 逻辑。咱们称说那些对象为 Effect。Effect 是一个简略的对象,这个对象蕴含了一些给 middleware 解释执行的信息。你能够把 Effect 看作是发送给 middleware 的指令以执行某些操作
举个例子,假如咱们有一个监听 PRODUCTS_REQUESTED
action 的 Saga。每次匹配到 action,它会启动一个从服务器上获取产品列表的工作。
import {takeEvery} from 'redux-saga/effects'
import Api from './path/to/api'
function* watchFetchProducts() {yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}
function* fetchProducts() {const products = yield Api.fetch('/products')
console.log(products)
}
假如咱们想测试下面的 generator:
const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // 咱们冀望失去什么?
咱们想要查看 generator yield 的后果的第一个值。在咱们的状况里,这个值是执行 Api.fetch('/products')
这个 Promise 的后果。在测试过程中,执行真正的服务(real service)是一个既不可行也不实用的办法,所以咱们必须 模仿(mock) Api.fetch
函数。也就是说,咱们须要将实在的函数替换为一个假的,这个假的函数并不会真的发送 AJAX 申请而只会查看是否用正确的参数调用了 Api.fetch
实际上咱们须要的只是保障 fetchProducts
工作 yield 一个调用正确的函数,并且函数有着正确的参数。
相比于在 Generator 中间接调用异步函数,咱们能够仅仅 yield 一条形容函数调用的信息。也就是说,咱们将简略地 yield 一个看起来像上面这样的对象:
// Effect -> 调用 Api.fetch 函数并传递 `./products` 作为参数
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
这样的话,在测试 Generator 时,所有咱们须要做的就是,将 yield 后的对象作一个简略的 deepEqual
来查看它是否 yield 了咱们冀望的指令
出于这样的起因,redux-saga
提供了一个不一样的形式来执行异步调用。
import {call} from 'redux-saga/effects'
function* fetchProducts() {const products = yield call(Api.fetch, '/products')
// ...
}
当初咱们不立刻执行异步调用,相同,call
创立了一条形容后果的信息。就像在 Redux 里你应用 action 创立器,创立一个将被 Store 执行的、形容 action 的纯文本对象。call
创立一个纯文本对象形容函数调用。redux-saga
middleware 确保执行函数调用并在响应被 resolve 时复原 generator。
这让你能容易地测试 Generator,就算它在 Redux 环境之外。因为 call
只是一个返回纯文本对象的函数而已。
import {call} from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch,'./products')"
)
还有很多不同作用的辅助函数 Effect 创立器
- take(pattern)
- take.maybe(pattern)
- take(channel)
- take.maybe(channel)
- put(action)
- put.resolve(action)
- put(channel, action)
- call(fn, …args)
- call([context, fn], …args)
- call([context, fnName], …args)
- apply(context, fn, args)
- cps(fn, …args)
- cps([context, fn], …args)
- fork(fn, …args)
- fork([context, fn], …args)
- spawn(fn, …args)
- spawn([context, fn], …args)
- join(task)
- join(…tasks)
- cancel(task)
- cancel(…tasks)
- cancel()
- select(selector, …args)
- actionChannel(pattern, [buffer])
- flush(channel)
- cancelled()
- setContext(props)
- getContext(prop)
Dispatch Actions
假如每次保留之后,咱们想发动一些 action 告诉 Store 数据获取胜利了
//...
function* fetchProducts(dispatch)
const products = yield call(Api.fetch, '/products')
dispatch({type: 'PRODUCTS_RECEIVED', products})
}
与咱们在上一节中看到的从 Generator 外部间接调用函数,有着雷同的毛病。如果咱们想要测试 fetchProducts
接管到 AJAX 响应之后执行 dispatch,咱们还须要模仿 dispatch
函数。
咱们须要同样的申明式的解决方案。只需创立一个对象来批示 middleware 咱们须要发动一些 action,而后让 middleware 执行实在的 dispatch。这种形式咱们就能够同样的形式测试 Generator 的 dispatch:只需查看 yield 后的 Effect,并确保它蕴含正确的指令。
redux-saga 为此提供了另外一个函数 put
,这个函数用于创立 dispatch Effect。
import {call, put} from 'redux-saga/effects'
//...
function* fetchProducts() {const products = yield call(Api.fetch, '/products')
// 创立并 yield 一个 dispatch Effect
yield put({type: 'PRODUCTS_RECEIVED', products})
}
当初,咱们能够像上一节那样轻易地测试 Generator:
import {call, put} from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// 冀望一个 call 指令
assert.deepEqual(iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch,'./products')"
)
// 创立一个假的响应对象
const products = {}
// 冀望一个 dispatch 指令
assert.deepEqual(iterator.next(products).value,
put({type: 'PRODUCTS_RECEIVED', products}),
"fetchProducts should yield an Effect put({type:'PRODUCTS_RECEIVED', products})"
)
当初咱们通过 Generator 的 next
办法来将假的响应对象传递到 Generator。在 middleware 环境之外,咱们可齐全管制 Generator,通过简略地模仿后果并还原 Generator,咱们能够模仿一个实在的环境。相比于去模仿函数和窥探调用(spying calls),模仿数据要简略的多。
错误处理
咱们假如近程读取因为某些起因失败了,API 函数 Api.fetch
返回一个被回绝(rejected)的 Promise。
咱们心愿通过在 Saga 中发动 PRODUCTS_REQUEST_FAILED
action 到 Store 来解决那些谬误。
import Api from './path/to/api'
import {call, put} from 'redux-saga/effects'
// ...
function* fetchProducts() {
try {const products = yield call(Api.fetch, '/products')
yield put({type: 'PRODUCTS_RECEIVED', products})
}
catch(error) {yield put({ type: 'PRODUCTS_REQUEST_FAILED', error})
}
}
为了测试故障案例,咱们将应用 Generator 的 throw
办法。
import {call, put} from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// 冀望一个 call 指令
assert.deepEqual(iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch,'./products')"
)
// 创立一个模仿的 error 对象
const error = {}
// 冀望一个 dispatch 指令
assert.deepEqual(iterator.throw(error).value,
put({type: 'PRODUCTS_REQUEST_FAILED', error}),
"fetchProducts should yield an Effect put({type:'PRODUCTS_REQUEST_FAILED', error})"
)
咱们传递一个模仿的 error 对象给 throw
,这会引发 Generator 中断以后的执行流并执行捕捉区块(catch block)。
你也能够让你的 API 服务返回一个失常的含有谬误标识的值。例如,你能够捕获 Promise 的回绝操作,并将它们映射到一个谬误字段对象。
import Api from './path/to/api'
import {call, put} from 'redux-saga/effects'
function fetchProductsApi() {return Api.fetch('/products')
.then(response => ({ response}))
.catch(error => ({ error}))
}
function* fetchProducts() {const { response, error} = yield call(fetchProductsApi)
if (response)
yield put({type: 'PRODUCTS_RECEIVED', products: response})
else
yield put({type: 'PRODUCTS_REQUEST_FAILED', error})
}
一个登录流程例子
import {take, put, call, fork, cancel} from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
yield call(Api.storeItem, {token})
return token
} catch(error) {yield put({type: 'LOGIN_ERROR', error})
} finally {
// finally 区块执行在任何类型的实现上(失常的 return, 谬误, 或强制勾销), 返回该 generator 是否曾经被勾销
if (yield cancelled()) {// ... put special cancellation handling code here}
}
}
function* loginFlow() {while(true) {const {user, password} = yield take('LOGIN_REQUEST')
// fork return a Task object
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if(action.type === 'LOGOUT') yield cancel(task)
yield call(Api.clearItem('token'))
}
}
loginFlow
- 监听
LOGIN_REQUEST
期待发动 action - 从内部取得参数, 以 非阻塞调用 的模式执行申请
- 监听
LOGOUT
和LOGIN_ERROR
期待发动 - 如果属于
LOGOUT
则勾销下面申请 - 两种发动都会执行清理流程
authorize
调用 authorize 申请
-
胜利
- 发动
LOGIN_SUCCESS
保留数据 - 执行 Api.storeItem
- 返回 token
- 发动
- 谬误: 发动
LOGIN_ERROR
- 减少勾销逻辑
总结
- 功能强大,多种辅助函数和 API, 通过这些能够把所有业务逻辑放到 saga, 优雅而弱小, 并且放弃 Redux 的纯正
- 可测试性,能够另辟蹊跷达到功能测试的成果
- 创立简单,灵便细粒化的写法进步编写和了解门槛
Dva
dva 是基于现有利用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全副代码不到 100 行。(Inspired by elm and choo.)
Model
他最外围的是提供了 app.model
办法,用于把 reducer, initialState, action, saga 封装到一起
比方:
app.model({
namespace: 'products',
state: {list: [],
loading: false,
},
subscriptions: [function(dispatch) {dispatch({type: 'products/query'});
},
],
effects: {['products/query']: function*() {yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
reducers: {['products/query'](state) {return { ...state, loading: true,};
},
['products/query/success'](state, { payload}) {return { ...state, loading: false, list: payload};
},
},
});
在有 dva 之前,咱们通常会创立 sagas/products.js
, reducers/products.js
和 actions/products.js
,而后在这些文件之间来回切换。
数据流向
数据的扭转产生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会扭转数据的时候能够通过 dispatch
发动一个 action,
- 如果是同步行为会间接通过
Reducers
扭转State
- 如果是异步行为(副作用)会先触发
Effects
而后流向Reducers
最终扭转State
State
State 示意 Model 的状态数据,能够是 任意类型值。
操作的时候每次都要当作 不可变数据(immutable data)来看待,保障每次都是全新对象,没有援用关系,这样能力保障 State 的独立性,便于测试和追踪变动。
Action
Action 是一个一般 javascript 对象 ,它是 扭转 State 的惟一路径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所取得的数据,最终都会通过 dispatch 函数调用一个 action,从而扭转对应的数据。action 必须带有 type
属性指明具体的行为,其它字段能够自定义,如果要发动一个 action 须要应用 dispatch
函数
dispatch({type: 'add',});
dispatch 函数
dispatching function 是一个用于 触发 action 的函数,action 是扭转 State 的惟一路径,然而它只形容了一个行为,而 dipatch 能够看作是触发这个行为的形式,而 Reducer 则是形容如何扭转数据的。
dispatch({
type: 'user/add', // 如果在 model 外调用,须要增加 namespace
payload: {}, // 须要传递的信息});
Reducer
$$
type Reducer<S, A> = (state: S, action: A) => S
$$
承受两个参数:之前曾经累积运算的后果和以后要被累积的值,返回的是一个新的累积后果。
在 dva 中,reducers 聚合积攒的后果是 以后 model 的 state 对象。通过 actions 中传入的值,与以后 reducers 中的值进行运算取得新的值。须要留神的是 Reducer 必须是纯函数,所以同样的输出必然失去同样的输入,它们不应该产生任何副作用。并且,每一次的计算都应该应用immutable data,这种个性简略了解就是每次操作都是返回一个全新的数据(独立,污浊),所以热重载和工夫旅行这些性能才可能应用。
Effect
Effect 被称为副作用,之所以叫副作用是因为它使得咱们的函数变得不纯,同样的输出不肯定取得同样的输入。
dva 为了管制副作用的操作,底层引入了 redux-sagas 做异步流程管制,因为采纳了 generator 的相干概念,所以将异步转成同步写法,从而将 effects 转为纯函数。
Subscription
$$
({dispatch, history}, done) => unlistenFunction
$$
Subscriptions 是一种从 源 获取数据的办法,它来自于 elm。在 app.start()
时被执行,数据源能够是以后的工夫、服务器的 websocket 连贯、keyboard 输出、geolocation 变动、history 路由变动等等。
Subscription 语义是订阅,用于订阅一个数据源,而后依据条件 dispatch 须要的 action。
import key from 'keymaster';
...
app.model({
namespace: 'count',
subscriptions: {keyEvent({dispatch}) {key('⌘+up, ctrl+up', () => {dispatch({type:'add'}) });
},
}
});
官网说的比拟抽象, 实际上它的流程大略如下
key
的名称没有任何束缚, 只是用于在保留, 最大作用用来勾销监听- dispatch 只能作用以后
model
所在的reducer
和effects
- 只会在调用 app.start() 的时候,遍历所有 model 中的 subscriptions 执行一遍。
- 配置的函数须要返回一个函数,该函数应该用来勾销订阅的该数据源。调用 app.unmodel() 执行
Dva 图解
最常见的 Web 类示例之一: TodoList = Todo list + Add todo button
图解一: React 表示法
依照 React 官网领导意见, 如果多个 Component 之间要产生交互, 那么状态 (即: 数据) 就保护在这些 Component 的最小公约父节点上, 也即是 <App/>
<TodoList/> <Todo/>
以及<AddTodoBtn/>
自身不维持任何 state, 齐全由父节点 <App/> 传入 props 以决定其展示, 是一个纯函数的存在模式, 即: Pure Component
图解二: Redux 表示法
React 只负责页面渲染, 而不负责页面逻辑, 页面逻辑能够从中独自抽取进去, 变成 store
与图一相比, 几个显著的改良点:
- 状态及页面逻辑从
<App/>
外面抽取进去, 成为独立的 store, 页面逻辑就是 reducer <TodoList/>
及<AddTodoBtn/>
都是 Pure Component, 通过 connect 办法能够很不便地给它俩加一层 wrapper 从而建设起与 store 的分割: 能够通过 dispatch 向 store 注入 action, 促使 store 的状态进行变动, 同时又订阅了 store 的状态变动, 一旦状态有变, 被 connect 的组件也随之刷新- 应用 dispatch 往 store 发送 action 的这个过程是能够被拦挡的, 自然而然地就能够在这里减少各种
Middleware
, 实现各种自定义性能
这样一来, 各个局部各司其职, 耦合度更低, 复用度更高, 扩展性更好
图解三: 退出 Saga
- 点击创立 Todo 的按钮, 发动一个 type = addTodo 的 action
- saga 拦挡这个 action, 发动 http 申请, 如果申请胜利, 则持续向 reducer 发一个 type = addTodoSucc 的 action, 提醒创立胜利, 反之则发送 type = addTodoFail 的 action 即可
图解四: Dva 表示法
Dva 是基于 React + Redux + Saga 的最佳实际积淀, 做了 3 件很重要的事件, 大大晋升了编码体验:
- 把 store 及 saga 对立为一个
model
的概念, 写在一个 js 文件外面 - 减少了一个 Subscriptions, 用于收集其余起源的 action
- model 写法很简洁, 相似于 DSL 或者 RoR
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {add(state) {
const newCurrent = state.current + 1;
return {
...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {return { ...state, current: state.current - 1};
},
},
effects: {*add(action, { call, put}) {yield call(delay, 1000);
yield put({type: 'minus'});
},
},
subscriptions: {keyboardWatcher({ dispatch}) {key('⌘+up, ctrl+up', () => {dispatch({type:'add'}) });
},
},
});