React-final-form: `useField()` hook

Created on 4 Mar 2019  Â·  6Comments  Â·  Source: final-form/react-final-form

Feature Request

It'd be nice to have a useField() hook for the existing react-final-form. This would give basically the same performance as <Field>, but with data returned from the hook instead of passed to the render prop.

useField(props: FieldProps): FieldRenderProps

The idea would be to get the form store via context, then subscribe to it within a useEffect(), and update a useState() each time it changes. Basically, implementation would look exactly like the hooks version of <Field>.

This would allow for fields to be created without the render prop, which would be nice when creating styled field components. For example:

function Field(props)
  let { input, meta } = useField(props)
  return (
    <StyldeField>
      <StyledLabel>Bio</StyledLabel>
      <StyledTextarea {...input} />
      {meta.touched && meta.error && <StyledError>{meta.error}</StyledError>}
    </StyledField>
  )
)

And the corresponding <Field> based example from the README:

function StyledField(props) {
  return (
    <Field
      {...props}
      render={({ input, meta }) => (
        <StyledFieldWrapper>
          <StyledLabel>Bio</StyledLabel>
          <StyledTextarea {...input} />
          {meta.touched && meta.error && <StyledError>{meta.error}</StyledError>}
        </StyledFieldWrapper>
      )}
    />
  )
}

Most helpful comment

With hooks, if you are using useField(), your entire form must rerender on every keypress

I played around with a version of useField using the current context setup of react-final-form and haven't found that to be the case. It isn't very polished, but is there something I'm missing?

// useField.js
import {fieldSubscriptionItems} from 'final-form';
import {useContext, useEffect, useRef, useState} from 'react';
import {ReactFinalFormContext} from 'react-final-form';

export const all = fieldSubscriptionItems.reduce((result, key) => {
  result[key] = true;
  return result;
}, {});

function eventValue(event) {
  if (!event || !event.target) {
    return event;
  }

  if (['checkbox', 'radio'].includes(event.target.type)) {
    return event.target.checked;
  }

  return event.target.value;
}

export default function useField(name, subscription = all) {
  const autoFocus = useRef(false);
  const [
    {blur, change, focus, name: metaName, value = '', ...meta},
    setState,
  ] = useState({});
  const form = useContext(ReactFinalFormContext);
  useEffect(() => {
    form.registerField(
      name,
      newState => {
        if (autoFocus.current) {
          autoFocus.current = false;
          setTimeout(() => newState.focus());
        }
        setState(newState);
      },
      subscription,
    );
  }, [
    form,
    name,
    ...fieldSubscriptionItems.map(key => Boolean(subscription[key])),
  ]);
  return {
    input: {
      onBlur: () => blur(),
      onChange: event => change(eventValue(event)),
      onFocus: () => {
        if (focus) {
          focus();
        } else {
          autoFocus.current = true;
        }
      },
      name,
      value,
    },
    meta,
  };
}
// App.js
import React from 'react';
import {Form} from 'react-final-form';
import useField from './useField';

const MyField = React.memo((props) => {
  const {name, placeholder} = props;
  console.log(`Rendering "${name}"!`);
  const {input, meta} = useField(name, {value: true});
  return (
    <>
      <input {...input} placeholder={placeholder} />
      {meta.touched && meta.error && (
        <span>{meta.error}</span>
      )}
    </>
  );
});

export default function MyForm() {
  const onSubmit = whatever => console.log(whatever);
  return (
    <Form
      onSubmit={onSubmit}
      render={({handleSubmit, invalid, pristine}) => {
        console.log('Rendering form!');
        return (
          <form onSubmit={handleSubmit}>
            <div>
              <label>First Name</label>
              <MyField name="firstName" placeholder="First Name" />
            </div>
            <div>
              <label>Last Name</label>
              <MyField name="lastName" placeholder="Last Name" />
            </div>

            <button type="submit" disabled={pristine || invalid}>
              Submit
            </button>
          </form>
        );
      }}
      subscription={{submitting: true, pristine: true}}
    />
  );
}

All 6 comments

Is this somehow different to react-final-form-hooks?

Thanks for sharing that @Kaltsoon I had no idea final form had hooks support yet! I was about to do what op did. Super cool!

Thanks for this issue, @jamesknelson. It's very helpful to have other people attack a problem you've attempted to solve and to evaluate the differences.

So yes, RFFH is the package that uses hooks to use FF. What Render Props™ give you that hooks cannot – and never will be able to (somewhat validated) – is the ability to let your components (e.g. Field) control when they rerender. With hooks, if you are using useField(), your entire form _must_ rerender on every keypress, so the whole "optimization through subscriptions" architecture of FF is thrown to the wind. The good news is, however, that 96% of forms don't need that level of optimizations, so hooks are great.

The most amazing thing about the hooks announcement with relation to Final Form is the validation of keeping form state in a separate container, outside of React. Within hours of the hooks announcement, I was able to whip up a version of RFF with almost no effort.

With hooks, if you are using useField(), your entire form must rerender on every keypress

I played around with a version of useField using the current context setup of react-final-form and haven't found that to be the case. It isn't very polished, but is there something I'm missing?

// useField.js
import {fieldSubscriptionItems} from 'final-form';
import {useContext, useEffect, useRef, useState} from 'react';
import {ReactFinalFormContext} from 'react-final-form';

export const all = fieldSubscriptionItems.reduce((result, key) => {
  result[key] = true;
  return result;
}, {});

function eventValue(event) {
  if (!event || !event.target) {
    return event;
  }

  if (['checkbox', 'radio'].includes(event.target.type)) {
    return event.target.checked;
  }

  return event.target.value;
}

export default function useField(name, subscription = all) {
  const autoFocus = useRef(false);
  const [
    {blur, change, focus, name: metaName, value = '', ...meta},
    setState,
  ] = useState({});
  const form = useContext(ReactFinalFormContext);
  useEffect(() => {
    form.registerField(
      name,
      newState => {
        if (autoFocus.current) {
          autoFocus.current = false;
          setTimeout(() => newState.focus());
        }
        setState(newState);
      },
      subscription,
    );
  }, [
    form,
    name,
    ...fieldSubscriptionItems.map(key => Boolean(subscription[key])),
  ]);
  return {
    input: {
      onBlur: () => blur(),
      onChange: event => change(eventValue(event)),
      onFocus: () => {
        if (focus) {
          focus();
        } else {
          autoFocus.current = true;
        }
      },
      name,
      value,
    },
    meta,
  };
}
// App.js
import React from 'react';
import {Form} from 'react-final-form';
import useField from './useField';

const MyField = React.memo((props) => {
  const {name, placeholder} = props;
  console.log(`Rendering "${name}"!`);
  const {input, meta} = useField(name, {value: true});
  return (
    <>
      <input {...input} placeholder={placeholder} />
      {meta.touched && meta.error && (
        <span>{meta.error}</span>
      )}
    </>
  );
});

export default function MyForm() {
  const onSubmit = whatever => console.log(whatever);
  return (
    <Form
      onSubmit={onSubmit}
      render={({handleSubmit, invalid, pristine}) => {
        console.log('Rendering form!');
        return (
          <form onSubmit={handleSubmit}>
            <div>
              <label>First Name</label>
              <MyField name="firstName" placeholder="First Name" />
            </div>
            <div>
              <label>Last Name</label>
              <MyField name="lastName" placeholder="Last Name" />
            </div>

            <button type="submit" disabled={pristine || invalid}>
              Submit
            </button>
          </form>
        );
      }}
      subscription={{submitting: true, pristine: true}}
    />
  );
}

I’m also curious why useField would need to re-render in every update. Shouldn’t it be possible to selectively render fields by checking whether they should be rendered from within the notification handler, as opposed to within shouldCmponentUpdate?

The idea here wouldn’t be to create a completely hook-based version - just to add a single hook to the existing version, which accesses final form via context, like @wtgtybhertgeghgtwtg has done.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

niros-welldone picture niros-welldone  Â·  3Comments

Soundvessel picture Soundvessel  Â·  4Comments

Noisycall picture Noisycall  Â·  4Comments

jkantr picture jkantr  Â·  4Comments

3dos picture 3dos  Â·  3Comments