React Hook表格:独特的实现
#javascript #教程 #react #form

表格每天都使用,登录/注册,订购某物时填写信息,...它确实是网站的杰作。

我开始用Redux Form React 中制作表单,该使用Redux存储有关表单的信息。是的,这是我们使用Redux做所有事物的旧时。

如今,情况发生了变化。我们有多个库:FormikReact Final FormReact Hook Form,...大多数时候使用 react state 存储信息。

我知道, remix 之类的框架鼓励我们使用纯HTML来制作形式。但是通常,如果我们想要一个良好的用户体验,我们必须使用客户库快速反馈,或者当您需要在字段上取决于彼此的复杂验证时。

React Hook Form 是一个专注于性能的库。查看其实现真的很有趣,可以学习一些可以在其他情况下可以使用的模式。
让我们看一下与其他表单库实现相比,它使其与众不同的是什么。


先决条件

在开始谈论实施之前,我想定义一些术语将全部放在同一页面上:

  • 字段:从用户收集数据的元素(输入,选择,datepicket,...)。
  • 字段名称:字段的标识符。
  • 字段值:用户填充的值。

什么是“简单”的实现?

如果今天,我必须进行表单实施。本能地,我会使用 state 来制作像FormikReact Final Form这样的人:

function MyForm() {
  const [values, setValues] = useState({
    firstname: "",
    lastname: "",
  });
  const onChange = (fieldName, fieldValue) => {
    setValues((prevValues) => ({
      ...prevValues,
      [fieldName]: fieldValue,
    }));
  };

  return (
    <form
      onSubmit={() => {
        // Do something with the form values
        // that are in the `values` variable
      }}
    >
      <label>
        Firstname
        <input
          type="text"
          name="firstname"
          value={values["firstname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <label>
        Lastname
        <input
          type="text"
          name="lastname"
          value={values["lastname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

这里没有什么花哨的。我只是将用户填充的值存储在React状态下。我们走了。
这是一个真正简化的实现。在现实生活中,我可能会使用还原器,因为我想存储更多的值:验证错误,知道表单是否在提交,是否脏,...

如果您想查看更现实的实现
// I do not handle validation and form states
// but if I do I will probably use a reducer to that 
// instead of multiple states
function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);

  const handleSubmit = (onSubmit) => (e) => {
    e.preventDefault();

    onSubmit(values);
  };

  const register = (fieldName) => {
    return {
      value: values[fieldName],
      onChange: (event) => {
        setValues((prevValues) => ({
          ...prevValues,
          [fieldName]: fieldValue,
        }));
      },
    };
  };

  return {
    register,
    handleSubmit,
  };
}

function MyForm() {
  const { values, onChange, handleSubmit } = useForm({
    firstname: "",
    lastname: "",
  });

  return (
    <form
      onSubmit={() => {
        // Do something with the form values
        // that are in the `values` variable
      }}
    >
      <label>
        Firstname
        <input
          type="text"
          name="firstname"
          value={values["firstname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <label>
        Lastname
        <input
          type="text"
          name="lastname"
          value={values["lastname"]}
          onChange={(e) => onChange(e.target.value)}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

你知道吗?那不是实现 React hook表格的方式。


React Hook表单实现的关键点

不要使用反应状态

要知道的主要内容是,该库不使用React状态 /还原器来存储数据,而是使用引用。
它使用 React Ref 的懒惰初始化:

function useForm(config) {
  const formControl = useRef(undefined);

  // Lazy initialization of the React ref
  // Enter the condition only at the first render
  // (`createFormControl` returns an object
  if (formControl.current === undefined) {
    formControl.current = createFormControl(config);
  }
}

然后在createFormControl中,所有内容都存储在const中,它们被突变:

function createFormControl({ initialValues }) {
  const formValues = initialValues;

  const onChange = (fieldName, fieldValue) => {
    formValues[fieldName] = fieldValue;
  };

  return {
    onChange,
  };
}

现在,它的速度非常快,因为不再渲染。

mmmm等等,不再渲染了吗?我们怎么知道何时变化和形式状态?

让我们看看。


观察者模式

这种模式在行业中真正使用:react-queryreact-redux,...使用它。

原理真的很简单,但如此强大。
我们有:

  • a subject:这是一个跟踪实体变化并通知此更改的对象
  • observers:他们通过订阅主题
  • 来聆听实体的变化。

如果您想查看实现
function createSubject() {
  const listeners = [];

  const subscribe = (listener) => {
    // Add the listener
    listeners.push(listener);

    // Return an unsubscribe method
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  };

  const update = (value) => {
    for (const listener of listeners) {
      listener(value);
    }
  };

  return {
    subscribe,
    update,
  };
}

React Hook Form 有3个主题:

  • watch:跟踪字段值的变化
  • array:跟踪字段数组值的更改
  • state:跟踪表单状态的变化

现在,useWatch钩订阅了watch主题,并在我们想跟踪已更改的字段时更新React state

,在需要时,我们在这里进行组件。

等等!当我想在表单变脏时被通知时,当其他状态值更改时,我的组件不会重新渲染。怎么可能?

这是下一个关键点。


代理 / DefineProperty

如果您不知道什么是代理,您可以阅读我的文章Proxy in JS: what the hell?

在RHF中,代理用于知道状态的哪些特性用于组件。

感谢他们,我们可以知道组件会聆听哪些属性,并且仅在这些属性发生变化时才渲染。

function createProxy(formState, listenedStateProps) {
  const result = {};

  // Loop on the property which are in the form state
  for (const propertyName in formState) {
    Object.defineProperty(result, {
      get() {
        // Keep in mind that the property is listened
        listenedStateProps[propertyName] = true;

        // And returns the actual value
        return formState[propertyName];
      },
    });
  }

  return result;
}

,因此,观察者模式我们可以在更改式状态属性时更新组件。

// control is an object that has all the logic
// and the mutated object like `_formValues`,
// `_formState`, `_subjects`, ...
function useFormState(control) {
  // At start nothing is listened
  // In reality there are more properties
  const listenedStateProps = useRef({
    isDirty: false,
    isValid: false,
  });
  // Initialize with the current `_formState` which is
  // mutated
  const [formState, setFormState] = useState(
    control._formState
  );

  useEffect(() => {
    return control._subjects.state.subscribe(
      ([stateProp, stateValue]) => {
        // If the changed property is listened let's update
        if (listenedStateProps.current[stateProp]) {
          setState((prev) => ({
            ...prev,
            [stateProp]: stateValue,
          }));
        }
      }
    );
  }, [control._subjects]);

  return createProxy(formState, listenedStateProps);
}

稳定的事件听众没有过时的外部数据

另一种策略是,在事件侦听器中使用的值的参考用途,这些值要多于useCallbackuseEffect中使用。

为什么?
因为我们不想在回调中有过时的数据,因此我们必须在useCallback的依赖性中添加它。因此,每当依赖性正在改变时,它都会创建一个全新的参考,因为这是事件侦听器。

注意:它实际上在每个渲染上创建了一个新的参考

而不是:

function MyComponent({ someData }) {
  // The reference of showData is not stable!
  const showData = useCallback(() => {
    console.log("The data is", someData);
  }, [someData]);

  return (
    <MemoizedButton type="button" onClick={showData}>
      Show the data, please
    </MemoizedButton>
  );
}

我们有:

function MyComponent({ someData }) {
  const someDataRef = useRef(someData);

  useLayoutEffect(() => {
    // Keep the reference up-to-date
    someDataRef.current = someData;
  });

  // The reference of showData is now stable!
  const showData = useCallback(() => {
    console.log("The data is", someDataRef.current);
  }, []);

  return (
    <MemoizedButton type="button" onClick={showData}>
      Show the data, please
    </MemoizedButton>
  );
}

如果您已经有我的文章useEvent: the new upcoming hook?,则可能已经注意到这是相同的原则。不幸的是,useEvent不会很快到来,所以我们必须在项目中更长一点。

补充信息

实际上,在 react hook形式代码库中实现不相同。

REF直接在渲染中更新,但我不建议您使用它,因为可能会因新的并发功能造成一些麻烦,并且在组件中存在不一致。

function MyComponent({ someData }) {
  const someDataRef = useRef(someData);

  // Do not update directly in the render!!!
  someDataRef.current = someData;

  // But use a `useLayoutEffect`
  useLayoutEffect(() => {
    someDataRef.current = someData;
  });
}

这是相同的模式,而So想要的koude24 hook最终不会出来:(



结论

您应该更舒适地浏览 React Hook Form 并了解代码。
某些要点可以在您自己的代码库中或要开发库中。

注意不要过于优化您的代码。如果您想将相同的模式与突变应用相同,我建议每次数据发生变化,而不试图在认为不是必需的情况下不进行突变,因为当组件有条件地呈现时可能会给您带来麻烦。例如,我会防止这种仅在当前渲染上使用formState.isDirty时突变形式状态的kind of code,但是当您在下一个渲染时侦听状态肮脏时不起作用。


请随时发表评论,如果您想看到更多,可以在Twitch上关注我或转到我的Website。如果you want to buy me a coffee

,这里有一个小链接