目錄
細粒度的state
粗粒度
加入表單校驗
#useReducer改寫
全域store
dispatch一个函数
首頁 web前端 js教程 用hooks寫個登入表單 - 前沿開發團隊

用hooks寫個登入表單 - 前沿開發團隊

Jun 22, 2020 pm 06:02 PM
hooks react.js redux 狀態管理

最近嘗試用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>
      <input> {
          setUsername(e.target.value);
        }}
      />
      <input> {
          setPassword(e.target.value);
        }}
      />
      <input> {
          setCaptcha(e.target.value);
        }}
      />
      <button>提交</button>
    </p>
  );
};

export default LoginForm;
登入後複製

這種細粒度的狀態,很簡單也很直觀,但是狀態一多的話,要針對每個狀態寫相同的邏輯,就挺麻煩的,且太過分散。

粗粒度

我們將username、password、capacha定義為一個state就是所謂粗粒度的狀態分割:

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>
  );
};
登入後複製

可以看到,setXXX 方法減少了,setState的命名也比較貼切,只是這個setState不會自動合併狀態項,需要我們手動合併。

加入表單校驗

一個完整的表單當然不能缺少驗證環節,為了能夠在出現錯誤時,input下方顯示錯誤訊息,我們先抽出一個子元件Field:

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

我們使用schema-typed這個函式庫來做一些欄位定義及驗證。它的使用很簡單,api用起來類似React的PropType,我們定義如下字段驗證:

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

然後在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>
      <field> {
          setState({
            ...state,
            username: e.target.value,
          });
        }}
      />
        ...
      <button>提交</button>
    </field></p>
  );
};
登入後複製

然後我們在不輸入任何內容的時候點擊提交,就會觸發錯誤提示:
用hooks寫個登入表單 - 前沿開發團隊

#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,
    };
  });
}, []);
登入後複製

還有沒有其他的方案呢,我們注意到了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;
  }
}
登入後複製

對應的在LoginForm中呼叫userReducer,傳入我們的reducer函數和initialState

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

  const submit = ...

  return (
    <p>
      <field></field>
      ...
      <button>提交</button>
    </p>
  );
};
登入後複製

在Field子元件中新增name屬性識別更新的key,並傳入dispatch方法

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);
登入後複製

這樣我們透過傳入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);
登入後複製

接着我们将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教程

以上是用hooks寫個登入表單 - 前沿開發團隊的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1665
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
React父元件怎麼呼叫子元件的方法 React父元件怎麼呼叫子元件的方法 Dec 27, 2022 pm 07:01 PM

呼叫方法:1、類別元件中的呼叫可以利用React.createRef()、ref的函數式宣告或props自訂onRef屬性來實作;2、函式元件、Hook元件中的呼叫可以利用useImperativeHandle或forwardRef拋出子組件ref來實作。

vue3中的hooks如何使用 vue3中的hooks如何使用 May 11, 2023 pm 10:58 PM

一、什麼是hookshook是鉤子的意思,看到「鉤子」是不是就想到了鉤子函數?事實上,hooks還真是函數的一種寫法。 vue3借鏡reacthooks開發出了CompositionAPI,所以也就代表CompositionAPI也能進行自訂封裝hooks。 vue3中的hooks就是函數的一種寫法,就是將檔案的一些單獨功能的js程式碼進行抽離出來,放到單獨的js檔案中,或者說是一些可以重複使用的公共方法/功能。其實hooks和vue2中的mixin有點類似,但相對mixins而言,hooks更清楚

深入理解React的自訂Hook 深入理解React的自訂Hook Apr 20, 2023 pm 06:22 PM

React 自訂 Hook 是將元件邏輯封裝在可重複使用函數中的方式,它們提供了一種在不編寫類別的情況下重複使用狀態邏輯的方式。本文將詳細介紹如何自訂封裝 hook。

react怎麼設定div高度 react怎麼設定div高度 Jan 06, 2023 am 10:19 AM

react設定div高度的方法:1、透過css方式實現div高度;2、在state中宣告一個物件C,並在該物件中存放更換按鈕的樣式,然後取得A並重新設定C中的「marginTop」即可。

怎麼調試R​​eact源碼?多種工具下的除錯方法介紹 怎麼調試R​​eact源碼?多種工具下的除錯方法介紹 Mar 31, 2023 pm 06:54 PM

怎麼調試R​​eact源碼?以下這篇文章帶大家聊聊多種工具下的調試React源碼的方法,介紹一下在貢獻者、create-react-app、vite專案中如何debugger React的真實源碼,希望對大家有所幫助!

React為什麼不將Vite作為構建應用的首選 React為什麼不將Vite作為構建應用的首選 Feb 03, 2023 pm 06:41 PM

React為什麼不將Vite作為建置應用的首選?以下這篇文章就來帶大家聊聊React不將Vite當作預設推薦的原因,希望對大家有幫助!

如何實現線上答案中的答題進度與狀態管理功能 如何實現線上答案中的答題進度與狀態管理功能 Sep 24, 2023 pm 01:37 PM

如何實現線上答案中的答題進度和狀態管理功能,需要具體程式碼範例在開發線上答題系統時,答題進度和狀態管理是非常重要的功能之一。透過合理設計和實現答案進度和狀態管理功能,可以幫助使用者了解自己的答案進度,提升使用者體驗和使用者參與度。以下將介紹如何實現線上答案中的答題進度和狀態管理功能,並提供具體的程式碼範例。一、答題進度管理功能的實現線上答題中,答題進度管理是指使用者在答

React Redux教學:如何使用Redux管理前端狀態 React Redux教學:如何使用Redux管理前端狀態 Sep 26, 2023 am 11:33 AM

ReactRedux教學:如何使用Redux管理前端狀態React是一個非常受歡迎的JavaScript庫,用於建立使用者介面。而Redux是一種用於管理應用程式狀態的JavaScript庫。它們結合起來可以幫助我們更好地管理前端狀態。本文將介紹如何使用Redux在React應用程式中管理狀態,並提供具體的程式碼範例。一、安裝和設定Redux首先,我們需要安裝Re

See all articles