Wed 06 Jun 2018 — Mon 18 Jun 2018

Redux

https://redux.js.org/

There is a store, which represents your state over time. State is an object. It's worth trying to separate view state from your main model inside the state object.

You change the state by sending actions to the store with store.dispatch(someAction). Actions have a mandatory type property, and may have extra properties.

Action creators are functions which make actions.

Reducers are functions which take the state and an action, and return a new state. Reducers don't have to operate on the whole state at once: you can decide to just give them a small piece to work on.

There is a combineReducers({stateProperty: reducer, ...}) function which handles splitting up the state amongst the various reducers.

When you update the state (by dispatching actions to the store), it will emit events, which you can listen to with store.subscribe(listener). This returns a handle, which you can later pass to store.unsubscribe(handle).

Using Redux with React

Redux has a connect() function which generates React components. This takes two functions:

mapStateToProps(state)
returns a dictionary of props we want to where to find them on the state.
mapDispatchToProps(state)
returns a dictionary of props we want to callback functions. These functions should probably dispatch actions.

The result of connect is a function which we call on one of our React Components to hook it up.

We don't need to do this for all of our components: we should decide which will be in charge of state.

If we wrap our React App in a Provider component, it will dependency inject the store to all the components which were generated using connect().

Middleware

These are decorators which can affect every call to store.dispatch(). You register them in a specific order.

You could do this with reducers instead, but that wouldn't work with Redux's built in combineReducers() function.

Reselect

Reselect is a helpful library which lets you define a selector function on the state. This is memoized, so you can use it to make computations of the state efficiently.

You can use a selector function within another.

Async Actions with Thunks

Normally we can only dispatch actions to the store.

The Thunks middleware lets us dispatch functions as well. Functions you dispatch will be called with the dispatch() and getState() methods passed in, so you can set up callback chains.

Redux-Saga

An alternative to managing asynchrony with thunks.

It's supposed to look like we're doing the grubby (impure) bits on a separate thread.

To setup, start with createSagaMiddleWare(). This gives you a redux middleware, which means we wrap it in Redux's applyMiddleware() function, and then pass the result to createStore().

The Saga middleware object we created has a run(saga) function, which takes the sagas you have written.

We write our sagas as generator functions, inside which we yield calls to Saga's API functions (described below).

Ideally, your saga should be a declarative description of side-effects. It should not execute the effects directly, because that makes it more difficult to test and reason about.

Calling functions:

call(f, ...args) and apply(f, [args])
asynchronously call f with arguments.
delay(ms)
exactly what you'd expect. Usually you want to do yield call(delay, ms) instead.
fork(f, args)
non-blocking attached task — errors and cancellation shared with parent.
spawn(f, args)
non-blocking detached task — entirely separate from parent.

Doing things in response to actions:

put(action)
dispatch an action to the store.
take(action)
wait until we see this action.
takeEvery(action, function)
every time we see this action, run this fuction.
takeLatest(action, function)
when we see this action, run this function debounced.
throttle(ms, action, f, args)
when we see this action, run this function, then ignore the action for some time period.

Manipulating tasks:

join(tasks)
wait for all the tasks for finish.
all([...generators])
launch everything in parallel, then wait for it all to finish. This is like forking in a loop and then joining everything we forked.
cancel(task)
cancel a task and its children.
cancel()
cancel myself and my children.
race(tasks)
complete one of these tasks, then cancel the rest.

Manipulating state

select(f_state, ...args)
get some data out of the store.

Alternatively, we can yield on anything which returns a Promise and Saga will understand it. This makes it harder to test our Sagas though.

Combining Sagas

For design and readability reasons, we'll probably want to write a lot of different generator functions, then bundle them together into a single one at the end.

We can concatenate sub-sagas using generation concatenation: yield* subSaga().

Alternatively, we can call a saga the same way we call functions which return promises: yield call(subSaga()).

Channels

Channels are a way for tasks to communicate.

This is useful if you need to run something sequentially in order (so you need to block), but still want to buffer up inputs.

Creating channels:

channel()
any old channel
actionChannel()
a channel which is connected up to the store

Using channels:

Channel.put(message)
put a message.
Channel.take(f)
wait for the next message.
Channel.flush(f)
get all the messages that are in the channel now.
Channel.close()
duh.