AWRAL
Awesome Wrapper for Redux Action's Lifecycle
Swiss Army knife for async actions in Redux.
Like promise-middleware, but:
- without any middlewares/store changes
- smaller (80 lines, 1KB gzipped)
- simpler (?)
- targeting zero-code duplications, high reusability
- follows flux-standard-action
Checking sources maybe faster than reading docs
Why?
Assume you're using redux and redux-thunk.
When you work with async stuff you probably have next code in your container:
const mapStateToProps = state => ({userId: state.userId})
const mapDispatchToProps = dispatch => ({
getUser(id) {
dispatch(GET_USER(id))
}
})
Where GET_USER is the next async action:
export const GET_USER = id => async (dispatch, getState) => {
const state = getState()
// 1. Validate input before dispatching `pending`
if (id === 111 && state.user === 'Alex') {
// 1.1 Do something if input is invalid
const payload = {
error: `You can't do it, because you're Alex and your id is 111`
}
dispatch({type: 'GET_USER_FAIL', meta: id, payload, error: true})
return {payload, status: {error: true}}
}
// 2. Dispatch `PENDING` action
dispatch({type: 'GET_USER_PENDING'}, (meta: id))
// 3. Create payload for async function
const apiPayload = {id, token: state.token}
// 4. Obtain result from async function
const result = await fetch('/my-url', {body: apiPayload, method:'POST'}).then(res => res.json()).then(res => )
// 5. Check is request successful or failed (contains errors).
const status = result.ok && result !== 400 ? {success: true} : {error: true}
// 6. Get data from request object (typically accessible as `result.data`)
const payload = resultFromServer.data
if (resultFromServer.ok) {
// 7.if result successful -> dispatch `SUCCESS` action
dispatch({type: 'GET_USER_SUCCESS', meta: id, payload})
} else {
// 8.if result failed -> dispatch `FAIL` action
dispatch({type: 'GET_USER_FAIL', meta: id, payload, error: true})
}
// 9. Finally do/dispatch something
dispatch({type: 'GET_USER_FINALLY', meta: id, payload})
// 10. Resolve values (typical case for Redux-form, but about it later)
return {payload, status}
}
As you see, there are few steps here:
- Validate input before dispatching
pendingand Do something if input is invalid - Dispatch
PENDINGaction - Create payload for async function
- Obtain result from async function
- Check is request successful or failed (contains errors).
- Get data from request object (typically accessible as
result.data) - if result successful -> dispatch
SUCCESSaction - if result failed -> dispatch
FAILaction - Finally do/dispatch something
- Return values
In a big projects duplicating this lifecycle in each action isn't DRY (honestly, it'd be a crime.). Imagine that you have 300 similar actions in your app. Much logic will be duplicated.
Awral
Create Awral
Awrals are extendable, new awral nest parent.
Just call .of on parent awral and pass config differencies.
// More about config below
const baseAwral = Awral.of(config)
// This awral extends `baseAwral` + use specificConfig (overwrites methods, see next section)
const awralForSpecificLifecycle = baseAwral.of(specificConfig)
Configure Awral
// commonly used in Functional programming "ID" function
const id = (a) => a
const actionCreator = status => ({dispatch, ACTION_NAME, getState, ...rest}) => {
// Where `rest` are:
// - `payload` - result from asyncFunction preprocess throught `afterCheck`
// - `error` - if action `fail`ed
// - `meta` - some value obtained from `meta` function
dispatch({type: `${ACTION_NAME}_${status.toUpperCase()}`, ...rest})
}
// Default values sorted in the order of execution:
const commonAwral = Awral.of({
meta: id, // 0
failBeforePending: null, // 1
pending: actionCreator('pending'), // 2
fnPayload: id, // 3
check: id, // 5
transformResult: id, // 6
success: actionCreator('success'), // 7
fail: actionCreator('fail'), // 8
finally: actionCreator('finally'), // 9
resolve: id // 10
})
Above we discussed typical async action lifecycle, and Awral encapsulates it, but also provides manipulation with meta:
0. Create meta action property - meta
- Validate input before dispatching
pendingandfailif input is invalid -failBeforePending - Dispatch
{ACTION_NAME}_PENDINGaction -pending - Create payload for async function -
fnPayload - Obtain result from async function - automatically
- Check is request successful or failed (contains errors) -
check - Get data from request object (typically accessible as
result.data) -transformResult - if result successful -> dispatch
{ACTION_NAME}_SUCCESSaction -success - if result failed -> dispatch
{ACTION_NAME}_FAILaction -fail - Finally do/dispatch something -
finally - Return payload and status -
resolve
Let's discuss each property detailed:
import Awral from 'awral'
// Your async request to API
// mentioned below as 'async function'
const getUserFromServer = async ({id, token}) => {
// something async using 'fetch' or your XHR agent (any Promise)
return fetch('/my/url').then(res => res.json())
}
// "args" - arguments passed to your action
// Create `meta` for actions
const meta = args => {
// `getState()`, `dispatch()`, action prefix as `ACTION_NAME` are available in `this`
const {_meta} = this.getState()
return {data, _meta}
}
// Dispatch `fail` before `pending` and stops execution
// Returned value used as payload for `fail` action
const failBeforePending = args => {
// `getState()`, `dispatch()`, action prefix as `ACTION_NAME` are available in `this`
const state = this.getState()
const shouldFail = args === 111 && state.user === 'Alex'
if (shouldFail) {
const payload = {
error: 'Sorry, you can do it :('
}
// run `fail` action with this payload
// more about `fail` below
this._.fail({payload})
// abort continuing
return true
}
}
// Modify request data before passing it to async function
const fnPayload = args => {
// `getState()`, `dispatch()`, action prefix as `ACTION_NAME` are available in `this`
const state = this.getState()
// get something from state
const {token} = state
// returned value will be passed to your async function
return {
token,
id
}
}
// check is request successful or not
const check = result => result.ok && result.code !== 400
// `fetch()` wraps request into `data` property
// Obtain only data from API result
// And use it in `fail/success/finally`
const transformResult = result => result.data
const baseAwralUsedAcrossApp = Awral.of({fnPayload, check, transformResult})
const getUserAwral = baseAwralUsedAcrossApp.of({
failBeforePending
})
// Custom awral with specific handling
const GET_USER = getUserAwral(getUserFromServer)('GET_USER')
// Actions with typical lifecycle
const GET_ADMIN = baseAwralUsedAcrossApp(getAdmins)('GET_ADMIN')
const GET_LINKS = baseAwralUsedAcrossApp(getLinks)('GET_LINKS')
const GET_STUFF = baseAwralUsedAcrossApp(getStuff)('GET_STUFF')
By default there are 3 types of actions:
pending- dispatched beforeasyncFunctionexecutionfail- dispatched ifcheckfunction returnedfalsesuccess- dispatched ifcheckfunction returnedtrue
Global Awral lifecycle:
- get actions
meta
const actionCreator = status => ({dispatch, ACTION_NAME, getState, ...rest}) => {
dispatch({type: `${ACTION_NAME}_${status.toUpperCase()}`, ...rest})
}