Jadual Kandungan
细粒度的state
粗粒度
加入表单校验
useReducer改写
全局store
dispatch一个函数
Rumah hujung hadapan web tutorial js 用hooks写个登录表单 - 前沿开发团队

用hooks写个登录表单 - 前沿开发团队

Jun 22, 2020 pm 06:02 PM
hooks react.js redux Pengurusan status

最近尝试用React hooks相关api写一个登陆表单,目的就是加深一下对hooks的理解。本文不会讲解具体api的使用,只是针对要实现的功能,一步一步深入。所以阅读前要对 hooks有基本的认识。最终的样子有点像用hooks写一个简单的类似redux的状态管理模式。

细粒度的state

一个简单的登录表单,包含用户名、密码、验证码3个输入项,也代表着表单的3个数据状态,我们简单的针对username、password、capacha分别通过useState建立状态关系,就是所谓的比较细粒度的状态划分。代码也很简单:

// 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 className="login-form">
      <input
        placeholder="用户名"
        value={username}
        onChange={(e) => {
          setUsername(e.target.value);
        }}
      />
      <input
        placeholder="密码"
        value={password}
        onChange={(e) => {
          setPassword(e.target.value);
        }}
      />
      <input
        placeholder="验证码"
        value={captcha}
        onChange={(e) => {
          setCaptcha(e.target.value);
        }}
      />
      <button onClick={submit}>提交</button>
    </p>
  );
};

export default LoginForm;
Salin selepas log masuk

这种细粒度的状态,很简单也很直观,但是状态一多的话,要针对每个状态写相同的逻辑,就挺麻烦的,且太过分散。

粗粒度

我们将username、password、capacha定义为一个state就是所谓粗粒度的状态划分:

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

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

  return (
    <p className="login-form">
      <input
        placeholder="用户名"
        value={state.username}
        onChange={(e) => {
          setState({
            ...state,
            username: e.target.value,
          });
        }}
      />
      ...
      <button onClick={submit}>提交</button>
    </p>
  );
};
Salin selepas log masuk

可以看到,setXXX 方法减少了,setState的命名也更贴切,只是这个setState不会自动合并状态项,需要我们手动合并。

加入表单校验

一个完整的表单当然不能缺少验证环节,为了能够在出现错误时,input下方显示错误信息,我们先抽出一个子组件Field:

const Filed = ({ placeholder, value, onChange, error }) => {
  return (
    <p className="form-field">
      <input placeholder={placeholder} value={value} onChange={onChange} />
      {error && <span>error</span>}
    </p>
  );
};
Salin selepas log masuk

我们使用schema-typed这个库来做一些字段定义及验证。它的使用很简单,api用起来类似React的PropType,我们定义如下字段验证:

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

然后在state中添加errors,并在submit方法中触发model.check进行校验。

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 className="login-form">
      <Field
        placeholder="用户名"
        value={state.username}
        error={state.errors["username"].errorMessage}
        onChange={(e) => {
          setState({
            ...state,
            username: e.target.value,
          });
        }}
      />
        ...
      <button onClick={submit}>提交</button>
    </p>
  );
};
Salin selepas log masuk

然后我们在不输入任何内容的时候点击提交,就会触发错误提示:
Jietu20200530-150144.jpg

useReducer改写

到这一步,感觉我们的表单差不多了,功能好像完成了。但是这样就没问题了吗,我们在Field组件打印 console.log(placeholder, "rendering"),当我们在输入用户名时,发现所的Field组件都重新渲染了。这是可以试着优化的。
那要如何做呢?首先要让Field组件在props不变时能避免重新渲染,我们使用React.memo来包裹Filed组件。

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件。如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现

export default React.memo(Filed);

但是仅仅这样的话,Field组件还是全部重新渲染了。这是因为我们的onChange函数每次都会返回新的函数对象,导致memo失效了。
我们可以把Filed的onChange函数用useCallback包裹起来,这样就不用每次组件渲染都生产新的函数对象了。

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

还有没有其他的方案呢,我们注意到了useReducer,

useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。它是useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数

useReducer的一个重要特征是,其返回的dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。那么我们就可以将dispatch放心传递给子组件而不用担心会导致子组件重新渲染。
我们首先定义好reducer函数,用来操作state:

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;
  }
}
Salin selepas log masuk

相应的在LoginForm中调用userReducer,传入我们的reducer函数和initialState

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

  const submit = ...

  return (
    <p className="login-form">
      <Field
        name="username"
        placeholder="用户名"
        value={state.username}
        error={state.errors["username"].errorMessage}
        dispatch={dispatch}
      />
      ...
      <button onClick={submit}>提交</button>
    </p>
  );
};
Salin selepas log masuk

在Field子组件中新增name属性标识更新的key,并传入dispatch方法

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

export default React.memo(Filed);
Salin selepas log masuk

这样我们通过传入dispatch,让子组件内部去处理change事件,避免传入onChange函数。同时将表单的状态管理逻辑都迁移到了reducer中。

全局store

当我们的组件层级比较深的时候,想要使用dispatch方法时,需要通过props层层传递,这显然是不方便的。这时我们可以使用React提供的Context  api来跨组件共享的状态和方法。

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

函数式组件可以利用createContext和useContext来实现。

这里我们不再讲如何用这两个api,大家看看文档基本就可以写出来了。我们使用unstated-next来实现,它本质上是对上述api的封装,使用起来更方便。

我们首先新建一个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);
Salin selepas log masuk

接着我们将LoginForm包裹一层Provider

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

const LoginFormContainer = () => {
  return (
    <Store.Provider>
      <LoginForm />
    </Store.Provider>
  );
};
Salin selepas log masuk

这样在子组件中就可以通过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);
Salin selepas log masuk

可以看到不用考虑组件层级就能轻易访问到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 {...props} value={value} dispatch={dispatch} error={error} />
  );
};

export default FiledContainer;
Salin selepas log masuk

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

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

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

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

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 };
}
Salin selepas log masuk

如上我们在调用_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 };
}
Salin selepas log masuk

这样我们就可以定义一个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);
  };
};
Salin selepas log masuk

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

const LoginForm = () => {
  const { state, dispatch } = Store.useContainer();
  
  .....
return (
  <p className="login-form">
    <Field
      name="username"
      placeholder="用户名"
    />
      ....
    <button onClick={() => dispatch(login())}>提交</button>
  </p>
);
}
Salin selepas log masuk

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

推荐教程:《JS教程

Atas ialah kandungan terperinci 用hooks写个登录表单 - 前沿开发团队. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn

Alat AI Hot

Undresser.AI Undress

Undresser.AI Undress

Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover

AI Clothes Remover

Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool

Undress AI Tool

Gambar buka pakaian secara percuma

Clothoff.io

Clothoff.io

Penyingkiran pakaian AI

AI Hentai Generator

AI Hentai Generator

Menjana ai hentai secara percuma.

Artikel Panas

R.E.P.O. Kristal tenaga dijelaskan dan apa yang mereka lakukan (kristal kuning)
2 minggu yang lalu By 尊渡假赌尊渡假赌尊渡假赌
Repo: Cara menghidupkan semula rakan sepasukan
4 minggu yang lalu By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island Adventure: Cara mendapatkan biji gergasi
4 minggu yang lalu By 尊渡假赌尊渡假赌尊渡假赌

Alat panas

Notepad++7.3.1

Notepad++7.3.1

Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina

SublimeText3 versi Cina

Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1

Hantar Studio 13.0.1

Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6

Dreamweaver CS6

Alat pembangunan web visual

SublimeText3 versi Mac

SublimeText3 versi Mac

Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Cara memanggil kaedah komponen anak dalam komponen induk React Cara memanggil kaedah komponen anak dalam komponen induk React Dec 27, 2022 pm 07:01 PM

Kaedah panggilan: 1. Panggilan dalam komponen kelas boleh dilaksanakan dengan menggunakan React.createRef(), pengisytiharan fungsi ref atau props atribut onRef tersuai 2. Panggilan dalam komponen fungsi dan komponen Hook boleh dilaksanakan dengan menggunakan useImperativeHandle atau forwardRef untuk membuang a; Rujuk Komponen kanak-kanak dilaksanakan.

Cara menggunakan cangkuk dalam vue3 Cara menggunakan cangkuk dalam vue3 May 11, 2023 pm 10:58 PM

1. Apakah cangkuk bermaksud cangkuk Apabila anda melihat "cangkuk", adakah anda memikirkan fungsi cangkuk? Malah, cangkuk benar-benar cara menulis fungsi. vue3 membangunkan CompositionAPI berdasarkan reacthooks, jadi ini bermakna CompositionAPI juga boleh menyesuaikan cangkuk enkapsulasi. Cangkuk dalam vue3 ialah cara menulis fungsi, iaitu mengekstrak kod js beberapa fungsi individu fail dan meletakkannya ke dalam fail js yang berasingan, atau beberapa kaedah/fungsi awam yang boleh digunakan semula. Sebenarnya, cangkuk agak serupa dengan mixin dalam vue2, tetapi berbanding dengan mixin, cangkuk lebih jelas.

Bagaimana untuk menyahpepijat kod sumber React? Pengenalan kepada kaedah penyahpepijatan menggunakan pelbagai alatan Bagaimana untuk menyahpepijat kod sumber React? Pengenalan kepada kaedah penyahpepijatan menggunakan pelbagai alatan Mar 31, 2023 pm 06:54 PM

Bagaimana untuk menyahpepijat kod sumber React? Artikel berikut akan membincangkan cara untuk menyahpepijat kod sumber React di bawah pelbagai alat, dan memperkenalkan cara untuk menyahpepijat kod sumber sebenar React dalam projek penyumbang, create-react-app, dan vite.

Pemahaman mendalam tentang Hooks tersuai React Pemahaman mendalam tentang Hooks tersuai React Apr 20, 2023 pm 06:22 PM

Cangkuk tersuai bertindak balas ialah cara untuk merangkum logik komponen dalam fungsi boleh guna semula. Ia menyediakan cara untuk menggunakan semula logik keadaan tanpa menulis kelas. Artikel ini akan memperkenalkan secara terperinci cara menyesuaikan cangkuk enkapsulasi.

Bagaimana untuk melaksanakan kemajuan menjawab dan fungsi pengurusan status dalam menjawab dalam talian Bagaimana untuk melaksanakan kemajuan menjawab dan fungsi pengurusan status dalam menjawab dalam talian Sep 24, 2023 pm 01:37 PM

Bagaimana untuk melaksanakan kemajuan menjawab dan fungsi pengurusan status dalam menjawab dalam talian memerlukan contoh kod khusus Apabila membangunkan sistem jawapan dalam talian, kemajuan menjawab dan pengurusan status adalah salah satu fungsi yang sangat penting. Dengan mereka bentuk dan melaksanakan dengan betul kemajuan menjawab dan fungsi pengurusan status, ia boleh membantu pengguna memahami kemajuan menjawab mereka sendiri dan meningkatkan pengalaman pengguna dan penyertaan pengguna. Berikut akan memperkenalkan cara melaksanakan kemajuan menjawab dan fungsi pengurusan status dalam menjawab dalam talian, dan memberikan contoh kod khusus. 1. Pelaksanaan fungsi pengurusan kemajuan menjawab soalan Dalam menjawab soalan dalam talian, pengurusan kemajuan menjawab soalan merujuk kepada

Bagaimanakah fungsi C++ menguruskan keadaan dalam pengaturcaraan serentak? Bagaimanakah fungsi C++ menguruskan keadaan dalam pengaturcaraan serentak? Apr 26, 2024 am 11:06 AM

Teknik biasa untuk mengurus keadaan fungsi dalam pengaturcaraan serentak C++ termasuk: Storan setempat-benang (TLS) membenarkan setiap utas mengekalkan salinan pembolehubah bebasnya sendiri. Pembolehubah atom membenarkan pembacaan dan penulisan atom pembolehubah yang dikongsi dalam persekitaran berbilang benang. Mutexes memastikan ketekalan keadaan dengan menghalang berbilang benang daripada melaksanakan bahagian kritikal pada masa yang sama.

Tutorial React Redux: Cara menggunakan Redux untuk mengurus keadaan bahagian hadapan Tutorial React Redux: Cara menggunakan Redux untuk mengurus keadaan bahagian hadapan Sep 26, 2023 am 11:33 AM

Tutorial ReactRedux: Cara Mengurus Keadaan Bahagian Depan dengan Redux React ialah perpustakaan JavaScript yang sangat popular untuk membina antara muka pengguna. Dan Redux ialah perpustakaan JavaScript untuk mengurus keadaan aplikasi. Bersama-sama mereka membantu kami mengurus keadaan bahagian hadapan dengan lebih baik. Artikel ini akan memperkenalkan cara menggunakan Redux untuk mengurus keadaan dalam aplikasi React dan memberikan contoh kod khusus. 1. Pasang dan sediakan Redux Mula-mula, kita perlu memasang Re

Mengapa React tidak menggunakan Vite sebagai pilihan pertama untuk membina apl Mengapa React tidak menggunakan Vite sebagai pilihan pertama untuk membina apl Feb 03, 2023 pm 06:41 PM

Mengapa React tidak menggunakan Vite sebagai pilihan pertama untuk membina aplikasi? Artikel berikut akan bercakap dengan anda tentang sebab mengapa React tidak mengesyorkan Vite sebagai pengesyoran lalai. Saya harap ia akan membantu anda!

See all articles