React-final-form: Update field value according to another field

Created on 5 Oct 2018  路  15Comments  路  Source: final-form/react-final-form

Hi,

I am having an issue with chained selects. Is there any way to change values of other fields according to another field's value?

I created a sandbox so that it is easy to look at. If you select a currency from first select, let's say "usd", the country select doesn't select any option. As you can see in the state of the form, "country" value is still "IE" (the previous value).

I would like to select the first country available for a given currency. So, I thought removing <option /> it would work but it doesn't. Any idea how to solve this without wrapping the form and send data to its parent component? Is there any way to solve this just using RFF?

Thanks.

Most helpful comment

As you can see in the state of the form, "country" value is still "IE" (the previous value).

Seems like this could be handled with a Declarative Form Rule, no?

All 15 comments

@jferrettiboke I've been struggling with this one recently too, and here's the approach that I'm currently taking:

const isString = (v) => typeof v === 'string'
// function keyword required for recursion
function DependentField({
  parentFields,
  ...remainingProps
}) {
  if (!parentFields?.length) {
    return <Field {...remainingProps} />
  }
  const [nextParent, ...remainingParents] = parentFields
  const parentName = isString(nextParent) ? nextParent : nextParent.parentName
  // If injectedProp not specified, use parentName
  const injectedProp = isString(nextParent) ?
    nextParent :
    nextParent.injectedProp || parentName
  return (
    <Field
      name={parentName}
      subscription={{value: true}}
      render={({input: {value}}) => {
        const renderProps = {
          ...remainingProps,
          [injectedProp]: value,
        }
        return (
          <DependentField parentFields={remainingParents} {...renderProps} />
        )
      }}
    />
  )
}

If any listed parent field changes, then the changed value is passed to the child as an updated prop. It can be used like so:

const MyDependentField = () => (
  <DependentField
    parentFields=['parentField1', { parentName: 'parentField2', injectedProp: 'parentFieldTwo' }]
    name='myDependentField'
  >
    {({ input, meta, parentField1, parentFieldTwo}) => (
      {/* render whatever you want here */}
    )}
  </DependentField>
)

I wish there were a nicer way of doing this out of the box or via an extension, but the DependentField approach here has worked for me nicely.

A few related issues:
https://github.com/final-form/react-final-form/issues/297
https://github.com/final-form/react-final-form/issues/273

EDIT: added support for injectedProp for use in field arrays

We use a FormSpy component to do for example something like this:

<Field name="first">
  { /* ... */ }
</Field>
<FormSpy
  subscription={{ /* ... */ }}
  render={({ form }) => (
    <Field name="second">
      { /* you can do something like this here: */ }
      {({ input }) => (
        <Input {...input} enabled={form.getFieldState('first').valid} />
      )}
    </Field>
  )}
/>

In short: with FormSpy you get the FormApi object to interact with the form, for example, use getFieldState method.

My challenge with FormSpy is the subscription prop. In the above case, you鈥檇 need to subscribe to {{ values: true }} at the very least for this component to render appropriately.

When subscribed to values, the FormSpy component will trigger a rerender if any field鈥檚 value has changed, regardless of whether or not I even use that field鈥檚 value to render my dependent component.

Yeah, everything depends on the scale, but for small to medium forms it鈥檚 easily sufficient.

Agreed, as long as render doesn鈥檛 trigger unnecessary side effects, it should be fine for smaller forms (e.g. a dropdown that depends on another field and a server response should cache the server responses appropriately).

It just seems counter to the design of this library to subscribe to more fields than necessary.

As you can see in the state of the form, "country" value is still "IE" (the previous value).

Seems like this could be handled with a Declarative Form Rule, no?

Thanks @erikras. This seems to be the solution.

:man_facepalming: can't believe I didn't see that before. thanks @erikras

In case someone is interested how I solved my needs, check this sandbox out.

/*
    For use inside a react-final-form context - change a field based on another field.

    Example: Change everytime the other field changes

      <WhenFieldChanges
          field='department'
          set='subDepartment'
          to={-1}
        />

    Example: Only change if `shouldChangeHandler` condition is true

      <WhenFieldChanges
        field='department'
        set='subDepartment'
        shouldChangeHandler=(department => { 
          if (department === -1) return true
          else return false
        }}
        to={-1}
      />
 */
import React from 'react'
import PropTypes from 'prop-types'
import { Field, useFormState } from 'react-final-form'
import { OnChange } from 'react-final-form-listeners'

const WhenFieldChanges = ({ shouldChangeHandler, field, set, to }) => {
  const { values } = useFormState()

  return (
    <Field name={set} subscription={{}}>
      {(
        // No subscription. We only use Field to get to the change function
        { input: { onChange } },
      ) => (
        <OnChange name={field}>
          {() => {
            if (shouldChangeHandler && shouldChangeHandler(values[field]))
              onChange(to)
            else onChange(to)
          }}
        </OnChange>
      )}
    </Field>
  )
}

WhenFieldChanges.propTypes = {
  field: PropTypes.string.isRequired,
  set: PropTypes.string.isRequired,
  shouldChangeHandler: PropTypes.func,
  to: PropTypes.any.isRequired,
}
WhenFieldChanges.defaultProps = {}

export default WhenFieldChanges

hey @engineforce since this is a hot topic and I don't think the previous solution is very neat (it seems a lot of boilerplate to me), can you share a solution using the hook?

@dbertella, turn out the solution posted by jferrettiboke is more declarative than mine and quite clean. You can find my solution using form.change (not hook, my mistake) at my sandbox.

I actually found out myself how to do this using hooks and it looks very easy, maybe less polish but very neat, this is an example:

const FormComponent = ({handleSubmit}) => {
 const input1Field = useField('input1')
 const input2Field = useField('input1')
return  (
<form onSubmit={handleSubmit}>
  <input
    {...input1Field.input}
    onChange={(e: ChangeEvent<HTMLInputElement>) => {
        input2Field.input.onChange(e.target.value)
    }}
  />
  <input {...input21Field.input} />
</form>
)
}

Super easy actually and it works quite well!
what do you think about it?
(I didn't test this particular code but it should work more or less, I can make a sandbox perhaps)

EDIT: forked your example above using hooks (thanks for the tip again!)
https://codesandbox.io/s/react-final-form-issue-348b-ixj9n

I like crobinson42's solution quite a bit, but it does have two bugs:

1) onChange will always fire, even if shouldChangeHandler returns false.
2) values[field] assumes values is a flat object.

I went with a patched version that also uses form.change:

import React from "react";
import PropTypes from "prop-types";
import { useForm, useFormState } from "react-final-form";
import { OnChange } from "react-final-form-listeners";
import get from "lodash/get";

const WhenFieldChanges = ({ shouldChangeHandler, field, set, to }) => {
  const { values } = useFormState();
  const form = useForm();

  return (
    <OnChange name={field}>
      {() => {
        if (shouldChangeHandler)
          shouldChangeHandler(get(values, field)) &&
            form.change(set, to);
        else form.change(set, to);
      }}
    </OnChange>
  );
};

WhenFieldChanges.propTypes = {
  field: PropTypes.string.isRequired,
  set: PropTypes.string.isRequired,
  shouldChangeHandler: PropTypes.func,
  to: PropTypes.any.isRequired
};
WhenFieldChanges.defaultProps = {};

export default WhenFieldChanges;

@erikras I'm migrating an old codebase from redux-form and I was using a custom hook to get the value of a specific field and conditionally render some elements based on that. Basically similar to the old formValues HoC but less tedious. Using a declarative form rule wouldn't cut it in my case. And using values from FormRenderProps isn't ideal because you can't subscribe to just one field value in <Form>.

I tried doing const {input: {value}} = useField(name) but that doesn't seem to work and probably will cause problems since it calls registerField without any validation etc. unlike the main <Field> for that name.

A lot of people would find a useFieldValue hook a welcome addition, I assure you. There needs to be a way to subscribe to only the value of a single field without registering all the stuff that comes along with <Field>.

Was this page helpful?
0 / 5 - 0 ratings