Skip to content

Instantly share code, notes, and snippets.

@heygrady
Last active February 24, 2019 01:20
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save heygrady/2ebb3b4580891f3d87783fbe001dc703 to your computer and use it in GitHub Desktop.
Save heygrady/2ebb3b4580891f3d87783fbe001dc703 to your computer and use it in GitHub Desktop.
Core concepts of redux modules

Redux modules

Our applications have grown organically and our current collection of actions, reducers and selectors are due for an upgrade. This document is an attempt to outline an better way to organize our redux code.

The biggest changes here are the introduction of "modules" and redux-sagas.

Core concepts

  • modules — a grouping of actions, constants, reducers, etc. that all deal with the same portion of the state.
  • actions — action creators
  • constants — used by action creators, reducers and sagas
  • reducers — functions that update a specific portion of the state
  • sagas — generator functions that handle actions, like a thunk but can work more like a reducer
  • selectors — functions that read values from the state.

New libraries to introduce

  • redux-actions — Utility functions for creating actions and reducers. The benefit is that it encourages a standard action format and functional, testable reducers.
  • redux-saga and redux-saga-watch-actions — Manage async tasks and watch for actions. Solves issues where we need to take further action when an unrelated action happens.
  • @comfy/redux-selectors — Utility functions for creating memoized, composable selectors.

A quick note on Immutable

The biggest missing piece in this document is a story on how/if Immutable.js fits into this. We're currently using Immutable JS to wrap objects that come from our API layer. We're also using it in our reducers to enforce immutability. However, our application is somewhat uneven. There are numerous areas when object are partially immutable, or are unreliably immutable. At any given point in our app it is hard to know if the object we're dealing with is an Immutable JS object or not.

Some light reading on the internet reveals that:

  1. Immutable.js adds a large footprint to our app bundles
  2. The next version of Immutable.js has been stuck in beta for a while; documentation is lacking
  3. Immutable.js adds complexity to our code
  4. Immutable.js is not required to avoid mutation: JavaScript natively supports immutable alterations with the Object/Array spread operators

The biggest benefit of Immutable.js is the use of records to do loose prop-type validation on the objects that come from our API. One possibility is to resurrect something like prop-types for reducers or json-schema for reducers to do basic state validatio. Digging a little deeper in to both solutions reveals that most people have dropped prop-type checking in favor of flowtype or typescript for reducers.

Top-level src/modules/ folder

Let's dig in to this proposal. The first thing we want to do is group our actions, constants, reducers, etc. by feature instead of by type. This means we want to stop doing global src/actions/, src/reducers/ and src/selectors/ folders.

We will create a new top-level folder, src/modules, which will hold all of our redux "modules". A module is a logical grouping of actions, reducers, etc.; grouped by feature. In order simplify the decisions about feature grouping, the modules folder should be organized to mirror the state. Each top-level key in the redux state should have a corresponding "module" in the src/modules/ folder.

Current structure: grouped by type

Today we have things organized by functional type. Below you can see a folder structure where high-level redux concepts each get a top-level folder in src/. This is fine for smaller apps but can get cumbersome as apps grow. One particular issue is how related actions, constants, and selectors are separated from each other in the folder structure.

It's worth noting that the alphabetical structure of the folders has a real-world effect on how we explore the code. Anyone using the folder tree to navigate the code will run into issues when they open up the components/ folder. Suddenly the actions and reducers are miles away from each other in the tree view. Of course, an advanced developer knows all of the quick keys that makes it easy to find and open files. However, it shouldn't be discounted that grouping by type can make it difficult to explore the code.

Key point: Grouping by type can make it difficult to explore the code.

src/
  actions/ <-- all redux actions
  components/
  constants/ <-- mixed app and redux constants
  media/
  reducers/ <-- all redux reducers
  routes/
  sagas/ <-- all redux sagas
  selectors/ <-- all redux selectors
  store/
  styles/
  utils/
  index.js

Proposed structure: grouped by feature

This proposal advocates grouping redux modules together by feature. Below you can see what it looks like for a single feature, the imaginary featureName. You can see that there are no longer any top-level src/action/, etc. folders. Instead, the actions, etc. that apply to each feature are grouped together. In the final structure there would be a src/modules/${key} folder for each top-level key in the redux state.

It's worth noticing that a requirement of a module, that it default export its rootReducer, makes it much easier to compose our apps reducers together. We'll see this in more detail further down.

Key points:

  • Grouping by feature keeps related code in the same place.
  • Exporting the rootReducer makes the the store composable
src/
  components/
  constants/ <-- app constants (no redux constants)
  media/
  modules/
    featureName/ <-- grouped by feature
      actions/
      constants/
      reducers/
      sagas/
      selectors/
      index.js <-- exports the rootReducer for this feature
    index.js <-- combines all rootReducers for the store
  routes/
  store/
  styles/
  utils/
  index.js

General guidelines:

  • Each top-level "key" in the state gets a module folder
  • Each "module" should export a rootReducer by default
  • Each module may optionally export a rootSaga as well
  • The modules/index.js combines the module's reducers for the store

Top-level state

Each "key" in the state...

For this walk-through, let's imagine that we have a state that looks like the following example. Each of the keys below represents a deeper part of the state tree. For this proposal we will be exploring the app portion of the state.

// example: top-level redux state
const state = {
  app, // <-- we'll be exploring this feature
  experiments,
  forms,
  localStorage,
  modals,
  map,
  routes,
  search,
  ui,
}

src/modules/ folder structure

... gets a module folder.

Below you can see that we've created a folder for each top-level key in our state. The idea is that each "module" would contain the actions, constants, reducers, etc. for that portion of the state.

  • Each module folder should have an index.js that default exports a reducer
  • The modules/index.js file should use combineReducers to create the rootReducer we use in the redux store
src/modules/
  app/
  experiments/
  forms/
  localStorage/
  modals/
  map/
  routes/
  search/
  ui/
  index.js <-- combines the rootReducers and rootSagas for the store

modules/index.js

The modules/index.js combines the module's reducers for the store

  • Notice that we import the rootReducer from every "module" and combine them.
  • Notice that we import the rootSaga too!
  • Notice that every module has a reducer but not every module has a saga
import { combineReducers } from 'redux'
import { combineSagas } from 'redux-saga-watch-actions'

import app, { rootSaga as appSaga } from './app'
import experiments from './experiments'
import forms from './forms'
import localStorage from './localStorage'
import modals from './modals'
import map, { rootSaga as mapSaga } from './map'
import routes from './routes'
import search from './search'
import ui, { rootSaga as uiSaga } from './ui'

const rootReducer = combineReducers({
  app,
  experiments,
  forms,
  localStorage,
  modals,
  map,
  routes,
  search,
  ui,
})

export const rootSaga = combineSagas(appSaga, mapSaga, uiSaga)
export default rootReducer

The app/ module

Each top-level module will be expected to have a folder for each of the module concepts. You can see below that our app/ module contains folders for actions, constants, reducers, etc.

src/modules/app/
  actions/
  constants/
  reducers/ <-- exports a rootReducer
  sagas/ <-- exports a rootSaga
  selectors/
  index.js <-- exports the rootReducer and rootSaga for this module

General guidelines:

  • A module's index.js
    • should export a rootReducer that combines the child reducers of that module's state tree
    • may optionally export a rootSaga that combines all of the sagas
    • should not export actions, constants or selectors
  • Each folder (actions/, constants/, reducers/, etc.) should contain an index file that exports the interface for the top-level of that module's state tree.

app state

Here we'll explore the shape for the app portion of the redux state. Every module has a different focus; here we're imagining that the app is responsibility for basic housekeeping related to the app as a whole. Other parts of the state, like the map relate to more specific features.

We're designing our app state to handle some basic values related to bootstrapping the app and managing the current user. Notice that hasMounted and isMobile are simple booleans while user is a deeply complex object.

General guidelines:

  • Each key in the state gets its own reducer
  • Deeply nested objects get deeply nested reducers
const state = {
  hasMounted,
  isMobile,
  user: {
    addresses: {
      billing: {
        // ...
      },
      home: {
        // ...
      },
    },
    email,
    firstName,
    lastName,
    meta: {
      error,
      isLoading,
      loadedAt,
    },
    phone,
  },
}

Sneak peak at full structure

Before we get too deep into this, let's take a look at the final structure of our app module.

  • Every concept gets its own folder
  • Every key gets its own reducer
  • Complex objects get dedicated selectors
src/modules/app/
  actions/index.js
  constants/index.js
  reducers/
    userReducer/
      addresses.js
      index.js
      metaReducer.js
    index.js
  sagas/
    fetchUserSaga.js
    loginUserSaga.js
    logoutUserSaga.js
    index.js
  selectors/
    index.js
    user.js
  index.js

Actions

Following along with our app example, let's look at the actions/index.js file.

General guidelines:

  • Create action creators using createAction
  • Prefer props objects for complex payloads
  • Always name the action to match the constant exactly
  • Prefer actions that describe what happened/should happen
    • hasMounted — declares that the app is fully mounted
    • beginFetchingUser — declares that the process for fetching a user is beginning. You can infer from this that the user is currently loading, used to toggle the user.meta.isLoading value.
    • loginUser — handled by a saga, manages the process of validating and fetching a user's profile data.
  • Avoid setFoo unless there's no better name. The goal is to have descriptive actions that describe state changes, not dictate.
  • Never create an action by hand; always use an action creator to create an action

app/actions/index.js

You can see below that we're importing constants and using createAction to create standardized action creators. You can also see that we have some lifecycle actions for the process of fetching and receiving the user.

Using createAction allows us to quickly mock up our action creators. As an additional benefit, it enforces the use of the flux standard actions specification, which specifies how action objects should look. The end result is that we can quickly create high-quality, standardized action creators.

Key point: createAction makes it painless to create high-quality, feature rich action creators.

import { createAction } from 'redux-actions'
import {
  BEGIN_FETCHING_USER,
  END_FETCHING_USER,
  ERROR_FETCHING_USER,
  FETCH_USER,
  HAS_MOUNTED,
  IS_MOBILE,
  LOGIN_USER,
  LOGOUT_USER,
  RECEIVE_USER,
} from '../constants'

export const hasMounted = createAction(HAS_MOUNTED)
export const isMobile = createAction(IS_MOBILE)

export const loginUser = createAction(LOGIN_USER)
export const logoutUser = createAction(LOGOUT_USER)

export const fetchUser = createAction(FETCH_USER)
export const beginFetchingUser = createAction(BEGIN_FETCHING_USER)
export const endFetchingUser = createAction(END_FETCHING_USER)
export const errorFetchingUser = createAction(ERROR_FETCHING_USER)
export const receiveUser = createAction(RECEIVE_USER)

Using an action creator

Any action creator created using createAction will have a type and a payload. It will take the first argument and make that the payload. Below you can see that you can pass nothing, pass scalar values or pass in complex objects. In every case, the arguments become the action payload. There is an ability to construct custom payloads using a payloadCreator function if you desire; but the default payload creator covers most use cases.

At first glance it might feel wrong that the payload is created in such a loose manner. In practice, this is less of a concern. The important part is establishing a consistent convention in your code base.

Key point: always use a props object as your payload unless your passing a single scalar value.

// example: exploring how action creators work
import { hasMounted, setMobile, receiveUser, errorFetchingUser } from 'modules/app/actions'

// ✅ pass nothing; no payload
hasMounted() // { type: HAS_MOUNTED }

// ✅ pass a scalar value; simple payload
isMobile(true) // { type: IS_MOBILE, payload: true }

// ✅ Prefer: use a props object to pass complex data
receiveUser({ user }) // { type: RECEIVE_USER, payload: { user } }

// ❌ Avoid: using complex objects directly as the payload
receiveUser(user) // { type: RECEIVE_USER, payload: user }

// ❌ Avoid: passing multiple args; requires custom payload creator
receiveUser(user, headers, jobId) // { type: RECEIVE_USER, payload: user }

// ✅ Prefer: use a props object
receiveUser({ user, headers, jobId }) // { type: RECEIVE_USER, payload: { user, headers, jobId } }

// ✅ pass errors
const error = new Error('whoops!')
errorFetchingUser(error) // { type: ERROR_FETCHING_USER, payload: error, error: true }

Constants

Constants are probably the most annoying part of a redux application. We typically only use constants indirectly. It can feel painful to create a constant only to immediately jam it into an action creator. At the same time, constants are the glue that holds the app together.

It's important to see constants as separate from actions and reducers. It is a common mistake to lump constants into your actions files or into your reducer files. In practice, a constant is used in actions, reducers and sagas and deserves a place along side them.

General guidelines:

  • Treat constants as a top-level concept alongside actions, reducers and sagas.
  • Namespace your constants
  • Prefer actions that describe what happened
    • an action declares what happened
    • a reducer decides what should happen
    • a saga will take additional action
  • Avoid SET_FOO unless there's no better name; SET_FOO is a code smell test that indicates you are misusing redux.

app/constants/index.js

Below you can see the example constants we used in our action creators. Notice that we namespace all of the constants to avoid name collisions with any other modules. For instance, if another module had the concept of HAS_MOUNTED it would be possible to accidentally trigger that action in both places. Using namespaces avoids any possibility of collisions.

  • Notice that they are grouped together by use; not alphabetized
  • Some of these constants are for reducers, others for sagas
export const HAS_MOUNTED = '@@package-name/app/HAS_MOUNTED'
export const IS_MOBILE = '@@package-name/app/IS_MOBILE'

export const LOGIN_USER = '@@package-name/app/LOGIN_USER'
export const LOGOUT_USER = '@@package-name/app/LOGOUT_USER'

export const FETCH_USER = '@@package-name/app/FETCH_USER'
export const BEGIN_FETCHING_USER = '@@package-name/app/BEGIN_FETCHING_USER'
export const END_FETCHING_USER = '@@package-name/app/END_FETCHING_USER'
export const ERROR_FETCHING_USER = '@@package-name/app/ERROR_FETCHING_USER'
export const RECEIVE_USER = '@@package-name/app/RECEIVE_USER'

Reducers

For beginners, reducers are pretty confusing. The boilerplate reducer code uses switch cases and leaves a lot up to the implementor. Developers that are new to redux are often flummoxed and end up crafting unwieldy, difficult to maintain reducers.

However, reducers can become quite simple when you follow a few guidelines. One the biggest mistakes a redux developer can make is to create one massive switch/case reducer for each part of the state. In reality, there should be a reducer for every key in the state tree. Breaking your reducers into smaller functions will greatly reduce the complexity of your reducers.

General guidelines:

  • Use handleActions and combineReducers to create your reducers
  • Avoid switch/case
  • Prefer small reducer files (less than 100 lines)
    • One reducer per file (usually)
    • Each reducer handles one key
    • Sub keys each get their own reducer
    • Each reducer is a function
  • Use combineReducers to sketch out the shape of your state tree
  • Use handleActions to capture multiple actions for individual keys
  • Use handleAction when your key only ever deals with one action
  • Use separate reducers for complex keys (any key with sub keys)

app/reducers/index.js

Each reducer is a function

  • Notice that the hasMounted and isMobile reducers both use handleAction to manage a single action
  • Notice that the hasMounted reducer completely ignores state and action and simply returns true
  • Notice that the isMobile reducer expects a payload, but assumes true if the payload is missing
  • Notice that the user key is punted to the userReducer
import { combineReducers } from 'redux'
import { handleAction } from 'redux-actions'
import {
  HAS_MOUNTED,
  IS_MOBILE,
} from '../constants'

import userReducer from './userReducer'

const rootReducer = combineReducers({
  hasMounted: handleAction(HAS_MOUNTED, () => true, false),
  isMobile: handleAction(
    IS_MOBILE,
    (state, action) => {
      const { payload } = action
      return payload === undefined ? true : payload
    },
    false
  ),
  user: userReducer,
})
export default rootReducer

app/reducers/userReducer/index.js

Each key gets its own reducer

Our userReducer needs to handle multiple complex keys, so we create a folder for it. It's important to notice that we never simply spread the user object into the state. Instead, we have an individual reducer for every sub key of the user object. This allows us to be very precise about how the payload is merged into the state.

Individual key reducers allows us to handle keys that need special consideration. Imagine if the email key came back from the API as "username".

Key-level reducers are especially helpful for the addressesReducer, which handles the various aspects of the address, which is likely quite complex. We're able to punt that complexity up the tree, which makes the reducer code and each level of the tree much easier to maintain.

  • Notice the userKeyReducer is configurable, allowing the same reducer to be used for multiple keys
  • Notice the addressesReducer handles the RECEIVE_USER action itself
  • Notice the metaReducer manages meta data for the user

Key points:

  • Each key gets its own reducer
  • Complex keys get separate reducers
import { combineReducers } from 'redux'
import { handleAction } from 'redux-actions'
import {
  RECEIVE_USER,
} from '../../constants'

import addressesReducer from './addressReducer'
import metaReducer from './metaReducer'

const userKeyReducer = (key) => handleAction(
  RECEIVE_USER,
  (state, action) => {
    const { payload } = action
    const { user } = payload
    return user[key]
  },
  null
)

const userReducer = combineReducers({
  addresses: addressesReducer,
  email: userKeyReducer('username'),
  firstName: userKeyReducer('firstName'),
  lastName: userKeyReducer('lastName'),
  meta: metaReducer,
  phone: userKeyReducer('phone'),
})

export default userReducer

app/reducers/userReducer/metaReducer.js

Use handleActions to capture multiple actions for individual keys

  • Use handleActions for managing multiple actions
  • Use handleAction for managing single actions
  • Notice the error reducer is receiving a special "error" action
  • Notice the loadedAt reducer is looking for RECEIVE_USER to create a timestamp
import { combineReducers } from 'redux'
import { handleAction, handleActions } from 'redux-actions'

import {
  BEGIN_FETCHING_USER,
  END_FETCHING_USER,
  ERROR_FETCHING_USER,
  RECEIVE_USER,
} from '../../constants'

const metaReducer = combineReducers({
  isLoading: handleActions({
    [BEGIN_FETCHING_USER]: () => true,
    [END_FETCHING_USER]: () => false
  }, null),
  error: handleAction(
    ERROR_FETCHING_USER,
    (state, action) => action.error
    null
  ),
  loadedAt: handleAction(
    RECEIVE_USER,
    () => Date.now()
    null
  )
})

export default metaReducer

Sagas

A saga is a somewhat advanced concept. Sagas cover most of the ground that thunks cover but they have some additional features that make them desirable. Typically an app will need both thunks and saga to cover different cases. If you are new to sagas you may be intimidated the usage of generator functions. In practice, redux-saga handles all of the annoying bits of working with generators.

The big advantage of sagas is that they can listen for actions that are also handled by reducers. For instance, if the MapContainer dispatches an updateCenter action, that could be caught by the a reducer in the map module and by a saga (in a totally different module) that fetches data for the side bar.

Here we're demonstrating the sagas for our app module.

app/sagas/index.js

The index file for a module's sagas is typically a simple mapping of actions to sagas. Sagas, like reducers, need to be attached to the actions that trigger them. Here we're using a simple utility function that handles that use-case much like handleActions does for a reducer.

Redux-saga also provides many advanced options for complex scenarios that are not typical. If you need to manage something like real-time messaging or one-off saga, you might enjoy exploring the docs.

Here you can see that we're mapping our async actions to our sagas. It's important to note that sagas can call each other. For instance, imagine that our loginUserSaga authenticates the user and then fetches their profile. In that scenario, the loginSaga would likely be the only place in the app that dispatches the fetchUser action.

  • Use watchActions to map a saga to an action
  • Use combineSagas (not shown here) to merge sagas together
  • Each saga gets its own file
  • Export a default rootSaga from your sagas/index.js
import { watchActions } from 'redux-saga-watch-actions'

import {
  FETCH_USER,
  LOGIN_USER,
  LOGOUT_USER,
} from '../constants'

import fetchUserSaga from './fetchUserSaga'
import loginUserSaga from './loginUserSaga'
import logoutUserSaga from './logoutUserSaga'

const rootSaga = watchActions({
  [FETCH_USER]: fetchUserSaga,
  [LOGIN_USER]: loginUserSaga,
  [LOGOUT_USER]: logoutUserSaga,
})

export default rootSaga

app/sagas/fetchUserSaga.js

Each saga gets its own file

A saga is a generator function that yields special "effects" that redux-saga responds to. In practice, you can do everything in a saga that you can do in a thunk but you can use a friendly async styles instead of writing promise chains. Because redux-saga runs your generator for you, it feels more like using async/await to manage asyncronous actions, like API calls. Under the hood, redux-saga relies on the advanced features of generators to make this work.

One stumbling block for saga is the use of effects. For instance, you can't dispatch actions, you need to put them instead. In practice, once you realize that put is the same as dispatch, it's easy to convert a thunk into a saga.

The big benefits of sagas are the friendly async syntax (no promise chains) and the ability to capture actions.

Here you can see a pretty standard API request. We use lifecycle actions to let the app know how the API request is proceeding. This allows for us to show loading and error messages as necessary. Once we have the data, we dispatch it to the reducer with the receiveUser action.

  • Use put like you would dispatch; remember to yield put(...)
  • Use select (sort of) like you would getState
  • Use call to call external functions, especially async calls
  • Notice const user = yield result.json()
    • Anything you yield will be managed by redux-saga
    • If you yield a promise, the result will be returned
import { call, put } from 'redux-saga/effects'
import {
  beginFetchingUser,
  endFetchingUser,
  errorFetchingUser,
  receiveUser,
} from '../actions'

const fetchUserSaga = function*(action) {
  yield put(beginFetchingUser())
  try {
    const { payload } = action
    const username = payload
    const url = `${API_URL}/user/${username}`
    const result = yield call(fetch, url)
    const user = yield result.json()
    yield put(receiveUser({ user }))
  } catch (error) {
    yield put(errorFetchingUser(error))
  }
  yield put(endFetchingUser())
}
export default

Selectors

Selectors are often neglected in a redux application. It's all too common for developers to manually select values from deep in the redux state. Writing ad-hoc selectors inside your components is an anti-pattern that can make refactoring impossible.

Selector functions make it easier to refactor your app later if the state needs to change shape. Common issues, like missing properties, can be handled centrally instead of deep within the app, everywhere they're needed.

Here we use @comfy/redux-selectors to smooth over some of the boilerplate of creating high quality selectors. It has some advanced features that make it easy to create complex, well memoized selectors. For the example here, using createSelector means never having to check for the existence of a key. It will return undefined if a key doesn't exist, even a deep key.

app/selectors/index.js

  • Name all selectors like selectFoo
  • Never name a selector like getFoo
  • Avoid the temptation to name selectors like fooSelector
  • Use createSelector to create simple selectors
  • Use composeSelectors to chain selectors together
  • Notice that selectHasMounted composes selectApp
  • Notice that selectUserIsLoading composes selectUser with selectIsLoading
import { createSelector, composeSelectors } from '@comfy/redux-selectors'

import {
  selectAddresses,
  selectHomeAddress,
  selectBillingAddress,

  selectEmail,
  selectFirstName,
  selectLastName,
  selectPhone,

  selectIsLoading,
  selectLoadedAt,
  selectError,
} from './user'

export const selectApp = createSelector('app')

export const selectHasMounted = composeSelectors(selectApp, 'hasMounted')
export const selectIsMobile = composeSelectors(selectApp, 'isMobile')

export const selectUser = composeSelectors(selectApp, 'user')

export const selectUserAddresses = composeSelectors(selectUser, selectAddresses)
export const selectUserHomeAddress = composeSelectors(selectUser, selectHomeAddress)
export const selectUserBillingAddress = composeSelectors(selectUser, selectBillingAddress)

export const selectUserEmail = composeSelectors(selectUser, selectEmail)
export const selectUserFirstName = composeSelectors(selectUser, selectFirstName)
export const selectUserLastName = composeSelectors(selectUser, selectLastName)
export const selectUserPhone = composeSelectors(selectUser, selectPhone)

export const selectUserIsLoading = composeSelectors(selectUser, selectIsLoading)
export const selectUserLoadedAt = composeSelectors(selectUser, selectLoadedAt)
export const selectUserError = composeSelectors(selectUser, selectError)

app/selectors/user.js

Selectors simply read values from the state that is passed into them.

  • Notice that selectAddresses expects state to be a user object. This enables this selector to be used deep within a component if you have a user object and need to read the addresses key.
  • Notice that selectIsLoading composes selectMeta. Again, this allows for selecting the meta.isLoading value directly from the user object.
import { createSelector, composeSelectors } from '@comfy/redux-selectors'

export const selectAddresses = createSelector('addresses')
export const selectHomeAddress = composeSelectors(selectAddresses, 'home')
export const selectBillingAddress = composeSelectors(selectAddresses, 'billing')

export const selectEmail = createSelector('email')
export const selectFirstName = createSelector('firstName')
export const selectLastName = createSelector('lastName')
export const selectPhone = createSelector('phone')

export const selectMeta = createSelector('meta')
export const selectIsLoading = composeSelectors(selectMeta, 'isLoading')
export const selectLoadedAt = composeSelectors(selectMeta, 'loadedAt')
export const selectError = composeSelectors(selectMeta, 'error')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment