0.8.3 • Published 6 years ago

lessdux v0.8.3

Weekly downloads
4
License
MIT
Repository
github
Last release
6 years ago

What is This?

redux is a great library for managing the state of your applications. It does, however, require a lot of boilerplate code. If you've worked with raw redux before then you know that feeling when you have to write a new action constant, a new action creator, and maybe even a new reducer for doing something simple like fetching an object from a database. Furthermore, when you are dealing with async resources, (resources that are fetched asynchronously from remote services), there is even more boilerplate for when the resource is being created, loaded, when an error is thrown, etc. Libraries like apollo-client that use redux take care of this under the hood, but if you don't have a GraphQL service then you are out of luck. lessdux is a much lower level library that makes it easier to deal with async resources in raw redux and also provides some nice utilities in the process.

Installation

To install, run yarn add lessdux or npm install lessdux.

Create a Reducer

createReducer takes the initialState of the reducer and an optional reducerMap and onStateChange callback.

reducerMap is simply an object that maps action constants to reducer functions with signature (state, action) => newState. It is not necessary to provide this argument as the reducer can function without it.

The secret sauce of the reducer returned from createReducer is its ability to automatically react to async resource 'action type prefixes'. This means that whenever an action is dispatched with action.type = ${ACTION_TYPE_PREFIX}_${RESOURCE_NAME}, e.g.FETCH_USER, if the reducer's slice of the state has a key that is the camelCased name of the resource, i.e. user, it will automatically set its loading prop to true and its failedLoading prop to false. More on this below.

The onStateChange callback is called whenever the reducer returns a new state object. This is useful for reacting to automatic or global state changes for doing things like rebuilding tooltips or reattaching DOM event listeners in one place.

This is how you might call it:

/* reducers/user.js */
import createReducer from 'lessdux'

// Reducer
export default createReducer(
  { username: 'placeholder' }, // Initial state
  {
    SET_USERNAME: (state, action) => { ...state, username: action.payload.username } // Return new state object
  },
  ReactTooltip.rebuild // Reattach tooltips
)

Create a Resource

Resources are the individual objects that you fetch from your remote services. These objects could be lists (arrays), mappings (objects), or even primitive types like strings or numbers. For every resource you use you'll have to define a prop-types shape and an initial state for your reducer. Since the only variable here is the shape of the actual object, we made a utility for doing this:

/* reducers/user.js */
import createReducer, { createResource } from 'lessdux'

// Shapes
const {
  shape: statusesShape,
  initialState: statusesInitialState
} = createResource(PropTypes.arrayOf(PropTypes.string)) // This is the shape of the object returned from your remote services, an array of strings in this case
export { statusesShape } // Export shapes for use in components `propTypes`, E.g. Component.propTypes = { statuses: statusesShape.isRequired }

// Reducer
export default createReducer(
  { username: 'placeholder', statuses: statusesInitialState } // Initial state
  // ...
)

The returned shape will look like this:

const statusesInitialState = PropTypes.shape({
  loading: PropTypes.bool.isRequired,
  data: PropTypes.arrayOf(PropTypes.string),
  failedLoading: PropTypes.bool.isRequired
})

You may also pass the following configuration object to add the extra states. More on this below.

const {
  shape: statusesShape,
  initialState: statusesInitialState
} = createResource(PropTypes.arrayOf(PropTypes.string), {
  // Add all extra states
  withCreate: true,
  withUpdate: true,
  withDelete: true
})
export { statusesShape }

Create Actions

You will also need to create the respective actions for this resource. We also have a utility for that:

/* actions/user.js */
import { createActions } from 'lessdux'

/* Actions */

// Statuses
export const statuses = createActions('STATUSES')

/* Action Creators */

// Statuses
export const fetchStatuses = () => ({ type: statuses.FETCH })

createActions takes a CONSTANT_CASE resource name and returns an object with action constants for FETCH, RECEIVE, and FAIL_FETCH. No need to append the resource name yourself. You may also pass the configuration object for creating, updating, and deleting just like with createResource.

If you want to add custom action constants, you may do so like so:

export const statuses = {
  ...createActions('STATUSES'),
  LIKE: 'LIKE_STATUSES'
}

The Different States and 'Action Type Prefixes'

These are all the 'action type prefixes' that a reducer reacts to and the properties they set on the resource shape object:

const actionTypePrefixesToState = {
  CREATE: { creating: true, failedCreating: false },
  RECEIVE_CREATED: { creating: false, failedCreating: false },
  FAIL_CREATE: { creating: false, failedCreating: true },
  UPDATE: { updating: true, failedUpdating: false },
  RECEIVE_UPDATED: { updating: false, failedUpdating: false },
  FAIL_UPDATE: { updating: false, failedUpdating: true },
  DELETE: { deleting: true, failedDeleting: false },
  RECEIVE_DELETED: { deleting: false, failedDeleting: false },
  FAIL_DELETE: { deleting: false, failedDeleting: true },

  FETCH: { loading: true, failedLoading: false },
  RECEIVE: { loading: false, failedLoading: false },
  FAIL_FETCH: { loading: false, failedLoading: true }
}

RECEIVE_${any} actions will also set the value of data to the value of action.payload.${camelCaseResourceName}. This means you don't have to write a function just to receive the result of a FETCH. Pretty neat right?

Render If?

Naturally, you'll want to render different things depending on the state of the resource and we have a component that does just that:

/* statuses-list.js */
import { RenderIf } from 'lessdux'

import { statusesShape } from '../reducers/user'
import Spinner from '../components/spinner'

const StatusesList = ({ statuses }) => (
  <div>
    Look at Your Statuses
    <ul>
      <RenderIf
        loading={<Spinner />} // Rendered while loading
        data={statuses.data && statuses.data.map(s => <li>{s}</li>)} // Rendered when data is ready
        failedLoading="There was an error loading your statuses..." // Rendered on errors
      />
    </ul>
  </div>
)

StatusesList.propTypes = {
  statuses: statusesShape.isRequired
}

export default StatusesList

This is the full list of props RenderIf can receive:

  • resource // The resource object shape, (required)
  • creating // Render while creating
  • loading // Render while loading
  • updating // Render while updating
  • deleting // Render while deleting
  • done // Render when done and data is ready
  • failedCreating // Render on creating failure
  • failedLoading // Render on loading failure
  • failedUpdating // Render on updating failure
  • failedDeleting // Render on deleting failure
  • loadingExtra // Render when any value in extraLoadingValues is true, (defaults to loading prop)
  • failedLoadingExtra // Render when any value in extraFailedValues is true, (defaults to failedLoading prop)
  • extraLoadingValues // Array of extra values that if truthy, signify that the resource is still loading
  • extraValues // Array of extra values that if null or undefined signify that the resource failed to load
  • extraFailedValues // Array of extra values that if truthy, signify that the resource failed loading

Example Usage with redux-saga

This is how you might use it with a library like redux-saga:

import { takeLatest, call, put, select } from 'redux-saga/effects'

import * as userActions from '../actions/user'
import myAPICaller from '../utils/my-api-caller'

/**
 * Fetches the statuses.
 */
function* fetchStatuses() {
  try {
    const statuses = yield call(myAPICaller, '/statuses')

    yield put(action(userActions.statuses.RECEIVE, { statuses }))
  } catch (err) {
    yield put(errorAction(userActions.statuses.FAIL_FETCH, err))
  }
}

/**
 * Likes all the statuses.
 */
function* likeStatuses() {
  try {
    yield put(action(userActions.statuses.UPDATE))

    const statuses = yield call(myAPICaller, '/statuses/like')

    yield put(action(userActions.statuses.RECEIVE_UPDATED, { statuses }))
  } catch (err) {
    yield put(errorAction(userActions.statuses.FAIL_UPDATE, err))
  }
}

/**
 * The root of the user saga.
 */
export default function* arbitratorSaga() {
  // Statuses
  yield takeLatest(userActions.statuses.FETCH, fetchStatuses)
  yield takeLatest(userActions.statuses.LIKE, likeStatuses) // This is our custom action!
}
0.8.3

6 years ago

0.8.2

6 years ago

0.8.1

6 years ago

0.8.0

6 years ago

0.7.3

6 years ago

0.7.2

6 years ago

0.7.1

6 years ago

0.7.0

6 years ago

0.6.1

6 years ago

0.6.0

6 years ago

0.5.0

6 years ago

0.4.2

6 years ago

0.4.1

6 years ago

0.4.0

6 years ago

0.3.0

6 years ago

0.2.0

6 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.1.0

6 years ago

0.0.1

6 years ago