TODO! This example needs to be updated for Kea 1.0!

Everything below should still work, but might no longer be considered best practice

Example - Forms

Before Kea I used to dread forms in React. They would always require a lot of work to set up properly.

I could either use setState for the simplest one-component forms... or a bulky library like redux-form for a connected Redux-based form. Neither of those is a great option... and writing a pure-redux-form requires way too much boilerplate.

With Kea, forms are finally easy! Even the code to setup a form from scratch is quick to write, as you shall soon see.

What shall we do?

In this chapter we will first build a simple form that looks like this:

Go ahead, play with it!

... and then abstract the remaining boilerplate into a form builder.

0. Install kea-saga

We'll be using sagas in this example. To add support for them, install the kea-saga and redux-saga packages.

# if using yarn
yarn add kea-saga redux-saga

# if using npm
npm install kea-saga redux-saga --save

Then import sagaPlugin from kea-saga and add it to your resetContext():

import sagaPlugin from 'kea-saga'

resetContext({
  plugins: [
    sagaPlugin
  ]
})

Read here for more

1. Defining the features we need

If you played with the demo above, you'll see what we need to build the following features:

  • Default values
  • Custom validation rules
  • Show errors only after we have pressed "submit"
  • Disable the submit button when submitting
  • Async processing of the request

Let's build it piece by piece, starting with the data model.

2. Actions and reducers

So what do we need to keep track of in order to build this form?

At the very minimum, we'll need the following reducers:

  • An object values, which contains the form data (name, email and message)
  • A boolean isSubmitting, which knows if we're actively submitting the form or not
  • A boolean showErrors to know if we will show errors or not

These three reducers are enough to give us everything, except for validation rules and errors. We'll skip those for now.

What about the actions we can perform on the form? The minimum set is as follows:

  • setValue to update the value of one field (or setValues to update many simultaneously)
  • submit, to try to submit the form
  • submitSuccess, if the form was successfully submitted
  • submitFailure, if there was an error, e.g. a validation mismatch

Putting them together and adding defaults and propTypes gives us the following code:

// form.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { kea } from 'kea'

const defaults = {
  name: 'John Doe',
  email: '',
  message: ''
}

const propTypes = {
  name: PropTypes.string,
  email: PropTypes.string,
  message: PropTypes.string
}

export default kea({
  actions: () => ({
    setValue: (key, value) => ({ key, value }),
    setValues: (values) => ({ values }),

    submit: true,
    submitSuccess: true,
    submitFailure: true
  }),

  reducers: ({ actions }) => ({
    values: [defaults, PropTypes.shape(propTypes), {
      [actions.setValue]: (state, payload) => {
        return Object.assign({}, state, { [payload.key]: payload.value })
      },
      [actions.setValues]: (state, payload) => {
        return Object.assign({}, state, payload.values)
      },
      [actions.submitSuccess]: () => defaults
    }],

    isSubmitting: [false, PropTypes.bool, {
      [actions.submit]: () => true,
      [actions.submitSuccess]: () => false,
      [actions.submitFailure]: () => false
    }],

    showErrors: [false, PropTypes.bool, {
      [actions.submit]: () => true,
      [actions.submitSuccess]: () => false
    }]
  }),

  // ...
})

Seems clear enough? :)

3. The component

We could continue extending the logic by adding error handling, validations and actual submission logic, but since it's nice to see something tangible, let's first build the component itself!

A very crude version will look something like this:

// index.js

import React, { Component } from 'react'
import { connect } from 'kea'

import form from './form'

@connect({
  actions: [
    form, [
      'setValue',
      'submit'
    ]
  ],
  values: [
    form, [
      'values',
      'isSubmitting'
    ]
  ]
})
export default class FormComponent extends Component {
  render () {
    const { isSubmitting, values } = this.props
    const { submit, setValue } = this.actions

    const { name, email, message } = values

    return (
      <div>
        <div className='form-field'>
          <label>Name</label>
          <input type='text' value={name} onChange={e => setValue('name', e.target.value)} />
        </div>

        <div className='form-field'>
          <label>E-mail</label>
          <input type='text' value={email} onChange={e => setValue('email', e.target.value)} />
        </div>

        <div className='form-field'>
          <label className='block'>Message</label>
          <textarea value={message} onChange={e => setValue('message', e.target.value)} />
        </div>

        <button disabled={isSubmitting} onClick={submit}>
          {isSubmitting ? 'Submitting...' : 'Submit!'}
        </button>
      </div>
    )
  }
}

This code works! In fact, try it below:

The only problem: once you hit "submit", it will forever be stuck in the isSubmitting state.

We need to add some logic to make it actually do something.

4. Submitting the form

We will use the takeLatest helper to listen to the submit action and respond with either a submitSuccess or submitFailure action:

// form.js

import { put, delay } from 'redux-saga/effects'

export default kea({
  // actions, reducers, ...

  takeLatest: ({ actions, workers }) => ({
    [actions.submit]: function * () {
      const { submitSuccess, submitFailure } = this.actions

      // get the form data...
      const values = yield this.get('values')
      console.log('Submitting form with values:', values)

      // simulate a 1sec async request.
      yield delay(1000)

      if (true) { // if the request was successful
        window.alert('Success')
        yield put(submitSuccess())
      } else {
        window.alert('Error')
        yield put(submitFailure())
      }
    }
  })
})

Adding this code results in the following form:

Go ahead, write some data and try submitting it!

If you replace the yield delay(1000) part with an actual API call, this will be a fully functional form.

Only one thing left to do...

5. Errors and validation

We want to prevent an empty form from being submitted!

The easiest solution is to create a selector errors that depends on values. This selector checks the content of each field and returns an object describing which fields have errors.

We'll also create another selector hasErrors, which gives a simple yes/no answer to the question "does this form have errors?".

Finally, we'll check the value of hasErrors in the submit worker, and dispatch a submitFailure action in case the form doesn't pass the validation.

Something like this:

// form.js

// ...

const isEmailValid = (email) => /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test(email)

const missingText = 'This field is required'
const invalidEmail = 'Invalid e-mail'

export default kea({
  // actions, reducers, takeLatest, ...

  selectors: ({ selectors }) => ({
    errors: [
      () => [selectors.values],
      (values) => ({
        name: !values.name ? missingText : null,
        email: !values.email ? missingText : (!isEmailValid(values.email) ? invalidEmail : null),
        message: !values.message ? missingText : null
      }),
      PropTypes.object
    ],

    hasErrors: [
      () => [selectors.errors],
      (errors) => Object.values(errors).filter(k => k).length > 0,
      PropTypes.bool
    ]
  }),

  takeLatest: ({ actions, workers }) => ({
    [actions.submit]: function * () {
      const { submitSuccess, submitFailure } = this.actions

      const hasErrors = yield this.get('hasErrors')

      if (hasErrors) {
        yield put(submitFailure())
        return
      }

      // ... the rest of the submit worker
    }
  })
})

In order to display the errors, we will also update our component as follows:

// index.js

export default class Form extends Component {
  render () {
    const { isSubmitting, errors, values } = this.props
    const { submit, setValue } = this.actions

    const { name, email, message } = values

    return (
      <div>
        <div className='form-field'>
          <label>Name</label>
          <input type='text' value={name} onChange={e => setValue('name', e.target.value)} />
          {errors.name ? <div className='form-error'>{errors.name}</div> : null}
        </div>

        <div className='form-field'>
          <label>E-mail</label>
          <input type='text' value={email} onChange={e => setValue('email', e.target.value)} />
          {errors.email ? <div className='form-error'>{errors.email}</div> : null}
        </div>

        <div className='form-field'>
          <label className='block'>Message</label>
          <textarea value={message} onChange={e => setValue('message', e.target.value)} />
          {errors.message ? <div className='form-error'>{errors.message}</div> : null}
        </div>

        <button disabled={isSubmitting} onClick={submit}>
          {isSubmitting ? 'Submitting...' : 'Submit!'}
        </button>
      </div>
    )
  }
}

Plugging in the changes results in the following form:

This field is required
This field is required

Almost perfect! The only thing: we don't want to show the red errors before the user submits the form.

Remember the showErrors reducer from before? Now is its time to shine!

We have two choices with it. We can either use it in our render function like so:

export default class Form extends Component {
  render () {
    const { isSubmitting, errors, values, showErrors } = this.props

    return (
      <div>
        <div className='form-field'>
          ...
          {showErrors && errors.name
            ? <div className='form-error'>{errors.name}</div>
            : null}
        </div>
        ...
      </div>
    )
  }
}

... or we can simply return an empty hash for the errors selector until showErrors becomes true.

I prefer the second approach as it moves the form logic away from the render function.

In order to do this, we'll rename the previous selector errors into allErrors and make an new selector errors, that depends on both allErrors and showErrors. We'll also make hasErrors depend on the renamed allErrors:

// form.js

// ...

export default kea({
  // actions, reducers, takeLatest, ...

  selectors: ({ selectors }) => ({
    allErrors: [
      () => [selectors.values],
      (values) => ({
        name: !values.name ? missingText : null,
        email: !values.email ? missingText : (!isEmailValid(values.email) ? invalidEmail : null),
        message: !values.message ? missingText : null
      }),
      PropTypes.object
    ],

    hasErrors: [
      () => [selectors.allErrors],
      (allErrors) => Object.values(allErrors).filter(k => k).length > 0,
      PropTypes.bool
    ],

    errors: [
      () => [selectors.allErrors, selectors.showErrors],
      (errors, showErrors) => showErrors ? errors : {},
      PropTypes.object
    ]
  })
})

And that's it! With a few actons, reducers and selectors, totalling about 75 lines of code, you have a fully functional and extremely extendable form library at your disposal!

This is the final result:

Note that it shares data with the form on top of the page.

6. Abstracting createForm

As you saw, it's really easy to create forms with Kea. And you're no longer dependant on heavy (~50KB) form libraries!

That said, what if you need a second form on your page? Should you copy paste all this code around?

No!

There's not a lot of boilerplate with our form solution, but even what is there can be eliminated.

Let's build a form builder!

The principle here is simple. We'll create a function, createForm, that takes as an input all the form-specific data and returns a kea({}) logic store. Something like this:

// create-form.js

import PropTypes from 'prop-types'
import { kea } from 'kea'

export default function createForm (options) {
  // parse and clean up `options`

  return kea({
    // actions, reducers, etc that are built from `options`
  })
}

So what is all of the form-specific data? What kind of an API do we want the createForm function to have?

Well, here's one option:

// form.js

import PropTypes from 'prop-types'
import { delay } from 'redux-saga/effects'

import createForm from './create-form'

const isEmailValid = (email) => /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test(email)
const missingText = 'This field is required'

export default createForm({
  propTypes: {
    name: PropTypes.string,
    email: PropTypes.string,
    message: PropTypes.string
  },

  defaults: {
    name: 'John Doe',
    email: '',
    message: ''
  },

  validate: (values) => ({
    name: !values.name ? missingText : null,
    email: !values.email ? missingText : (!isEmailValid(values.email) ? 'Invalid e-mail' : null),
    message: !values.message ? missingText : null
  }),

  submit: function * () {
    // simulate a 1sec submission
    yield delay(1000)

    // either return the response:
    // -> return { results: [] }

    // or throw an exception:
    // -> throw 'Everything is broken!'
  },

  success: function * (response) {
    window.alert('submit successful!', response)
  },

  failure: function * (error) {
    window.alert('submit error!', error.message)
  }
})

That's about as lean as it gets!

And what about this createForm?

Turns out it requires very minimal changes from the code above. Here it is in all its glory. See if you can spot what changed:

// create-form.js

import PropTypes from 'prop-types'
import { kea } from 'kea'
import { put, call } from 'redux-saga/effects'

const noop = () => ({})

export default function createForm (options) {
  const propType = options.propTypes ? PropTypes.shape(options.propTypes) : PropTypes.object
  const defaults = options.defaults || {}
  const validate = options.validate || noop

  const submit = options.submit || noop
  const success = options.success || noop
  const failure = options.failure || noop

  return kea({
    actions: () => ({
      setValue: (key, value) => ({ key, value }),
      setValues: (values) => ({ values }),

      submit: true,
      submitSuccess: (response) => ({ response }),
      submitFailure: (error) => ({ error })
    }),

    reducers: ({ actions }) => ({
      values: [defaults, propType, {
        [actions.setValue]: (state, payload) => {
          return Object.assign({}, state, { [payload.key]: payload.value })
        },
        [actions.setValues]: (state, payload) => {
          return Object.assign({}, state, payload.values)
        },
        [actions.submitSuccess]: () => defaults
      }],

      isSubmitting: [false, PropTypes.bool, {
        [actions.submit]: () => true,
        [actions.submitSuccess]: () => false,
        [actions.submitFailure]: () => false
      }],

      showErrors: [false, PropTypes.bool, {
        [actions.submit]: () => true,
        [actions.submitSuccess]: () => false
      }]
    }),

    selectors: ({ selectors }) => ({
      allErrors: [
        () => [selectors.values],
        validate,
        PropTypes.object
      ],

      hasErrors: [
        () => [selectors.allErrors],
        (allErrors) => Object.values(allErrors).filter(k => k).length > 0,
        PropTypes.bool
      ],

      errors: [
        () => [selectors.allErrors, selectors.showErrors],
        (errors, showErrors) => showErrors ? errors : {},
        PropTypes.object
      ]
    }),

    takeLatest: ({ actions, workers }) => ({
      [actions.submit]: function * () {
        const { submitSuccess, submitFailure } = this.actions

        const hasErrors = yield this.get('hasErrors')

        if (hasErrors) {
          yield put(submitFailure())
          return
        }

        try {
          const response = yield call(submit.bind(this))
          yield call(success.bind(this), response)
          yield put(submitSuccess(response))
        } catch (error) {
          yield call(failure.bind(this), error)
          yield put(submitFailure(error))
        }
      }
    })
  })
}

Since the result of calling createForm is just a regular kea logic store, you may connect to it in any way you please. In fact, the code for the component itself is identical to what you saw above.

Here it is again for completion:

// index.js

import React, { Component } from 'react'
import { connect } from 'kea'

import form from './form'

@connect({
  actions: [
    form, [
      'setValue',
      'submit'
    ]
  ],
  values: [
    form, [
      'values',
      'isSubmitting',
      'errors'
    ]
  ]
})
export default class CreatedForm extends Component {
  render () {
    const { isSubmitting, errors, values } = this.props
    const { submit, setValue } = this.actions

    const { name, email, message } = values

    return (
      <div>
        <div className='form-field'>
          <label>Name</label>
          <input type='text' value={name} onChange={e => setValue('name', e.target.value)} />
          {errors.name ? <div className='form-error'>{errors.name}</div> : null}
        </div>

        <div className='form-field'>
          <label>E-mail</label>
          <input type='text' value={email} onChange={e => setValue('email', e.target.value)} />
          {errors.email ? <div className='form-error'>{errors.email}</div> : null}
        </div>

        <div className='form-field'>
          <label className='block'>Message</label>
          <textarea value={message} onChange={e => setValue('message', e.target.value)} />
          {errors.message ? <div className='form-error'>{errors.message}</div> : null}
        </div>

        <button disabled={isSubmitting} onClick={submit}>
          {isSubmitting ? 'Submitting...' : 'Submit!'}
        </button>
      </div>
    )
  }
}

And this is the created form in action:

Now, there are surely additional things that can be done. For example:

  1. You may create an abstract Field component that removes even more boilerplate.
  2. You may add extra code for async validation. E.g. checking if the username is taken or not.
  3. You may publish this createForm as a separate NPM package and reap all the fame that comes with being an open source maintainer :).

... but those things are outside the scope of this guide and are left as an exercise for the reader.

I hope you found this guide useful!

Happy hacking! :D