Лекция 30
npm install redux-saga
<script
src="https://unpkg.com/redux-saga/dist/redux-saga.js"
></script>
import { call, put } from 'redux-saga/effects'
export function * fetchData(action) {
try {
const data = yield call(
Api.fetchUser,
action.payload.url
)
yield put({type: "FETCH_SUCCEEDED", data})
} catch (error) {
yield put({type: "FETCH_FAILED", error})
}
}
import { takeEvery } from 'redux-saga/effects'
function * watchFetchData() {
yield takeEvery('FETCH_REQUESTED', fetchData)
}
import {
createStore,
applyMiddleware
} from 'redux';
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers';
import sagas from './sagas/watchFetchData';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
/* initial state */,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(sagas);
export default store;
import { takeEvery } from 'redux-saga'
// FETCH_USERS
function* fetchUsers(action) { ... }
// CREATE_USER
function* createUser(action) { ... }
// use them in parallel
export default function* rootSaga() {
yield takeEvery('FETCH_USERS', fetchUsers)
yield takeEvery('CREATE_USER', createUser)
}
// эффект - объект с инструкциями
// для sagaMiddleware о необходимых действиях
yield call(
Api.fetchUser,
action.payload.url
)
=>
{
CALL: {
fn: Api.fetch,
args: ['/users/42']
}
}
import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'
function* watchFetchProducts() {
yield takeEvery(
'PRODUCTS_REQUESTED',
fetchProducts
)
}
function* fetchProducts() {
// yield a Promise
const products = yield Api.fetch('/products')
console.log(products)
}
// trying write a test
const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??)
// need to mock Api.fetch
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(
Api.fetch,
'/products'
)
}
=>
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
import { call } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products')
)
import { take, put } from 'redux-saga/effects'
function* watchFirstThreeTodosCreation() {
for (let i = 0; i < 3; i++) {
const action = yield take('TODO_CREATED')
}
yield put({type: 'SHOW_CONGRATULATION'})
}
import { fork, call, take, put, 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})
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
}
...
...
function* loginFlow() {
while (true) {
const {user, password}
= yield take('LOGIN_REQUEST')
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')
}
}
function* authorize(user, password) {
try {
...
} catch(error) {
...
} finally {
if (yield cancelled()) {
// cancellation handling code here
}
}
}
import { all, call } from 'redux-saga/effects'
// correct, effects will get executed in parallel
const [users, repos] = yield all([
call(fetch, '/users'),
call(fetch, '/repos')
])
import { race, take, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'
function* fetchPostsWithTimeout() {
const {posts, timeout} = yield race({
posts: call(fetchApi, '/posts'),
timeout: call(delay, 1000)
})
if (posts)
put({type: 'POSTS_RECEIVED', posts})
else
put({type: 'TIMEOUT_ERROR'})
}
// other tasks are autocancel
import { race, take, put } from 'redux-saga/effects'
function* backgroundTask() {
while (true) { ... }
}
function* watchStartBackgroundTask() {
while (true) {
yield take('START_BACKGROUND_TASK')
yield race({
task: call(backgroundTask),
cancel: take('CANCEL_TASK')
})
}
}
import { runSaga } from 'redux-saga'
function* saga() { ... }
const myIO = {
subscribe: ...,
dispatch: ...,
getState: ...,
}
runSaga(
myIO
saga,
)
import { take, actionChannel, call, ... }
from 'redux-saga/effects'
function* watchRequests() {
// will buffer messages
const requestChan
= yield actionChannel('REQUEST')
while (true) {
const {payload} = yield take(requestChan)
yield call(handleRequest, payload)
}
}
function* handleRequest(payload) { ... }
import { eventChannel, END } from 'redux-saga'
function countdown(secs) {
return eventChannel(emitter => {
const iv = setInterval(() => {
secs -= 1
if (secs > 0) emitter(secs)
else emitter(END)
}, 1000);
return () => {
clearInterval(iv)
}
}
)
}
function* saga() {
const chan = yield call(countdown, value)
try {
while (true) {
// take(END) will cause the saga
// to terminate by jumping
// to the finally block
let seconds = yield take(chan)
console.log(`countdown: ${seconds}`)
}
} finally {
console.log('countdown terminated')
}
}
function createSocketChannel(socket) {
return eventChannel(emit => {
const pingHandler = (event)
=> emit(event.payload)
socket.on('ping', pingHandler)
return ()
=> socket.off('ping', pingHandler)
})
}
function* pong(socket) {
yield call(delay, 5000)
yield apply(socket, socket.emit, ['pong'])
}
function* watchOnPings() {
const socketChannel
= yield call(createSocketChannel, socket)
while (true) {
const payload = yield take(socketChannel)
yield put({
type: INCOMING_PONG_PAYLOAD, payload
})
yield fork(pong, socket)
}
}
import { throttle } from 'redux-saga/effects'
function* handleInput(input) {
// ...
}
function* watchInput() {
yield throttle(500,
'INPUT_CHANGED',
handleInput
)
}
import { delay } from 'redux-saga'
function* handleInput(input) {
yield call(delay, 500)
...
}
function* watchInput() {
let task
while (true) {
const { input } = yield take('INPUT_CHANGED')
if (task) yield cancel(task)
task = yield fork(handleInput, input)
}
}
import { delay } from 'redux-saga'
function* handleInput({ input }) {
yield call(delay, 500)
...
}
function* watchInput() {
// will cancel current running handleInput task
yield takeLatest('INPUT_CHANGED', handleInput);
}
import { delay } from 'redux-saga'
import { call, put, take } from 'redux-saga/effects'
function* updateApi(data) {
...
}
export default function* updateResource() {
...
}
// updateApi
for(let i = 0; i < 5; i++) {
try {
return yield call(
apiRequest, { data }
);
} catch(err) {
if(i < 4) {
yield call(delay, 2000);
}
}
}
// attempts failed after 5 attempts
throw new Error('API request failed');
// updateResource
while (true) {
const { data } = yield take('UPDATE_START');
try {
const apiResponse = yield call(updateApi, data);
yield put({
type: 'UPDATE_SUCCESS',
payload: apiResponse.body,
});
} catch (error) {
yield put({
type: 'UPDATE_ERROR',
error
});
}
}
import { take, put, call, spawn, race } from 'redux-saga/effects'
import { delay } from 'redux-saga'
import { updateThreadApi, actions } from 'somewhere'
function* onArchive(action) {
...
}
export default function* main() {
while (true) {
const action = yield take('ARCHIVE_THREAD')
yield spawn(onArchive, action)
}
// onArchive
const { threadId } = action
const undoId = `UNDO_ARCHIVE_${threadId}`
const thread = { id: threadId, archived: true }
yield put(actions.showUndo(undoId))
yield put(actions.updateThread(thread))
const { undo, archive } = yield race({
undo: take(action => action.type === 'UNDO'
&& action.undoId === undoId),
archive: call(delay, 5000)
})
...
// onArchive
...
yield put(actions.hideUndo(undoId))
if (undo) {
yield put(actions.updateThread({
id: threadId,
archived: false
}))
} else if (archive) {
yield call(updateThreadApi, thread)
}
takeEvery(pattern, saga, ...args)
takeEvery(channel, saga, ...args)
// pattern: String | Array | Function
// saga: Generator
// args: Array
// channel: Channel
takeLatest(pattern, saga, ...args)
takeLatest(channel, saga, ...args)
// pattern: String | Array | Function
// saga: Generator
// args: Array
// channel: Channel
throttle(ms, pattern, saga, ...args)
// ms: Number
// pattern: String | Array | Function
// saga: Generator
// args: Array
take(pattern)
take(channel)
take()
take('*')
// саги не уничтожаются после специального
// события END
take.maybe(pattern)
take.maybe(channel)
put(action)
put(channel, action)
// ждёт работу редьюсеров
// и получит ошибку, если она
// произошла при обработке
put.resolve(action)
call(fn, ...args)
call([context, fn], ...args)
call([context, fnName], ...args)
// fn: Generator, Async Function, Function
apply(context, fn, [args])
// alias - call([context, fn], ...args)
// call для асинхронных методов в стиле Node.js
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()
import { CANCEL } from 'redux-saga'
import { fork, cancel } from 'redux-saga/effects'
function myApi() {
const promise = myXhr(...)
promise[CANCEL] = () => myXhr.abort()
return promise
}
function* mySaga() {
const task = yield fork(myApi)
yield cancel(task)
}
select(selector, ...args)
// selector(getState(), ...args)
actionChannel(pattern, [buffer])
import { actionChannel, call } from 'redux-saga/effects'
import api from '...'
function* takeOneAtMost() {
const chan = yield actionChannel('USER_REQUEST')
while (true) {
const {payload} = yield take(chan)
yield call(api.getUser, payload)
}
}
// стереть все события из буфера
flush(channel)
function* saga() {
const chan = yield actionChannel('ACTION')
try {
while (true) {
const action = yield take(chan)
// ...
}
} finally {
const actions = yield flush(chan)
// ...
}
}
cancelled()
function* saga() {
try {
// ...
} finally {
if (yield cancelled()) {
// logic that should execute only on Cancellation
}
// logic that should execute in all situations (e.g. closing a channel)
}
}
setContext(props)
getContext(prop)
race(effects)
race([...effects])
all(effects)
all([...effects])
import { delay } from 'redux-saga';
import { put, call, select } from 'redux-saga/effects';
import { getSuggestions } from '../api';
import PlacesActions from '../actions/places';
export default function* (action) {
...
};
const state = yield select();
if (!state.maps.isApiLoaded) {
return;
}
if (!action.payload) {
yield put(PlacesActions.suggestions([]));
yield put(PlacesActions.select(undefined));
return;
}
yield call(delay, 250);
...
...
const suggestions = yield call(
getSuggestions,
state.maps.autocompleteService,
state.countryCode,
action.payload
);
yield put(PlacesActions.suggestions(
suggestions || []
));
import { takeEvery, takeLatest, fork } from 'redux-saga/effects';
import PlacesActions from '../actions/places';
import PlacesSuggestionsSaga from './places-suggestions';
export default function* sagas() {
yield takeLatest(
PlacesActions.changeQuery,
PlacesSuggestionsSaga
);
}
import { eventChannel } from 'redux-saga';
import { take, put, race } from 'redux-saga/effects';
import { ComponentActions } from '../../actions/component';
export function notifyAboutWindowEventFactory(
events,
action
) {
let onWindowEvent;
let destroyed = false;
if (!(events instanceof Array)) {
events = [events];
}
...
}
...
function windowEventChannel() {
return eventChannel(emitter => {
onWindowEvent = event => emitter(event);
events.forEach(eventName =>
window.addEventListener(
eventName, onWindowEvent
)
);
return () => {
events.forEach(eventName => window.removeEventListener(eventName, onWindowEvent));
};
});
}
...
...
return function* () {
yield take(ComponentActions.init);
const channel = yield windowEventChannel();
...
}
...
return function* () {
...
while (!destroyed) {
const { event } = yield race({
event: take(channel),
destroy: take(ComponentActions.destroy),
});
if (event) {
yield put(action(event));
} else {
channel.close();
destroyed = true;
}
}
}
function requestPermission() {
return eventChannel(emitter => {
Notification.requestPermission((permission) => {
emitter(permission);
emitter(END);
});
return () => {};
});
}
function onClick(notification) {
return eventChannel(emitter => {
notification.onclick = () => {
emitter('clicked!');
emitter(END);
};
return () => {
notification.onclick = null;
};
});
}
function* notificationsSaga(action) {
const state = yield select();
if (state.window.inFocus) {
return;
}
let channel = yield call(requestPermission);
const permission = yield take(channel);
if (permission !== 'granted') {
return;
}
...
}
function* notificationsSaga(action) {
...
const notification = new Notification(
`You have ${action.payload.jobs.length} new job posts`,
{ icon: 'assets/new-jobs.png' }
);
channel = yield call(onClick, notification);
const {click, timeout} = yield race({
click: take(channel),
timeout: call(delay, 5000)
});
...
}
function* notificationsSaga(action) {
...
if (click) {
yield put(JobsActions.show());
window.focus();
window.scrollTo(0,0);
}
notification.close();
}
import { eventChannel, buffers } from 'redux-saga';
import { take, call, put } from 'redux-saga/effects';
import Api from '../api';
import JobsActions from '../actions/jobs';
function readFreshJobs() {
...
}
export default function* saga() {
...
}
// readFreshJobs() {
return eventChannel(emitter => {
const emit = (jobs) => {
emitter(jobs);
};
Api.socket.on(Api.events.jobsFresh, emit);
return () => Api.socket.removeListener(
Api.events.jobsFresh,
emit
);
}, buffers.fixed());
// saga
const channel = yield call(readFreshJobs);
while (true) {
let jobs = yield take(channel);
yield put(JobsActions.fresh(jobs));
}
import { eventChannel } from 'redux-saga';
import { take, put, fork, race } from 'redux-saga/effects';
import EventEmmiter from 'events'
import ComponentActions
from '../../shared/actions/component';
...
...
if (!window.GlobalEventBus) {
window.GlobalEventBus = new EventEmmiter();
}
let handler, bus = window.GlobalEventBus,
destroyed = false;
function globalEventChannel() {
...
}
export const globalEventBusSagaFactory
= function (allowedActions) {
return function * () {...}
};
// globalEventChannel
return eventChannel(emitter => {
handler = event => emitter({
...event,
external: true
});
bus.on('global.event', handler);
return () => {
bus.removeListener('global.event', handler);
};
});
// globalEventBusSagaFactory
yield take(ComponentActions.init);
const channel = yield globalEventChannel();
while (!destroyed) {
const {
local,
external,
destroy
} = yield race({
destroy: take(ComponentActions.destroy),
external: take(channel),
local: take(),
});
...
// globalEventBusSagaFactory
...
if (external
&& allowedActions.some(action =>
action === external.type)) {
yield put(external);
}
if (local && !local.external && allowedActions.some(action => action === local.type)) {
bus.emit('global.event', local);
}
if (destroy) {
bus.removeListener('global.event', handler);
destroyed = true;
}
}