This documentation is for the old Kea 1.0. To see the latest docs, click here!

Loaderes

When making network requests that fetch data, you end up writing the same thing over and over again:

1) an action to make the request
2) an action to handle success
3) an action to handle errors
4) a reducer to store the data
5) a reducer to store the loading state
6) a listener to make the request and return the data

The kea-loaderes plugin abstracts this pattern into a system of loaders.

Installation

First install the kea-loaders and kea-listeners packages:

# if you're using yarn
yarn add kea-loaders kea-listeners

# if you're using npm
npm install --save kea-loaders kea-listeners

Then install the plugin:

import { loadersPlugin } from 'kea-loaders'
import listenersPlugin from 'kea-listeners'
import { resetContext } from 'kea'

resetContext({
  createStore: true,
  plugins: [ loadersPlugin({ /* options */ }), listenersPlugin ]
})

Configuration options

The plugin takes the following options:

loadersPlugin({
  // Called when the listener throws an error
  // Feel free to alert the user in a nicer way,
  // for example by displaying a notification.
  // Also connect this to your bug tracking software.
  onError ({ logic, error, reducerKey, actionKey }) {
    console.error(`Error in ${actionKey} for ${reducerKey}:`, error)
  }
})

Sample usage

import { kea } from 'kea'

export const projectLogic = kea({
  key: props => props.id,

  loaders: ({ values, props }) => ({
    project: {
      loadProject: async (id = props.id) => projectsService.get(id),
    },

    // the above code creates these actions:
    // - loadProject: params => params
    // - loadProjectSuccess: project => ({ project })
    // - loadProjectFailure: error => ({ error })

    // ... and these reducers:
    // - project (whatever the loadProject loader returns)
    // - projectLoading (true or false)

    apiKeys: [[], { // default to [] instead of null
      loadApiKeys: async () => apiKeysService.find({ query: { projectId: props.id } }),
      createApiKey: async () => {
        const apiKey = await apiKeysService.create({ projectId: props.id })
        return [...(values.apiKeys || []), apiKey]
      },
    }],

    // the above code creates these actions:
    // - loadApiKeys: true
    // - loadApiKeysSuccess: apiKeys => ({ apiKeys })
    // - loadApiKeysFailure: error => ({ error })
    // - createApiKey: true
    // - createApiKeySuccess: apiKeys => ({ apiKeys })
    // - createApiKeyFailure: error => ({ error })

    // ... and these reducers:
    // - apiKeys (whatever the loadProject loader returns)
    // - apiKeysLoading (true or false)
  })

  // start the loaders after mounting the logic
  events: ({ actions }) => ({
    afterMount: () => {
      actions.loadProject()
      actions.loadApiKeys()
    },
  }),
})

export function Project ({ id }) {
  const { project, projectLoading } = useValues(projectLogic({ id }))
  const { loadProject } = useActions(projectLogic({ id }))

  return (
    <div>
      {projectLoading ? (
        <div>Loading project</div>
      ) : project ? (
        <div>Project: {project.id}</div>
      ) : (
        <div>No project found!</div>
      )}

      <button onClick={() => loadProject(id)}>Reload project</button>

      <button onClick={() => loadApiKeys()}>Load API keys</button>
      {/* In case your loader function takes no arguments, we recommend */}
      {/* passing onClick={() => loadProject()} in the click handler.   */}
      {/* Using just onClick={loadProject} will pass the click event as */}
      {/* a param to the action and that may cause unexpected issues... */}
    </div>
  )
}