亚洲最大看欧美片,亚洲图揄拍自拍另类图片,欧美精品v国产精品v呦,日本在线精品视频免费

  • 站長(zhǎng)資訊網(wǎng)
    最全最豐富的資訊網(wǎng)站

    用hooks寫個(gè)登錄表單 – 前沿開發(fā)團(tuán)隊(duì)

    最近嘗試用React hooks相關(guān)api寫一個(gè)登陸表單,目的就是加深一下對(duì)hooks的理解。本文不會(huì)講解具體api的使用,只是針對(duì)要實(shí)現(xiàn)的功能,一步一步深入。所以閱讀前要對(duì) hooks有基本的認(rèn)識(shí)。最終的樣子有點(diǎn)像用hooks寫一個(gè)簡(jiǎn)單的類似redux的狀態(tài)管理模式。

    細(xì)粒度的state

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

    // 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="驗(yàn)證碼"         value={captcha}         onChange={(e) => {           setCaptcha(e.target.value);         }}       />       <button onClick={submit}>提交</button>     </p>   ); };  export default LoginForm;

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

    粗粒度

    我們將username、password、capacha定義為一個(gè)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的命名也更貼切,只是這個(gè)setState不會(huì)自動(dòng)合并狀態(tài)項(xiàng),需要我們手動(dòng)合并。

    加入表單校驗(yàn)

    一個(gè)完整的表單當(dāng)然不能缺少驗(yàn)證環(huán)節(jié),為了能夠在出現(xiàn)錯(cuò)誤時(shí),input下方顯示錯(cuò)誤信息,我們先抽出一個(gè)子組件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這個(gè)庫(kù)來(lái)做一些字段定義及驗(yàn)證。它的使用很簡(jiǎn)單,api用起來(lái)類似React的PropType,我們定義如下字段驗(yàn)證:

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

    然后在state中添加errors,并在submit方法中觸發(fā)model.check進(jìn)行校驗(yàn)。

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

    然后我們?cè)诓惠斎肴魏蝺?nèi)容的時(shí)候點(diǎn)擊提交,就會(huì)觸發(fā)錯(cuò)誤提示:
    用hooks寫個(gè)登錄表單 - 前沿開發(fā)團(tuán)隊(duì)

    useReducer改寫

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

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

    export default React.memo(Filed);

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

    const changeUserName = useCallback((e) => {   const value = e.target.value;   setState((prevState) => { // 注意因?yàn)槲覀冊(cè)O(shè)置useCallback的依賴為空,所以這里要使用函數(shù)的形式來(lái)獲取最新的state(preState)     return {       ...prevState,       username: value,     };   }); }, []);

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

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

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

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

    全局store

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

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

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

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

    我們首先新建一個(gè)store.js文件,放置我們的reducer函數(shù),并新建一個(gè)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>   ); };

    這樣在子組件中就可以通過(guò)useContainer隨意的訪問(wèn)到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);

    可以看到不用考慮組件層級(jí)就能輕易訪問(wèn)到state和dispatch。但是這樣一來(lái)每次調(diào)用dispatch之后state都會(huì)變化,導(dǎo)致Context變化,那么子組件也會(huì)重新render了,即使我只更新username, 并且使用了memo包裹組件。

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

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

    這樣一來(lái)在value值不變的情況下,F(xiàn)ield組件就不會(huì)重新渲染了,當(dāng)然這里我們也可以抽象出一個(gè)類似connect高階組件來(lái)做這個(gè)事情:

    // 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一個(gè)函數(shù)

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

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

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

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

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

    通過(guò)這個(gè)特性,我們可以聲明一個(gè)ref對(duì)象,并且在useEffect中將current賦值為最新的state對(duì)象。那么在我們裝飾的dispatch函數(shù)中就可以通過(guò)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 }; }

    這樣我們就可以定義一個(gè)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中,我們提交表單時(shí)就可以直接調(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> ); }

    一個(gè)支持異步action的dispatch就完成了。

    推薦教程:《JS教程》

    贊(0)
    分享到: 更多 (0)
    網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)