溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

hooks實現(xiàn)登錄表單的方法

發(fā)布時間:2020-06-23 12:58:11 來源:億速云 閱讀:714 作者:元一 欄目:web開發(fā)

本篇文章展示了hooks實現(xiàn)登錄表單的方法具體操作,代碼簡明扼要容易理解,絕對能讓你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

React 是主流的前端框架,v16.8 版本引入了全新的 API,叫做 React Hooks,顛覆了以前的用法。Hooks讓我們的函數(shù)組件擁有了類似類組件的特性,比如local state、lifecycle,除了這幾個hooks還有其他的hooks,在此講解了解 Hooks API實現(xiàn)登錄表單。

細粒度的state

一個簡單的登錄表單,包含用戶名、密碼、驗證碼3個輸入項,也代表著表單的3個數(shù)據(jù)狀態(tài),我們簡單的針對username、password、capacha分別通過useState建立狀態(tài)關(guān)系,就是所謂的比較細粒度的狀態(tài)劃分。代碼也很簡單:

// 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;

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

粗粒度

我們將username、password、capacha定義為一個state就是所謂粗粒度的狀態(tài)劃分:

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>
  );
};

可以看到,setXXX 方法減少了,setState的命名也更貼切,只是這個setState不會自動合并狀態(tài)項,需要我們手動合并。

加入表單校驗

一個完整的表單當(dāng)然不能缺少驗證環(huán)節(jié),為了能夠在出現(xiàn)錯誤時,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>
  );
};

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

const model = SchemaModel({
  username: StringType().isRequired("用戶名不能為空"),
  password: StringType().isRequired("密碼不能為空"),
  captcha: StringType()
    .isRequired("驗證碼不能為空")
    .rangeLength(4, 4, "驗證碼為4位字符"),
});

然后在state中添加errors,并在submit方法中觸發(fā)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>
  );
};

然后我們在不輸入任何內(nèi)容的時候點擊提交,就會觸發(fā)錯誤提示

useReducer改寫

到這一步,感覺我們的表單差不多了,功能好像完成了。但是這樣就沒問題了嗎,我們在Field組件打印 console.log(placeholder, "rendering"),當(dāng)我們在輸入用戶名時,發(fā)現(xiàn)所的Field組件都重新渲染了。這是可以試著優(yōu)化的。
那要如何做呢?首先要讓Field組件在props不變時能避免重新渲染,我們使用React.memo來包裹Filed組件。

React.memo 為高階組件。它與 React.PureComponent 非常相似,但只適用于函數(shù)組件。如果你的函數(shù)組件在給定相同 props 的情況下渲染相同的結(jié)果,那么你可以通過將其包裝在 React.memo 中調(diào)用,以此通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)

export default React.memo(Filed);

但是僅僅這樣的話,F(xiàn)ield組件還是全部重新渲染了。這是因為我們的onChange函數(shù)每次都會返回新的函數(shù)對象,導(dǎo)致memo失效了。
我們可以把Filed的onChange函數(shù)用useCallback包裹起來,這樣就不用每次組件渲染都生產(chǎn)新的函數(shù)對象了。

const changeUserName = useCallback((e) => {
  const value = e.target.value;
  setState((prevState) => { // 注意因為我們設(shè)置useCallback的依賴為空,所以這里要使用函數(shù)的形式來獲取最新的state(preState)
    return {
      ...prevState,
      username: value,
    };
  });
}, []);

還有沒有其他的方案呢,我們注意到了useReducer,

useReducer 是另一種可選方案,它更適合用于管理包含多個子值的 state 對象。它是useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,并返回當(dāng)前的 state 以及與其配套的 dispatch 方法。并且,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化,因為你可以向子組件傳遞 dispatch 而不是回調(diào)函數(shù)

useReducer的一個重要特征是,其返回的dispatch 函數(shù)的標(biāo)識是穩(wěn)定的,并且不會在組件重新渲染時改變。那么我們就可以將dispatch放心傳遞給子組件而不用擔(dān)心會導(dǎo)致子組件重新渲染。
我們首先定義好reducer函數(shù),用來操作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;
  }
}

相應(yīng)的在LoginForm中調(diào)用userReducer,傳入我們的reducer函數(shù)和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>
  );
};

在Field子組件中新增name屬性標(biāo)識更新的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);

這樣我們通過傳入dispatch,讓子組件內(nèi)部去處理change事件,避免傳入onChange函數(shù)。同時將表單的狀態(tài)管理邏輯都遷移到了reducer中。

全局store

當(dāng)我們的組件層級比較深的時候,想要使用dispatch方法時,需要通過props層層傳遞,這顯然是不方便的。這時我們可以使用React提供的Context  api來跨組件共享的狀態(tài)和方法。

Context 提供了一個無需為每層組件手動添加 props,就能在組件樹間進行數(shù)據(jù)傳遞的方法

函數(shù)式組件可以利用createContext和useContext來實現(xiàn)。

這里我們不再講如何用這兩個api,大家看看文檔基本就可以寫出來了。我們使用unstated-next來實現(xiàn),它本質(zhì)上是對上述api的封裝,使用起來更方便。

我們首先新建一個store.js文件,放置我們的reducer函數(shù),并新建一個useStore hook,返回我們關(guān)注的state和dispatch,然后調(diào)用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 />
    </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。但是這樣一來每次調(diào)用dispatch之后state都會變化,導(dǎo)致Context變化,那么子組件也會重新render了,即使我只更新username, 并且使用了memo包裹組件。

當(dāng)組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發(fā)重渲染,并使用最新傳遞給 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也會在組件本身使用 useContext 時重新渲染

那么怎么避免這種情況呢,回想一下使用redux時,我們并不是直接在組件內(nèi)部使用state,而是使用connect高階函數(shù)來注入我們需要的state和dispatch。我們也可以為Field組件創(chuàng)建一個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;

這樣一來在value值不變的情況下,F(xiàn)ield組件就不會重新渲染了,當(dāng)然這里我們也可以抽象出一個類似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);

dispatch一個函數(shù)

使用redux時,我習(xí)慣將一些邏輯寫到函數(shù)中,如dispatch(login()),
也就是使dispatch支持異步action。這個功能也很容易實現(xiàn),只需要裝飾一下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 };
}

如上我們在調(diào)用_dispatch方法之前,判斷一下傳來的action,如果action是函數(shù)的話,就調(diào)用之并將state、_dispatch作為參數(shù)傳入,最終我們返回修飾后的dispatch方法。

不知道你有沒有發(fā)現(xiàn)這里的dispatch函數(shù)是不穩(wěn)定,因為它將state作為依賴,每次state變化,dispatch就會變化。這會導(dǎo)致以dispatch為props的組件,每次都會重新render。這不是我們想要的,但是如果不寫入state依賴,那么useCallback內(nèi)部就拿不到最新的state。

那有沒有不將state寫入deps,依然能拿到最新state的方法呢,其實hook也提供了解決方案,那就是useRef

useRef返回的 ref 對象在組件的整個生命周期內(nèi)保持不變,并且變更 ref的current 屬性不會引發(fā)組件重新渲染

通過這個特性,我們可以聲明一個ref對象,并且在useEffect中將current賦值為最新的state對象。那么在我們裝飾的dispatch函數(shù)中就可以通過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本身是穩(wěn)定的,所以我們的dispatch也能保持穩(wěn)定
  );

  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中,我們提交表單時就可以直接調(diào)用dispatch(login())了。

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

一個支持異步action的dispatch就完成了。

看完上述內(nèi)容,你們掌握hooks實現(xiàn)登錄表單的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI