ES6 generators, TypeScript 2.0, and more!
Futurice WWWeeklies 26.8.2016A 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 iterator = generator();
function *testingGenerators() {
console.log('inside: starting');
const firstYieldReturn = yield 1;
console.log('inside: firstYieldReturn', firstYieldReturn);
const secondYieldReturn = yield 2;
console.log('inside: secondYieldReturn', secondYieldReturn);
return 3;
}
const iterator2 = testingGenerators();
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 simply Javascript objects. They are yielded to the redux-saga middleware to tell it what to do.
The redux-saga library comes with built-in support for certain types of effects, along with effect creators. Using these primitives, you can do some really powerful stuff.
// 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);
// 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 });
}
}
// component.tsx
dispatch({ type: 'LOAD_DATA', id });
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);
}
}
Why does call
exist?
Why not just call the function or generator directly?
All you're doing is building generators that produce effects, i.e. plain JavaScript objects. No need to mock anything.
Testing becomes functional, with no side-effects.
If we have time and any of you are interested, I'd be more than happy to talk about: TypeScript 2.0, what to store in the Redux state while loading in progress or errors, structuring React applications...
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 in November.