React-redux-form: PROPOSAL: Local Form State (enhancement)

Created on 28 Sep 2016  路  7Comments  路  Source: davidkpiano/react-redux-form

Sometimes, a form and model state don't necessarily need to be tied to the global Redux store state, as the state is only relevant in the context of the form, and will be destroyed when the form is unmounted/submitted anyway.

In that case, we can _probably_ reuse most of the same API for <Form>, with a smarter model={...} prop that will detect if it is an instance of a React Component class:

class MyForm extends Component {
  render() {
    return (
      <Form model={this}>
        <Control model="user.name" />
        // ... etc.
      </Form>
    );
  }
}

This will rely on using context (which is already being used for partial models) to let the inner <Control>, <Field>, <Errors>, etc. components not to use the "global" dispatch, and rather to use a dispatch specific to MyForm's state. In this case, we will get:

this.state;
// { user: { name: '' }, $form: { user: { ... } }}

and then we can read our form and model state without having to connect() to anything, as it will be available on this.state.

Of course, this means that this data will not be made available to any outside components (unless somehow passed by props) and any actions dispatched inside a local <Form> will _not_ be detected by global reducers (which is the intent of the developer using local form state).

Thoughts? Ideas?

enhancement

Most helpful comment

Many times I find myself wanting to just use local state for forms and fields. However, as soon as I have to add validation I usually just switch everything over to rrf. Having an easy way to just use rrf for local states would be cool... but it might be the type of thing where it doesn't just work but instead you have to import Field from a different lib.

import { Form, Field, actions } from 'react-redux-form/localState';

I've seen other libraries function in this manner. Then, local doesn't have to be set. If you want both at once, do:

import { Form as localForm } from 'react-redux-form/localState';
import { Form as globalForm } from 'react-redux-form';

All 7 comments

On second thought, the above proposal is way too magical. I'd rather the developer have control of the local form and model values:

<Form
  local={true} // instead of model prop
  onModelChange={(value) => this.handleModelChange(value)}
  onFormChange={(form) => this.handleFormChange(form)}
>
  <Control model=".name" />
</Form>

where the developer can set up the handlers:

class MyLocalForm extends Component {
  handleModelChange(value) {
    // do anything you want here
    this.setState({ value: value });
  }
  handleFormChange(form) {
    // do anything you want here
    this.setState({ form: form });
  }
  // ...
}

Many times I find myself wanting to just use local state for forms and fields. However, as soon as I have to add validation I usually just switch everything over to rrf. Having an easy way to just use rrf for local states would be cool... but it might be the type of thing where it doesn't just work but instead you have to import Field from a different lib.

import { Form, Field, actions } from 'react-redux-form/localState';

I've seen other libraries function in this manner. Then, local doesn't have to be set. If you want both at once, do:

import { Form as localForm } from 'react-redux-form/localState';
import { Form as globalForm } from 'react-redux-form';

I don't know how complicated it would be given the internals... and it would be a significant departure from the current API... But maybe have a single storage/validate object that's stored on the context. Internally, Form, Filed, Control, etc. just all reference the object?

import { reduxStorage, localStateStorage } from 'react-redux-form'

<Form
  storage={ reduxStorage || localStateStorage }
  // ...
>

Essentially the current action creators API and "getter" functions would move into this storage object. So reduxStorage will dispatch as is currently being done. And localStateStorage would use setState ... I'm guessing it would have to passed this.state from the host component i.e. localStateStorage(this.state)

In this way the form API is decoupled from the storage mechanism, and you no longer need localForm or globalForm, or a lot of boilerplate to do something custom via handle... callbacks. And it would make it "trivial" for other storage engines that could use the browser's localStorage or sessionStorage, or even cookies (a terrible idea!), etc.

The API is large enough that it might be challenging to write a custom storage engine from scratch... But if someone had special requirements over a default implementation, maybe they could override the behavior thusly

let myCustomLocalStateStorage = (defaultLocalStateStorage) => {
  const someApiMethod = (...args) => {
    if (someCondition) {
       // ... do something different
    } else {
      return defaultLocalStateStorage.someApiMethod(...args)
    }
  }

  return {
    ...defaultLocalStateStorage,
    someApiMethod
  }
}

Just thinking out loud...

@cipater That's similar to my original idea:

      <Form model={this}>
        <Control model="user.name" />
        // ... etc.
      </Form>

although your idea is more explicit in that the developer would provide a storage API so that many different storage mechanisms can adapt to the form state updates.

However, this poses a couple issues:

  • We're ceding control of the parent component's state to an internal component - huge code smell.
  • A magical "storage" adapter is usually more opaque than a higher-order component.

I'd like to consider the general use case though - most of the time, if a developer wants a local form, all they care about is the submit value, so a minimal API would look something like this:

import { Form, Control, Errors } from 'react-redux-form/local'; // taking @lasergoat's idea

// in render():
<Form onSubmit={(values) => /* do something */}>
  <Control.text model=".name" required/>
  <Errors model=".name" messages={{valueMissing: 'This field is required'}} />
  <button>Submit!</button>
</Form>

And if the developer wanted to listen to every single form state update, such as when a field is focused or when a validation error occurs, we can add onUpdate:

<Form onSubmit={...} onUpdate={(formValue) => /* do something */}>
  // ...
</Form>

So, the following props will be added to <Form> (both local and Redux-specific):

  • onUpdate={(formValue) => ...}
  • onSubmitFailed={(formValue) => ...}
  • ignore={/* action event(s) to ignore /*} such as ['focus', 'blur'] (for performance)

Sounds good?

Sounds good?

Totally. I was just spit-balling... mostly a reaction to the idea that you might be building separate Form / Control / Errors components for redux vs. local state, which seemed impractical from a code maintenance perspective, especially if additional storage mechanisms were to be considered at some point in the future. But if that isn't the case, or if those components aren't actually been duplicated internally and react-redux-form/local is just some hand-waving with HOCs and exports, then that seems fine.

Regarding your comments about 'code smell' and opaqueness, I don't necessarily disagree... but I'm curious about your thoughts on the difference between actions.change('model.name', value) and reduxStorage.change('model.name', value). Are they not conceptually similar? I guess my question is, isn't actions essentially already a "magical storage" adapter that RRF uses to cede state control to redux? At least from a "setter" perspective. Is the only difference that actions isn't passed around on the context whereas ...Storage would be? Or am I missing something else?

or if those components aren't actually been duplicated internally and react-redux-form/local is just some hand-waving with HOCs and exports, then that seems fine.

Yep, that's exactly it!

Is the only difference that actions isn't passed around on the context whereas ...Storage would be? Or am I missing something else?

That's a good point, though the difference would not be in actions.change vs. someStorage.change, it would rather be in store.dispatch vs. someStorage.dispatch being passed around - that makes the code much more portable and reusable (as there is only one access point) and theoretically we can do some sort of custom storage adapter like you're suggesting.

I'll keep this in mind - there is the definite possibility of RRF being able to be used with other state management libraries besides Redux in the future. We just have to figure out the right abstraction.

Basic functionality for <LocalForm> shipped! https://github.com/davidkpiano/react-redux-form/releases/tag/v1.2.0

Was this page helpful?
0 / 5 - 0 ratings