Last active
October 30, 2018 13:03
-
-
Save ryyppy/484aff4b3da4c045c0e177d27bd00761 to your computer and use it in GitHub Desktop.
Redux saga example for (https://twitter.com/wiekatz/status/736564915448782848)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
import { takeLatest } from 'redux-saga'; | |
import { call, put } from 'redux-saga/effects'; | |
/** | |
* STATIC ENVIRONMENT | |
*/ | |
export const config = { | |
notificationDelay: 5000, | |
}; | |
/** | |
* ACTION CONSTANTS | |
*/ | |
export const FETCH_MESSAGES = 'FETCH_MESSAGES'; | |
export const UPDATE_MESSAGE_LIST = 'UPDATE_MESSAGE_LIST'; | |
export const SHOW_ERROR = 'SHOW_ERROR'; | |
/** | |
* ACTION-CREATORS | |
*/ | |
export function showError(msg: string): Action { | |
return { type: SHOW_ERROR, msg }; | |
} | |
export function hideError(): Action { | |
return { type: 'HIDE_ERROR' }; | |
} | |
export function updateMessageList( | |
messages: Array<Object>, | |
sortField: string, | |
sortAscending: boolean, | |
paging: PagingParam): Action { | |
// Cannot infer how this action really looks like ;-) | |
return { | |
type: UPDATE_MESSAGE_LIST, | |
messages, | |
sortField, | |
sortAscending, | |
...paging, | |
}; | |
} | |
/** | |
* UTILITY FUNCTIONS / SIDE-EFFECTS | |
*/ | |
export function delay(timeout: number): Promise<void> { | |
return new Promise(resolve => setTimeout(resolve, timeout)); | |
} | |
export function fetchData( | |
serviceUrl: string, | |
folderName: string, | |
sortField: string, | |
sortAscending: boolean, | |
page: string): Promise<FetchResult> { | |
// I allow myself to return a static result, | |
// we just pretend this will be returned | |
return Promise.resolve({ | |
messages: [ | |
{ subject: 'foo' }, | |
{ subject: 'bar' }, | |
{ subject: 'baz' }, | |
], | |
paging: { page, size: 1, total: 1 }, | |
}); | |
} | |
/** | |
* TYPE DECLARATIONS | |
*/ | |
type Action = { | |
type: string, | |
} | |
type Message = { | |
subject: string, | |
selected ?: boolean, | |
}; | |
type SagaParams = { | |
sortField: string, | |
sortAscending: boolean, | |
page: number, | |
} | |
type PagingParam = { | |
page: string, | |
size: number, | |
total: number, | |
}; | |
type FetchResult = { | |
messages: Array<Message>, | |
paging: PagingParam, | |
} | |
/** | |
* SAGAS / SAGA WRAPPER | |
*/ | |
export function *fetchMessagesSaga( | |
serviceUrl: string, | |
folderName: string, | |
{ sortField, sortAscending, page }: SagaParams): Generator<Object, void, FetchResult> { | |
try { | |
const { messages, paging } = yield call( | |
fetchData, | |
serviceUrl, | |
folderName, | |
sortField, | |
sortAscending, | |
page | |
); | |
const updateMessageListAction = updateMessageList( | |
messages.map(message => ({ ...message, selected: false })), | |
sortField, | |
sortAscending, | |
{ ...paging, page }, | |
); | |
yield put(updateMessageListAction); | |
} catch (e) { | |
yield put(showError(`Fehler und so: ${e.message}`)); | |
yield call(delay, config.notificationDelay); | |
yield put(hideError()); | |
} | |
} | |
export default function *(serviceUrl: string, folderName: string): Generator<Object, void, FetchResult> { | |
yield* takeLatest(FETCH_MESSAGES, fetchMessagesSaga, serviceUrl, folderName); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
// We don't test the takeLatest wrapper, since it just passes the | |
// parameters to the fetchMessagesSaga anyways | |
import { fetchMessagesSaga } from './fetchMessages'; | |
import watchFetchMessageSaga from './fetchMessages'; | |
// Pretend the env to come from a different file | |
import { config } from './fetchMessages'; | |
// Pretend the constants to come from a different file | |
import { FETCH_MESSAGES } from './fetchMessages'; | |
// Pretend the action creators to come from a different file | |
import { | |
showError, | |
hideError, | |
updateMessageList, | |
} from './fetchMessages'; | |
// Pretend the side-effect fns to come from a different file | |
import { | |
delay, | |
fetchData, // NOTE: Here we use the same real implementation FN | |
} from '../../src/sagas/fetchMessages'; | |
import { take, put, call, fork } from 'redux-saga/effects'; | |
describe('sagas/fetchMessagesSaga', () => { | |
const _oldNotificationDelay = config.notificationDelay; | |
before(() => { | |
config.notificationDelay = 4000; | |
}); | |
after(() => { | |
// That's why I don't like global dependencies ;-) | |
// especially in my testing framework | |
config.notificationDelay = _oldNotificationDelay; | |
}); | |
it('should yield call & put on success', () => { | |
const iter = fetchMessagesSaga('shmurl', 'shmolder name', { sortField: 'shmort field', sortAscending: true, page: 3 }); | |
let ret = iter.next(); | |
expect(ret.value).to.deep.equal(call(fetchData, 'shmurl', 'shmolder name', 'shmort field', true, 3)); | |
ret = iter.next({ | |
messages: [ | |
{ subject: 'some subject' }, | |
], | |
paging: { | |
total: 12, | |
size: 111, | |
}, | |
}); | |
const updateMessageListAction = updateMessageList( | |
[{ subject: 'some subject', selected: false }], | |
'shmort field', | |
true, | |
{ page: 3, total: 12, size: 111 }); | |
expect(ret.value).to.deep.equal(put(updateMessageListAction)); | |
// After the last yield, the result should reach | |
// the end of the generator, hence .done should be true | |
ret = iter.next(); | |
expect(ret.done).to.equal(true); | |
}); | |
it('should yield call, put(err), call(delay) and put(hideError) on error', () => { | |
const iter = fetchMessagesSaga('shmurl', 'shmolder name', { sortField: 'shmort field', sortAscending: true, page: 3 }); | |
let ret = iter.next(); | |
expect(ret.value).to.deep.equal(call(fetchData, 'shmurl', 'shmolder name', 'shmort field', true, 3)); | |
// Here, we want to provoke a fail for the `call` effect, | |
// so we use the standardised `throw` iterator function | |
// This will also automatically yield the next effect | |
// in our catch() branch | |
ret = iter.throw(new Error('some IO error')); | |
expect(ret.value).to.deep.equal(put(showError('Fehler und so: some IO error'))); | |
ret = iter.next(); | |
expect(ret.value).to.deep.equal(call(delay, 4000)); | |
ret = iter.next(); | |
expect(ret.value).to.deep.equal(put(hideError())); | |
ret = iter.next(); | |
expect(ret.done).to.equal(true); | |
}); | |
}); | |
/** | |
* BONUS: Testing the saga wrapper (takeLatest) | |
*/ | |
describe('sagas/fetchMessagesSaga', () => { | |
it('should yield forks on take effects', () => { | |
const iter = watchFetchMessageSaga('shmurl', 'shmolder name', { sortField: 'shmort field', sortAscending: true, page: 3 }); | |
// on initialization, takeLatest yields a take() and fork() | |
// see the official API for implementation details | |
let ret = iter.next(); | |
expect(ret.value).to.deep.equal(take(FETCH_MESSAGES)); | |
ret = iter.next({ type: FETCH_MESSAGES }); | |
// takeLatests passes the captured action as last parameter to fetchMessagesSaga | |
// but the saga doesn't really need it... we just test it for completeness reasons | |
expect(ret.value).to.deep.equal(fork(fetchMessagesSaga, 'shmurl', 'shmolder name', { type: FETCH_MESSAGES })); | |
console.log(ret.value); | |
// takeLatests does a while(true) loop, so we check another time | |
ret = iter.next(); | |
expect(ret.value).to.deep.equal(take(FETCH_MESSAGES)); | |
// .... and so on and on,... | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment