您好,登錄后才能下訂單哦!
本篇文章展示了hooks實現登錄表單的方法具體操作,代碼簡明扼要容易理解,絕對能讓你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
React 是主流的前端框架,v16.8 版本引入了全新的 API,叫做 React Hooks,顛覆了以前的用法。Hooks讓我們的函數組件擁有了類似類組件的特性,比如local state、lifecycle,除了這幾個hooks還有其他的hooks,在此講解了解 Hooks API實現登錄表單。
一個簡單的登錄表單,包含用戶名、密碼、驗證碼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;
這種細粒度的狀態,很簡單也很直觀,但是狀態一多的話,要針對每個狀態寫相同的邏輯,就挺麻煩的,且太過分散。
我們將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> ); };
可以看到,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> ); };
我們使用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 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> ); };
然后我們在不輸入任何內容的時候點擊提交,就會觸發錯誤提示
到這一步,感覺我們的表單差不多了,功能好像完成了。但是這樣就沒問題了嗎,我們在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 className="login-form"> <Field name="username" placeholder="用戶名" value={state.username} error={state.errors["username"].errorMessage} dispatch={dispatch} /> ... <button onClick={submit}>提交</button> </p> ); };
在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);
這樣我們通過傳入dispatch,讓子組件內部去處理change事件,避免傳入onChange函數。同時將表單的狀態管理邏輯都遷移到了reducer中。
當我們的組件層級比較深的時候,想要使用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 /> </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包裹組件。
當組件上層最近的 <MyContext.Provider> 更新時,該 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;
這樣一來在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);
使用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 className="login-form"> <Field name="username" placeholder="用戶名" /> .... <button onClick={() => dispatch(login())}>提交</button> </p> ); }
一個支持異步action的dispatch就完成了。
看完上述內容,你們掌握hooks實現登錄表單的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。