Including ES6 generators, TypeScript 2.1, and more!
React Helsinki 22.2.2017Custom projects
A preview server for static web projects
// component.tsx
const loadData = (dispatch: Dispatch<any>, id: string) => {
fetch(`/api/data/${id}`)
.then(response =>
response.ok ? response.json() : Promise.reject(response.error)
).then(json => {
dispatch({
type: 'STORE_DATA',
data: json
});
}).catch(error => {
dispatch({
type: 'DATA_FETCH_FAILED',
error: error || 'Something bad happened'
});
});
};
loadData(this.props.dispatch, this.props.id);
// actions.ts
const storeData = (data: any) => ({ type: 'STORE_DATA', data });
const fetchFailed = (error: string) => ({ type: 'FETCH_FAILED', error });
let inProgressFetches: string[] = [];
export const loadData = (dispatch: Dispatch<any>, id: string) => {
if (inProgressFetches.indexOf(id) > -1) return;
inProgressFetches.push(id);
fetch(`/api/data/${id}`)
.then(response =>
response.ok ? response.json() : Promise.reject(response.error)
).then(json => {
dispatch(storeData(json));
inProgressFetches = inProgressFetches.filter(n => n !== id);
}).catch(error => {
dispatch(fetchFailed(error));
inProgressFetches = inProgressFetches.filter(n => n !== id);
});
};
// component.tsx
import { loadData } from './actions';
loadData(this.props.dispatch, this.props.id);
// actions.ts
const storeData = (data: any) => ({ type: 'STORE_DATA', data });
const fetchFailed = (error: string) => ({ type: 'FETCH_FAILED', error });
let inProgressFetches: string[] = [];
export const loadData = (id: string) => (dispatch: Dispatch) => {
if (inProgressFetches.indexOf(id) > -1) return;
inProgressFetches.push(id);
fetch(`/api/data/${id}`)
.then(response =>
response.ok ? response.json() : Promise.reject(response.error)
).then(json => {
dispatch(storeData(json));
inProgressFetches = inProgressFetches.filter(n => n !== id);
}).catch(error => {
dispatch(fetchFailed(error));
inProgressFetches = inProgressFetches.filter(n => n !== id);
});
};
// component.tsx
import { loadData } from './actions';
this.props.dispatch(loadData(this.props.id));
function *generator() {
let i = 1;
while (true) {
yield i++;
}
}
const iter = generator();
iter.next(); // => { value: 1, done: false }
iter.next(); // => { value: 2, done: false }
iter.next(); // => { value: 3, done: false }
function *twoWayGenerator() {
console.log('inside: starting');
const firstYieldValue = yield 1;
console.log('inside: firstYieldValue', firstYieldValue);
const secondYieldValue = yield 2;
console.log('inside: secondYieldValue', secondYieldValue);
return 3;
}
const iter2 = twoWayGenerator();
iter2.next();
// 'inside: starting'
// => { value: 1, done: false }
iter2.next('hi there');
// 'inside: firstYieldValue', 'hi there'
// => { value: 2, done: false }
iter2.next('oh my');
// 'inside: secondYieldValue', 'oh my'
// => { value: 3, done: true }
iter2.next()
// => { value: undefined, done: true }
Sagas are like long-running background processes.
The idea comes from the database field, from a paper published in 1987.
redux-saga is a redux middleware that provides saga functionality using ES6 generators.
redux-saga allows you to intercept redux actions and do funky stuff using effects.
redux-saga effects are plain old Javascript objects. They are yielded to the redux-saga middleware to tell it what to do.
redux-saga comes with effect generators—functions that return appropriately formatted objects.
const effect = effectCreator(...args);
yield effect;
// createStore.ts
import { applyMiddleware, createStore } from 'redux';
import saga from './sagas';
import reducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, {}, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(saga);
// component.tsx
dispatch({ type: 'LOAD_DATA', id });
// sagas/index.ts
export default function* watchForLoadData(): IterableIterator<Effect> {
while (true) {
const { id } = yield take('LOAD_DATA');
yield fork(loadData, id);
}
}
function* loadData(id: string): IterableIterator<Effect> {
const { response, error } = yield call(api.fetchData, id);
if (response) {
yield put({ type: 'STORE_DATA', id, data: response.data });
} else {
yield put({ type: 'FETCH_ERROR', id, error });
}
}
function *fetchUsers(userIds: string[]): IterableIterator<Effect[]> {
const users = yield userIds.map(id => call(fetchUser, id));
yield put({ type: 'STORE_USERS', users });
}
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
function* fetchPostsWithTimeout(): IterableIterator<Effect> {
const { posts, timeout } = yield race({
posts: call(fetchPosts),
timeout: call(delay, 1000)
});
if (posts)
put({ type: 'POSTS_RECEIVED', posts });
else
put({ type: 'TIMEOUT_ERROR' });
}
function* backgroundTask(): IterableIterator<Effect> {
while (true) { ... }
}
function* watchStartBackgroundTask(): IterableIterator<Effect> {
while (true) {
yield take('START_BACKGROUND_TASK');
yield race({
task: call(backgroundTask),
cancel: take('CANCEL_TASK')
});
}
}
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
function* handleInput(input: string): IterableIterator<Effect> {
// debounce by 500ms
yield call(delay, 500);
...
}
function* watchInput() {
let task: Task;
while (true) {
const { input } = yield take('INPUT_CHANGED');
if (task) {
yield cancel(task);
}
task = yield fork(handleInput, input);
}
}
function* authorize(credentials: Token | { name: string, password: string }) {
// api.authorize throws on error
const token = yield call(api.authorize, credentials);
yield put(login.success(token));
return token;
}
function* authAndRefreshTokenOnExpiry(name: string, password: string) {
let token = yield call(authorize, { name, password });
while(true) {
yield call(delay, token.expires_in);
token = yield call(authorize, token);
}
}
/* Continued... */
/* Continued... */
function* watchAuth() {
while(true) {
try {
const { name, password } = yield take(LOGIN_REQUEST);
yield race([
take(LOGOUT),
call(authAndRefreshTokenOnExpiry, name, password);
])
} catch(error) {
yield put(login.error(error));
}
}
}
Why does call
exist?
Why not just call the function or generator directly from your saga?
All you're doing is building generators that produce effects, i.e. plain JavaScript objects. No need to mock whatever you're "calling".
Testing becomes functional, with no side-effects.
const iterator = loginSaga();
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST));
const mockAction = { user: '...', pass: '...' };
assert.deepEqual(
iterator.next(mockAction).value,
call(request.post, '/login', mockAction)
);
const mockError = 'invalid user/password'
assert.deepEqual(
iterator.throw(mockError).value,
put({ type: LOGIN_ERROR, error: mockError })
);
Questions? If we have time and any of you are interested, I'd be more than happy to talk about e.g. TypeScript.
If you're working on static web projects (i.e. projects with no backend in the repo), come talk! We're launching our closed alpha soon.