搜尋
首頁web前端js教程用hooks寫個登入表單 - 前沿開發團隊

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

Jun 22, 2020 pm 06:02 PM
hooksreact.jsredux狀態管理

最近嘗試用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中文網其他相關文章!

陳述
本文轉載於:segmentfault。如有侵權,請聯絡admin@php.cn刪除
超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

神秘的JavaScript:它的作用以及為什麼重要神秘的JavaScript:它的作用以及為什麼重要Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python還是JavaScript更好?Python還是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

如何安裝JavaScript?如何安裝JavaScript?Apr 05, 2025 am 12:16 AM

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版