Skip to content

Commit 62793c7

Browse files
committed
chore: Deprecate useFormValidityObserver React Hook
Originally, this hook seemed like a good idea. But actually, it's turned out to be something less performant than expected, and something that's more prone to footgunning than expected. See the `Design Decisions` document for additional details. Notice that `react` is still a peer dependency of `@form-observer/react`. In the future, we might use the React package to spy on the React version so that we can figure out how to appropriately generate the `autoObserve` function. If we decide against that, we can remove the peer dependency. Again, see the `Design Decisions` document for additional details.
1 parent fdd69e4 commit 62793c7

File tree

10 files changed

+101
-124
lines changed

10 files changed

+101
-124
lines changed

TODO.md

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
## Priority
44

5-
- [ ] ALSO deprecate `useFormValidityObserver` in favor of `useMemo(() => createFormValidityObserver())`. We may also want to update the `DESIGN_DECISIONS` docs when we do this.
65
- [ ] Release `0.9.0` after doing everything ABOVE this task.
76
- [ ] CONSIDER updating `@form-observer/lit` to automatically use `renderByDefault` and Lit's `render` function.
87
- [ ] Alternatively, consider updating our StackBlitz Lit example to have a renderer example and a default-renderer example.

docs/extras/design-decisions.md

+79
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,85 @@ This file is similar to a `Changelog`: It specifies the dates (in descending ord
1212

1313
It's possible that the need for this is already captured in the concept of `PR` (Pull Request) history. We will try to run with this approach _and_ the approach of PRs before coming to a final decision on what to use to accomplish this history-preserving goal.
1414

15+
## 2024-05-04
16+
17+
### Deprecate the `useFormValidityObserver` React Hook
18+
19+
Originally, we thought that it would be a good idea to memoize the `FormValidityObserver` for the React developers who would use our tool. That was the idea behind our `useFormValidityObserver` hook: It would enable developers to use the `FormValidityObserver` without having to think about memoization. It's a great idea! And when possible, I think that maintainers should take this route! But unfortunately, this approach is not always an option...
20+
21+
Consider a scenario where a developer uses the `useFormValidityObserver` hook with a complex configuration:
22+
23+
```tsx
24+
import { useFormValidityObserver } from "@form-observer/react";
25+
26+
function MyForm() {
27+
const { configure, ...rest } = useFormValidityObserver("focusout", {
28+
revalidateOn: "input",
29+
renderer(errorContainer, errorMessage) {
30+
// Implementation ...
31+
},
32+
defaultErrors: {
33+
// Default error message configuration ...
34+
},
35+
});
36+
}
37+
```
38+
39+
The result returned by `useFormValidityObserver` is memoized. However, the `useMemo` hook (which the function calls internally) uses the function's `type` argument and object options as its dependencies. If the `MyForm` component re-renders for any reason, the `"focusout"` and `"input"` strings will remain constant. But the `renderer` function and the `defaultErrors` object will be re-defined during re-renders, causing the `useMemo` hook to create a new `FormValidityObserver`. So those options need to be memoized.
40+
41+
```tsx
42+
import { useMemo, useCallback } from "react";
43+
import { useFormValidityObserver } from "@form-observer/react";
44+
45+
function MyForm() {
46+
const { configure, ...rest } = useFormValidityObserver("focusout", {
47+
revalidateOn: "input",
48+
renderer: useCallback((errorContainer, errorMessage) => {
49+
// Implementation
50+
}, []),
51+
defaultErrors: useMemo(
52+
() => ({
53+
// ... Default error message configuration
54+
}),
55+
[],
56+
),
57+
});
58+
}
59+
```
60+
61+
As you can see, this code becomes very ugly very quickly. Even worse, _the code becomes less performant_. Why? Well, because memoization comes with a cost. And the more that memoization is used, the more costs users will have to pay. Yes, memoization is helpful. Yes, under the right circumstances, memoization can bring performance benefits. But it should be used as little as possible and only where it's truly needed.
62+
63+
Here in our example, we're performing 3 memoizations for a single function call. (One for `defaultErrors`, one for `renderer`, and one for `useFormValidityObserver`'s internal call to `useMemo`.) That's not good. A better solution is to memoize the entire result once. But wait... Couldn't we just do that with `createFormValidityObserver`?
64+
65+
```tsx
66+
import { useMemo } from "react";
67+
import { createFormValidityObserver } from "@form-observer/react";
68+
69+
function MyForm() {
70+
const { configure, ...rest } = useMemo(() => {
71+
return createFormValidityObserver("focusout", {
72+
revalidateOn: "input",
73+
renderer(errorContainer, errorMessage) {
74+
// Implementation
75+
},
76+
defaultErrors: {
77+
// ... Default error message configuration
78+
},
79+
});
80+
}, []);
81+
}
82+
```
83+
84+
In fact, this is _more performant_ than `useFormValidityObserver`! If we memoize the result from `useFormValidityObserver`, then we perform _2_ memoizations. (One for `useFormValidityObserver`'s internal call to `useMemo`, and one for _our own_ call to `useMemo` on the function's result). But if we simply memoize the result of `createFormValidityObserver` directly, then we only have to perform _1_ memoization.
85+
86+
What does all of this mean? Well... It means that by default, even in the most ideal scenario, `useFormValidityObserver` will never be more performant than calling `useMemo` on `createFormValidityObserver` directly. And in most cases, `useFormValidityObserver` will be _less_ performant. Moreover, many developers won't even be aware of the concerns that we've discussed so far; so they're much more likely to footgun themselves if they use the flashy `useFormValidityObserver` hook!
87+
88+
We wanted to find a way to protect developers from `useMemo`, but we couldn't. (And honestly, no developer who wants to be skilled with React can avoid learning about `useMemo` in this day and age -- not until React Forget stabilizes, at least.) This dilemma is just an unavoidable downside of how React decides to operate as a framework, and we sadly can't do anything to fix that. What we _can_ do is guide developers on how to use `createFormValidityObserver` in React easily and performantly. And that's exactly what we've decided to do instead: Add clearer documentation as a better alternative.
89+
90+
Thankfully, things really aren't that complicated. People just need to wrap `createFormValidityObserver` inside `useMemo` (when using functional components). That's it.
91+
92+
Sidenote: Although we're removing the `useFormValidityObserver` hook, we aren't removing React as a peer dependency [yet]. The reason for this is that we're trying to figure out how to prepare for [React 19's changes to ref callbacks](https://react.dev/blog/2024/04/25/react-19#cleanup-functions-for-refs) (for the sake of `autoObserve`), and we're debating using the React package to help us with this. For example, we could use the React package to tell us the React version being used. Then, we could return one kind of `autoObserve` function if the React version is less than 19, and return a different kind of `autoObserve` function if the React version is greater than or equal to 19. But maybe that's a bad idea? Maybe we should just maintain different versions of our `@form-observer/react` package? Then again, it might not be a bad idea either since it seems like `import { version } from "react"` is [permitted](https://github.com/facebook/react/blob/main/packages/react/index.js#L78) (at least as of today). We'll see...
93+
1594
## 2024-04-28
1695

1796
### Only Allow One (1) Event Type to Be Passed to the `FormValidityObserver` Constructor

docs/form-validity-observer/integrations/react.md

+17-27
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,10 @@
22

33
A _convenience_ API for reducing code repetition in a [React](https://react.dev/) application using the [`FormValidityObserver`](../README.md).
44

5-
Utilities:
6-
7-
- [`createFormValidityObserver`](#function-createformvalidityobservertypes-options)
8-
- [`useFormValidityObserver`](#custom-hook-useformvalidityobservertypes-options)
9-
10-
Additional Topics:
11-
12-
- [`Usage with Class Components`](#usage-with-class-components)
13-
145
## Function: `createFormValidityObserver(type, options)`
156

167
Creates an enhanced version of the `FormValidityObserver`, known as the `ReactFormValidityObserver`. It accepts the exact same arguments as the [`FormValidityObserver`'s constructor](../README.md#constructor-formvalidityobservertypes-options).
178

18-
This function acts as the foundation for the [`useFormValidityObserver`](#custom-hook-useformvalidityobservertypes-options) hook. For those using class components, you can use `createFormValidityObserver` directly.
19-
209
### Return Type: `ReactFormValidityObserver<M, R>`
2110

2211
An enhanced version of the `FormValidityObserver`, designed specifically for React applications. It has the same Type Parameters as the `FormValidityObserver`. As with the `FormValidityObserver`, the type of `M` is derived from the [`renderer`](../README.md#form-validity-observer-options-renderer) option, and the type of `R` is derived from the [`renderByDefault`](../README.md#form-validity-observer-options-render-by-default) option.
@@ -220,34 +209,35 @@ function MyForm() {
220209

221210
The return type of `configure` is simply an object containing the props that should be applied to the configured field. In addition to the field's `name`, this object will include any validation props that were configured by the function (e.g., `required`, `minLength`, `maxLength`, `pattern`, etc.).
222211

223-
## Custom Hook: `useFormValidityObserver(types, options)`
212+
## Gotchas: Remember to Memoize Your Observer Instance(s) When Necessary
213+
214+
As we mentioned previously, React has a unique re-rendering model. Whenever a state change happens in a React functional component, _the entire component function_ is re-run. If you're instantiating classes (such as the `FormValidityObserver`) in the body of your component's function, then React may re-instantiate the class during a re-render -- even if you don't want that to happen. Sometimes this can lead to inconsistent/unexpected outcomes.
224215

225-
A custom React Hook that creates an enhanced version of the `FormValidityObserver` and [memoizes](https://react.dev/reference/react/useMemo) its value.
216+
To circumvent this problem, React provides the [`useMemo`](https://react.dev/reference/react/useMemo) hook. This hook guarantees that a given value will not change or be recalculated between re-renders. (If you ever want the value to be recalculated, you can provide an array of dependencies that indicate when the value should be recalculated. In the case of the `FormValidityObserver`, we only want to instantiate it _once_, so no dependencies are necessary.) Below is an example of how to use `useMemo` with the `createFormValidityObserver` function.
226217

227218
```tsx
228219
import { useMemo } from "react";
229-
import { useFormValidityObserver } from "@form-observer/react";
220+
import { createFormValidityObserver } from "@form-observer/react";
230221

231222
function MyForm() {
232-
const { autoObserve, configure } = useFormValidityObserver("focusout");
233-
234-
return (
235-
// If your component does not re-render, you don't need to memoize `autoObserve`'s return value.
236-
<form ref={useMemo(autoObserve, [autoObserve])}>
237-
<input {...configure("first-name", { required: "We need to know who you are!" })} />
238-
</form>
239-
);
223+
const { autoObserve, configure } = useMemo(() => createFormValidityObserver("focusout"), []);
224+
return <form ref={useMemo(autoObserve, [])}>{/* Form Fields */}</form>;
240225
}
241226
```
242227

243-
The purpose of the memoization is two-fold:
228+
Note: If you are using React's ESLint Rules for hooks, the linter will sometimes tell you to list invalid/unnecessary dependencies for `useMemo`. For example, in the code above, ESLint may tell you to list `autoObserve` as a dependency for the 2nd call to `useMemo`. But because the call to `createFormValidityObserver` is memoized, the `autoObserve` value will never change. Consequently, the 2nd call to `useMemo` should have no dependencies at all.
229+
230+
You can choose to disable the ESLint rule in cases like this one where the rule is incorrect, or you can explicitly list the dependency like so:
231+
232+
```tsx
233+
<form ref={useMemo(autoObserve, [autoObserve])}>{/* Form Fields */}</form>
234+
```
244235

245-
1. When the component employing `useFormValidityObserver` re-renders (whether due to state changes or prop updates), the memoization prevents the observer from being re-created/reset. (This is likely not a practical concern if `configure` is only used inside your component's returned JSX.)
246-
2. Because the outputs of `useFormValidityObserver` are memoized, they won't cause unnecessary re-renders in [memoized children](https://react.dev/reference/react/memo), nor will they cause or unnecessary function re-runs in hooks that depend on them (such as `useEffect`, `useCallback`, and custom hooks that have dependency arrays).
236+
Since `autoObserve` will never change, the `useMemo` hook will never run any recalculations when `autoObserve` is passed as a dependency. So in the end, you're free to decide how to handle your lint warnings in these situations -- whether by appeasing the linter or by disabling it locally.
247237

248-
If you don't need to worry about these scenarios, then you are free to use [`createFormValidityObserver`](#function-createformvalidityobservertypes-options) instead; it will give you the exact same result (unmemoized). If you _do_ need to worry about these scenarios, then bear in mind that **the observer will be recreated whenever the arguments to the hook change, whether by value _or_ by reference**.
238+
If you know that a functional component using `createFormValidityObserver` will never re-render, then you can ditch the `useMemo` hook entirely. For more details on memoization, see React's documentation on [`useMemo`](https://react.dev/reference/react/useMemo) and [`memo`](https://react.dev/reference/react/memo). You can also read [_When to useMemo and useCallback_](https://kentcdodds.com/blog/usememo-and-usecallback) by Kent C. Dodds.
249239

250-
Note that this is a very small hook created solely for your convenience. If you want, you can use `useMemo` directly to wrap any calls to `createFormValidityObserver` instead. For more details on memoization, see React's documentation on [`useMemo`](https://react.dev/reference/react/useMemo) and [`memo`](https://react.dev/reference/react/memo). You can also read [_When to useMemo and useCallback_](https://kentcdodds.com/blog/usememo-and-usecallback) by Kent C. Dodds.
240+
> Note: For those using class components, "memoization" happens automatically as long as the observer is created only once (i.e., during the class's construction).
251241
252242
## Usage with Class Components
253243

packages/preact/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ Here's an example of how to automatically validate your form fields when a user
2929

3030
```jsx
3131
import { useMemo } from "preact/hooks";
32-
import { useFormValidityObserver } from "@form-observer/preact";
32+
import { createFormValidityObserver } from "@form-observer/preact";
3333

3434
function MyForm() {
35-
const { autoObserve, configure, validateFields } = useFormValidityObserver("focusout");
35+
const { autoObserve, configure, validateFields } = useMemo(() => createFormValidityObserver("focusout"), []);
3636

3737
function handleSubmit(event) {
3838
event.preventDefault();

packages/react/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ Here's an example of how to automatically validate your form fields when a user
2929

3030
```jsx
3131
import { useMemo } from "react";
32-
import { useFormValidityObserver } from "@form-observer/react";
32+
import { createFormValidityObserver } from "@form-observer/react";
3333

3434
function MyForm() {
35-
const { autoObserve, configure, validateFields } = useFormValidityObserver("focusout");
35+
const { autoObserve, configure, validateFields } = useMemo(() => createFormValidityObserver("focusout"), []);
3636

3737
function handleSubmit(event) {
3838
event.preventDefault();
@@ -128,7 +128,7 @@ class MyForm extends Component {
128128
}
129129
```
130130

131-
For more details on what `createFormValidityObserver` and `useFormValidityObserver` can do (like custom validation, manual error handling, and more), see our [documentation](https://github.com/enthusiastic-js/form-observer/blob/main/docs/form-validity-observer/integrations/react.md).
131+
For more details on what `createFormValidityObserver` can do (like custom validation, manual error handling, and more), see our [documentation](https://github.com/enthusiastic-js/form-observer/blob/main/docs/form-validity-observer/integrations/react.md).
132132

133133
## Other Uses
134134

packages/react/__tests__/useFormValidityObserver.test.ts

-55
This file was deleted.

packages/react/index.d.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from "@form-observer/core";
22
export { default as createFormValidityObserver } from "./createFormValidityObserver.js";
3-
export { default as useFormValidityObserver } from "./useFormValidityObserver.js";
43
export type * from "./types.d.ts";

packages/react/index.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from "@form-observer/core";
22
export { default as createFormValidityObserver } from "./createFormValidityObserver.js";
3-
export { default as useFormValidityObserver } from "./useFormValidityObserver.js";

packages/react/package.json

-8
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@
2121
"types": "./createFormValidityObserver.d.ts",
2222
"default": "./createFormValidityObserver.js"
2323
},
24-
"./useFormValidityObserver": {
25-
"require": {
26-
"types": "./useFormValidityObserver.d.cts",
27-
"default": "./useFormValidityObserver.cjs"
28-
},
29-
"types": "./useFormValidityObserver.d.ts",
30-
"default": "./useFormValidityObserver.js"
31-
},
3224
"./types": {
3325
"require": {
3426
"types": "./types.d.cts"

packages/react/useFormValidityObserver.js

-26
This file was deleted.

0 commit comments

Comments
 (0)