Heim  >  Artikel  >  Web-Frontend  >  Verwenden Sie Hooks, um ein Anmeldeformular zu schreiben – Frontier Development Team

Verwenden Sie Hooks, um ein Anmeldeformular zu schreiben – Frontier Development Team

hzc
hzcnach vorne
2020-06-22 18:02:372443Durchsuche

Kürzlich habe ich versucht, ein Anmeldeformular mithilfe der React-Hooks-bezogenen API zu schreiben. Der Zweck besteht darin, mein Verständnis von Hooks zu vertiefen. In diesem Artikel wird nicht die Verwendung bestimmter APIs erläutert, sondern es wird Schritt für Schritt ein detaillierter Einblick in die zu implementierenden Funktionen gegeben. Daher müssen Sie vor dem Lesen über ein grundlegendes Verständnis von Hooks verfügen. Das endgültige Aussehen ähnelt ein wenig der Verwendung von Hooks zum Schreiben eines einfachen Redux-ähnlichen Zustandsverwaltungsmodells.

Feingranularer Zustand

Ein einfaches Anmeldeformular enthält drei Eingabeelemente: Benutzername, Passwort und Bestätigungscode, die auch die drei Datenzustände des Formulars darstellen. Wir konzentrieren uns einfach auf Benutzername, Passwort und Capacha stellen jeweils Statusbeziehungen über useState her, was die sogenannte relativ feinkörnige Statusaufteilung darstellt. Der Code ist auch sehr einfach:

// LoginForm.js

const LoginForm = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [captcha, setCaptcha] = useState("");

  const submit = useCallback(() => {
    loginService.login({
      username,
      password,
      captcha,
    });
  }, [username, password, captcha]);

  return (
    <p>
      <input> {
          setUsername(e.target.value);
        }}
      />
      <input> {
          setPassword(e.target.value);
        }}
      />
      <input> {
          setCaptcha(e.target.value);
        }}
      />
      <button>提交</button>
    </p>
  );
};

export default LoginForm;

Diese Art von feinkörnigen Zuständen ist sehr einfach und intuitiv, aber wenn es zu viele Zustände gibt, wird es ziemlich mühsam sein, für jeden Zustand die gleiche Logik zu schreiben es wird zu verstreut sein.

Grobkörnig

Wir definieren Benutzername, Passwort und Capacha als einen Zustand, der die sogenannte grobkörnige Zustandseinteilung darstellt:

const LoginForm = () => {
  const [state, setState] = useState({
    username: "",
    password: "",
    captcha: "",
  });

  const submit = useCallback(() => {
    loginService.login(state);
  }, [state]);

  return (
    <p>
      <input> {
          setState({
            ...state,
            username: e.target.value,
          });
        }}
      />
      ...
      <button>提交</button>
    </p>
  );
};

Wie Sie sehen können , die setXXX-Methode wird reduziert und die setState Die Benennung ist ebenfalls angemessener, aber dieser setState führt die Statuselemente nicht automatisch zusammen, und wir müssen sie manuell zusammenführen.

Formularverifizierung hinzufügen

Natürlich darf in einem vollständigen Formular der Verifizierungslink nicht fehlen. Um die Fehlermeldung unter der Eingabe anzuzeigen, wenn ein Fehler auftritt, extrahieren wir zunächst eine Unterkomponente Feld:

const Filed = ({ placeholder, value, onChange, error }) => {
  return (
    <p>
      <input>
      {error && <span>error</span>}
    </p>
  );
};

Wir verwenden die schematypisierte Bibliothek, um einige Felddefinitionen und -überprüfungen durchzuführen. Die Verwendung ist sehr einfach. Die API ähnelt dem PropType von React. Wir definieren die folgende Feldüberprüfung:

const model = SchemaModel({
  username: StringType().isRequired("用户名不能为空"),
  password: StringType().isRequired("密码不能为空"),
  captcha: StringType()
    .isRequired("验证码不能为空")
    .rangeLength(4, 4, "验证码为4位字符"),
});

und fügen dann Fehler zum Status hinzu und lösen model.check in der Submit-Methode aus.

const LoginForm = () => {
  const [state, setState] = useState({
    username: "",
    password: "",
    captcha: "",
    // ++++
    errors: {
      username: {},
      password: {},
      captcha: {},
    },
  });

  const submit = useCallback(() => {
    const errors = model.check({
      username: state.username,
      password: state.password,
      captcha: state.captcha,
    });

    setState({
      ...state,
      errors: errors,
    });

    const hasErrors =
      Object.values(errors).filter((error) => error.hasError).length > 0;

    if (hasErrors) return;
    loginService.login(state);
  }, [state]);

  return (
    <p>
      <field> {
          setState({
            ...state,
            username: e.target.value,
          });
        }}
      />
        ...
      <button>提交</button>
    </field></p>
  );
};

Wenn wir dann auf „Senden“ klicken, ohne etwas einzugeben, wird eine Fehlermeldung ausgelöst:
Verwenden Sie Hooks, um ein Anmeldeformular zu schreiben – Frontier Development Team

useReducer rewrite

At Zu diesem Zeitpunkt scheint unser Formular fast vollständig zu sein und die Funktion scheint abgeschlossen zu sein. Aber ist das in Ordnung? Wir drucken console.log(placeholder, "rendering") in der Feldkomponente. Wenn wir den Benutzernamen eingeben, stellen wir fest, dass alle Feldkomponenten neu gerendert werden. Dies kann optimiert werden.
Wie geht das? Erstens möchten wir, dass die Field-Komponente nicht erneut gerendert wird, wenn die Requisiten unverändert bleiben. Wir verwenden React.memo, um die Field-Komponente zu umschließen.

React.memo ist eine Komponente höherer Ordnung. Es ist React.PureComponent sehr ähnlich, funktioniert aber nur mit funktionalen Komponenten. Wenn Ihre Funktionskomponente mit denselben Requisiten dasselbe Ergebnis rendert, können Sie die Leistung der Komponente verbessern, indem Sie sich das Rendering-Ergebnis der Komponente merken, indem Sie es in React.memo einschließen und

export default React.memo(Filed);

aufrufen

Aber nur in diesem Fall werden alle Feldkomponenten neu gerendert. Dies liegt daran, dass unsere onChange-Funktion jedes Mal ein neues Funktionsobjekt zurückgibt, wodurch das Memo ungültig wird.
Wir können die onChange-Funktion von File mit useCallback umschließen, sodass wir nicht jedes Mal, wenn die Komponente gerendert wird, ein neues Funktionsobjekt generieren müssen.

const changeUserName = useCallback((e) => {
  const value = e.target.value;
  setState((prevState) => { // 注意因为我们设置useCallback的依赖为空,所以这里要使用函数的形式来获取最新的state(preState)
    return {
      ...prevState,
      username: value,
    };
  });
}, []);

Gibt es eine andere Lösung? Wir haben festgestellt, dass useReducer,

useReducer eine weitere Alternative ist, die sich besser für die Verwaltung von Zustandsobjekten eignet, die mehrere Unterwerte enthalten. Es ist eine Alternative zu useState. Es empfängt einen Reduzierer der Form (state, action) => newState und gibt den aktuellen Status und die passende Dispatch-Methode zurück. Darüber hinaus kann die Verwendung von useReducer auch eine Leistungsoptimierung für Komponenten durchführen, die tiefe Aktualisierungen auslösen, da Sie den Versand an Unterkomponenten anstelle von Rückruffunktionen übergeben können

Eine wichtige Funktion von useReducer ist, dass es dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变 zurückgibt. Dann können wir den Versand sicher an die Unterkomponente übergeben, ohne befürchten zu müssen, dass die Unterkomponente erneut gerendert wird.
Wir definieren zunächst die Reduzierfunktion, um den Zustand zu bedienen:

const initialState = {
  username: "",
  ...
  errors: ...,
};

// dispatch({type: 'set', payload: {key: 'username', value: 123}})
function reducer(state, action) {
  switch (action.type) {
    case "set":
      return {
        ...state,
        [action.payload.key]: action.payload.value,
      };
    default:
      return state;
  }
}

Rufen Sie entsprechend userReducer in LoginForm auf, übergeben Sie unsere Reduzierfunktion und initialState

const LoginForm = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const submit = ...

  return (
    <p>
      <field></field>
      ...
      <button>提交</button>
    </p>
  );
};

in der Field-Unterkomponente. Fügen Sie ein neues Namensattribut hinzu um den aktualisierten Schlüssel zu identifizieren und die Dispatch-Methode

const Filed = ({ placeholder, value, dispatch, error, name }) => {
  console.log(name, "rendering");
  return (
    <p>
      <input>
          dispatch({
            type: "set",
            payload: { key: name, value: e.target.value },
          })
        }
      />
      {error && <span>{error}</span>}
    </p>
  );
};

export default React.memo(Filed);

zu übergeben. Auf diese Weise können wir den Dispatch übergeben, damit die Unterkomponente das Änderungsereignis intern verarbeiten kann und die Übergabe der onChange-Funktion vermieden wird. Gleichzeitig wird die Statusverwaltungslogik des Formulars auf den Reduzierer migriert.

Globaler Shop

Wenn unsere Komponentenhierarchie relativ tief ist und wir die Versandmethode verwenden möchten, müssen wir sie Schicht für Schicht durch die Requisiten leiten, was offensichtlich unpraktisch ist. Zu diesem Zeitpunkt können wir die von React bereitgestellte Kontext-API verwenden, um Status und Methoden komponentenübergreifend zu teilen.

Context bietet eine Methode zum Übertragen von Daten zwischen Komponentenbäumen, ohne manuell Requisiten für jede Komponentenebene hinzuzufügen.

Funktionale Komponenten können mit createContext und useContext implementiert werden.

Hier werden wir nicht auf die Verwendung dieser beiden APIs eingehen. Sie können sie grundsätzlich ausschreiben, indem Sie sich die Dokumentation ansehen. Zur Implementierung verwenden wir „unstated-next“, was im Wesentlichen eine Kapselung der oben genannten API darstellt und bequemer zu verwenden ist.

我们首先新建一个store.js文件,放置我们的reducer函数,并新建一个useStore hook,返回我们关注的state和dispatch,然后调用createContainer并将返回值Store暴露给外部文件使用。

// store.js
import { createContainer } from "unstated-next";
import { useReducer } from "react";

const initialState = {
  ...
};

function reducer(state, action) {
  switch (action.type) {
    case "set":
        ...
    default:
      return state;
  }
}

function useStore() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return { state, dispatch };
}

export const Store = createContainer(useStore);

接着我们将LoginForm包裹一层Provider

// LoginForm.js
import { Store } from "./store";

const LoginFormContainer = () => {
  return (
    <store.provider>
      <loginform></loginform>
    </store.provider>
  );
};

这样在子组件中就可以通过useContainer随意的访问到state和dispatch了

// Field.js
import React from "react";
import { Store } from "./store";

const Filed = ({ placeholder, name }) => {
  const { state, dispatch } = Store.useContainer();

  return (
    ...
  );
};

export default React.memo(Filed);

可以看到不用考虑组件层级就能轻易访问到state和dispatch。但是这样一来每次调用dispatch之后state都会变化,导致Context变化,那么子组件也会重新render了,即使我只更新username, 并且使用了memo包裹组件。

当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染

那么怎么避免这种情况呢,回想一下使用redux时,我们并不是直接在组件内部使用state,而是使用connect高阶函数来注入我们需要的state和dispatch。我们也可以为Field组件创建一个FieldContainer组件来注入state和dispatch。

// Field.js
const Filed = ({ placeholder, error, name, dispatch, value }) => {
  // 我们的Filed组件,仍然是从props中获取需要的方法和state
}

const FiledInner = React.memo(Filed); // 保证props不变,组件就不重新渲染

const FiledContainer = (props) => {
  const { state, dispatch } = Store.useContainer();
  const value = state[props.name];
  const error = state.errors[props.name].errorMessage;
  return (
    <filedinner></filedinner>
  );
};

export default FiledContainer;

这样一来在value值不变的情况下,Field组件就不会重新渲染了,当然这里我们也可以抽象出一个类似connect高阶组件来做这个事情:

// Field.js
const connect = (mapStateProps) => {
  return (comp) => {
    const Inner = React.memo(comp);

    return (props) => {
      const { state, dispatch } = Store.useContainer();
      return (
        <inner></inner>
      );
    };
  };
};

export default connect((state, props) => {
  return {
    value: state[props.name],
    error: state.errors[props.name].errorMessage,
  };
})(Filed);

dispatch一个函数

使用redux时,我习惯将一些逻辑写到函数中,如dispatch(login()),
也就是使dispatch支持异步action。这个功能也很容易实现,只需要装饰一下useReducer返回的dispatch方法即可。

// store.js
function useStore() {
  const [state, _dispatch] = useReducer(reducer, initialState);

  const dispatch = useCallback(
    (action) => {
      if (typeof action === "function") {
        return action(state, _dispatch);
      } else {
        return _dispatch(action);
      }
    },
    [state]
  );

  return { state, dispatch };
}

如上我们在调用_dispatch方法之前,判断一下传来的action,如果action是函数的话,就调用之并将state、_dispatch作为参数传入,最终我们返回修饰后的dispatch方法。

不知道你有没有发现这里的dispatch函数是不稳定,因为它将state作为依赖,每次state变化,dispatch就会变化。这会导致以dispatch为props的组件,每次都会重新render。这不是我们想要的,但是如果不写入state依赖,那么useCallback内部就拿不到最新的state

那有没有不将state写入deps,依然能拿到最新state的方法呢,其实hook也提供了解决方案,那就是useRef

useRef返回的 ref 对象在组件的整个生命周期内保持不变,并且变更 ref的current 属性不会引发组件重新渲染

通过这个特性,我们可以声明一个ref对象,并且在useEffect中将current赋值为最新的state对象。那么在我们装饰的dispatch函数中就可以通过ref.current拿到最新的state。

// store.js
function useStore() {
  const [state, _dispatch] = useReducer(reducer, initialState);

  const refs = useRef(state);

  useEffect(() => {
    refs.current = state;
  });

  const dispatch = useCallback(
    (action) => {
      if (typeof action === "function") {
        return action(refs.current, _dispatch); //refs.current拿到最新的state
      } else {
        return _dispatch(action);
      }
    },
    [_dispatch] // _dispatch本身是稳定的,所以我们的dispatch也能保持稳定
  );

  return { state, dispatch };
}

这样我们就可以定义一个login方法作为action,如下

// store.js
export const login = () => {
  return (state, dispatch) => {
    const errors = model.check({
      username: state.username,
      password: state.password,
      captcha: state.captcha,
    });

    const hasErrors =
      Object.values(errors).filter((error) => error.hasError).length > 0;

    dispatch({ type: "set", payload: { key: "errors", value: errors } });

    if (hasErrors) return;
    loginService.login(state);
  };
};

在LoginForm中,我们提交表单时就可以直接调用dispatch(login())了。

const LoginForm = () => {
  const { state, dispatch } = Store.useContainer();
  
  .....
return (
  <p>
    <field></field>
      ....
    <button> dispatch(login())}>提交</button>
  </p>
);
}

一个支持异步action的dispatch就完成了。

推荐教程:《JS教程

Das obige ist der detaillierte Inhalt vonVerwenden Sie Hooks, um ein Anmeldeformular zu schreiben – Frontier Development Team. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen