Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit92ef3b0

Browse files
committed
Adds async action example
Introduces async actions, pseudo-async API, middleware, and types forall of it.See:https://rjzaworski.com/2015/09/typescript-redux-async-actions
1 parentaf1cf7e commit92ef3b0

File tree

12 files changed

+229
-22
lines changed

12 files changed

+229
-22
lines changed

‎karma.conf.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ module.exports = function(config) {
1212
basePath:'',
1313
frameworks:['jasmine','sinon'],
1414
files:[
15+
// polyfill features for phantom
16+
'node_modules/es6-promise/dist/es6-promise.js',
17+
18+
// source files
1519
'src/**/__tests__/*spec.ts',
1620
'src/**/__tests__/*spec.tsx'
1721
],

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"redux":"^3.5.2"
2323
},
2424
"devDependencies": {
25+
"es6-promise":"^3.2.1",
2526
"jasmine-core":"2.4.1",
2627
"karma":"^1.1.2",
2728
"karma-jasmine":"1.0.2",

‎src/actions/index.ts‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1-
exporttypeAction={
2-
type:'INCREMENT_COUNTER',
3-
delta:number,
4-
}|{
5-
type:'RESET_COUNTER',
6-
}
1+
typeQ<T>={request:T}
2+
typeS<T>={response:T}
3+
typeE={error:Error}
4+
5+
typeQEmpty=Q<null>
6+
typeQValue=Q<{value:number}>
7+
8+
exporttypeAction=
9+
// UI actions
10+
{type:'INCREMENT_COUNTER',delta:number}
11+
|{type:'RESET_COUNTER'}
12+
13+
// API Requests
14+
|({type:'SAVE_COUNT_REQUEST'}&QValue)
15+
|({type:'SAVE_COUNT_SUCCESS'}&QValue&S<{}>)
16+
|({type:'SAVE_COUNT_ERROR'}&QValue&E)
17+
18+
|({type:'LOAD_COUNT_REQUEST'}&QEmpty)
19+
|({type:'LOAD_COUNT_SUCCESS'}&QEmpty&S<{value:number}>)
20+
|({type:'LOAD_COUNT_ERROR'}&QEmpty&E)
721

822
exportconstincrementCounter=(delta:number):Action=>({
923
type:'INCREMENT_COUNTER',
@@ -13,3 +27,27 @@ export const incrementCounter = (delta: number): Action => ({
1327
exportconstresetCounter=():Action=>({
1428
type:'RESET_COUNTER',
1529
})
30+
31+
exporttypeApiActionGroup<_Q,_S>={
32+
request:(q?:_Q)=>Action&Q<_Q>
33+
success:(s:_S,q?:_Q)=>Action&Q<_Q>&S<_S>
34+
error:(e:Error,q?:_Q)=>Action&Q<_Q>&E
35+
}
36+
37+
exportconstsaveCount:ApiActionGroup<{value:number},{}>={
38+
request:(request)=>
39+
({type:'SAVE_COUNT_REQUEST', request}),
40+
success:(response,request)=>
41+
({type:'SAVE_COUNT_SUCCESS', request, response}),
42+
error:(error,request)=>
43+
({type:'SAVE_COUNT_ERROR', request, error}),
44+
}
45+
46+
exportconstloadCount:ApiActionGroup<null,{value:number}>={
47+
request:(request)=>
48+
({type:'LOAD_COUNT_REQUEST',request:null}),
49+
success:(response,request)=>
50+
({type:'LOAD_COUNT_SUCCESS',request:null, response}),
51+
error:(error,request)=>
52+
({type:'LOAD_COUNT_ERROR',request:null, error}),
53+
}

‎src/api.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
exportconstapi={
2+
save:(counter:{value:number}):Promise<null>=>{
3+
try{
4+
localStorage.setItem('__counterValue',counter.value.toString())
5+
returnPromise.resolve(null)
6+
}
7+
catch(e){
8+
returnPromise.reject(e)
9+
}
10+
},
11+
load:():Promise<{value:number}>=>{
12+
try{
13+
constvalue=parseInt(localStorage.getItem('__counterValue'),10)
14+
returnPromise.resolve({ value})
15+
}
16+
catch(e){
17+
returnPromise.reject(e)
18+
}
19+
},
20+
}

‎src/components/__tests__/counter_spec.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ describe('components/Counter', () => {
4545

4646
beforeEach(()=>{
4747
counter=setup()
48-
constbuttonEl=TestUtils.findRenderedDOMComponentWithTag(counter,'button')
49-
TestUtils.Simulate.click(buttonEl)
50-
TestUtils.Simulate.click(buttonEl)
51-
TestUtils.Simulate.click(buttonEl)
48+
const[increment]=TestUtils.scryRenderedDOMComponentsWithTag(counter,'button')
49+
TestUtils.Simulate.click(increment)
50+
TestUtils.Simulate.click(increment)
51+
TestUtils.Simulate.click(increment)
5252
})
5353

5454
it('increments counter',()=>{

‎src/components/counter.tsx‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import * as React from 'react'
22
import*asreduxfrom'redux'
33
import{connect}from'react-redux'
44

5-
import{incrementCounter}from'../actions'
5+
import{
6+
incrementCounter,
7+
saveCount,
8+
loadCount,
9+
}from'../actions'
10+
611
import{Store}from'../reducers'
712

813
typeOwnProps={
@@ -15,16 +20,21 @@ type ConnectedState = {
1520

1621
typeConnectedDispatch={
1722
increment:(n:number)=>void
23+
save:(n:number)=>void
24+
load:()=>void
1825
}
1926

2027
constmapStateToProps=(state:Store.All,ownProps:OwnProps):ConnectedState=>({
2128
counter:state.counter,
2229
})
2330

2431
constmapDispatchToProps=(dispatch:redux.Dispatch<Store.All>):ConnectedDispatch=>({
25-
increment:(n:number):void=>{
26-
dispatch(incrementCounter(n))
27-
},
32+
increment:(n:number)=>
33+
dispatch(incrementCounter(n)),
34+
load:()=>
35+
dispatch(loadCount.request()),
36+
save:(value:number)=>
37+
dispatch(saveCount.request({ value})),
2838
})
2939

3040
classCounterComponentextendsReact.Component<ConnectedState&ConnectedDispatch&OwnProps,{}>{
@@ -34,12 +44,24 @@ class CounterComponent extends React.Component<ConnectedState & ConnectedDispatc
3444
this.props.increment(1)
3545
}
3646

47+
_onClickSave=(e:React.SyntheticEvent)=>{
48+
e.preventDefault()
49+
this.props.save(this.props.counter.value)
50+
}
51+
52+
_onClickLoad=(e:React.SyntheticEvent)=>{
53+
e.preventDefault()
54+
this.props.load()
55+
}
56+
3757
render(){
3858
const{ counter, label}=this.props
3959
return<div>
4060
<label>{label}</label>
4161
<pre>counter ={counter.value}</pre>
4262
<buttonref='increment'onClick={this._onClickIncrement}>click me!</button>
63+
<buttonref='save'onClick={this._onClickSave}>save</button>
64+
<buttonref='load'onClick={this._onClickLoad}>load</button>
4365
</div>
4466
}
4567
}

‎src/index.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import*asReactfrom'react'// tslint:disable-line
22
import*asReactDOMfrom'react-dom'
3-
import*asReduxfrom'redux'
3+
import*asreduxfrom'redux'
44
import{Provider}from'react-redux'
55

66
import{
@@ -10,7 +10,13 @@ import {
1010

1111
import{Counter}from'./components/counter'
1212

13-
letstore:Redux.Store<Store.All>=Redux.createStore(reducers)
13+
import{apiMiddleware}from'./middleware'
14+
15+
constmiddleware=redux.applyMiddleware(
16+
apiMiddleware
17+
)
18+
19+
letstore:redux.Store<Store.All>=redux.createStore(reducers,{}asStore.All,middleware)
1420

1521
// Commented out ("let HTML app be HTML app!")
1622
window.addEventListener('DOMContentLoaded',()=>{
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import*asreduxfrom'redux'
2+
3+
import{apiMiddleware}from'../'
4+
5+
import{api}from'../../api'
6+
7+
import{
8+
Action,
9+
loadCount,
10+
saveCount,
11+
}from'../../actions'
12+
13+
constempty=()=>{}
14+
15+
constmockDispatch=(dispatch:(a:Action)=>void):redux.MiddlewareAPI<any>=>
16+
({ dispatch,getState:empty})
17+
18+
describe('apiMiddleware',()=>{
19+
20+
describe('when SAVE_COUNT_REQUEST succeeds',()=>{
21+
22+
it('includes request { value }',(done)=>{
23+
constsaveStub=sinon.stub(api,'save')
24+
.returns(Promise.resolve({}))
25+
26+
apiMiddleware(mockDispatch((actual:Action)=>{
27+
expect(saveStub.firstCall.args[0].value).toEqual(13)
28+
saveStub.restore()
29+
done()
30+
}))(empty)(saveCount.request({value:13}))
31+
})
32+
33+
it('fires SAVE_COUNT_SUCCESS',(done)=>{
34+
constsaveStub=sinon.stub(api,'save')
35+
.returns(Promise.resolve({}))
36+
37+
apiMiddleware(mockDispatch((actual:Action)=>{
38+
saveStub.restore()
39+
expect(actual.type).toEqual('SAVE_COUNT_SUCCESS')
40+
done()
41+
}))(empty)(saveCount.request())
42+
})
43+
44+
})
45+
46+
describe('when LOAD_COUNT_REQUEST succeeds',()=>{
47+
48+
it('fires LOAD_COUNT_SUCCESS',(done)=>{
49+
constloadStub=sinon.stub(api,'load')
50+
.returns(Promise.resolve({value:42}))
51+
52+
apiMiddleware(mockDispatch((actual:Action)=>{
53+
loadStub.restore()
54+
55+
if(actual.type==='LOAD_COUNT_SUCCESS'){
56+
expect(42).toEqual(actual.response.value)
57+
done()
58+
}
59+
else{
60+
done.fail('types don\'t match')
61+
}
62+
}))(empty)(loadCount.request())
63+
})
64+
})
65+
66+
67+
})

‎src/middleware/index.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import*asreduxfrom'redux'
2+
3+
import{api}from'../api'
4+
5+
import{
6+
Action,
7+
saveCount,
8+
loadCount,
9+
}from'../actions'
10+
11+
exportconstapiMiddleware=({ dispatch}:redux.MiddlewareAPI<any>)=>
12+
(next:redux.Dispatch<any>)=>
13+
(action:Action)=>{
14+
switch(action.type){
15+
16+
case'SAVE_COUNT_REQUEST':
17+
api.save(action.request)
18+
.then(()=>dispatch(saveCount.success({},action.request)))
19+
.catch((e)=>dispatch(saveCount.error(e,action.request)))
20+
break
21+
22+
case'LOAD_COUNT_REQUEST':
23+
api.load()
24+
.then(({ value})=>dispatch(loadCount.success({ value},action.request)))
25+
.catch((e)=>dispatch(loadCount.error(e,action.request)))
26+
break
27+
}
28+
29+
returnnext(action)
30+
}

‎src/reducers/__tests__/index_spec.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import{createStore}from'redux'
22

33
import{reducers}from'../index'
4-
import{incrementCounter}from'../../actions'
4+
import{
5+
incrementCounter,
6+
loadCount,
7+
}from'../../actions'
58

69
describe('reducers/counter',()=>{
710
it('starts at 0',()=>{
@@ -19,4 +22,15 @@ describe('reducers/counter', () => {
1922
})
2023
store.dispatch(incrementCounter(3))
2124
})
25+
26+
it('restores state',(done)=>{
27+
conststore=createStore(reducers)
28+
store.subscribe(()=>{
29+
const{ counter}=store.getState()
30+
expect(counter.value).toEqual(14)
31+
done()
32+
})
33+
store.dispatch(loadCount.success({value:14}))
34+
})
35+
2236
})

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp