최근에는 Hooks에 대한 이해를 심화시키기 위해 React Hooks 관련 API를 사용하여 로그인 양식을 작성하려고 했습니다. 이 문서에서는 특정 API의 사용을 설명하지 않지만 구현될 기능을 단계별로 자세히 살펴보겠습니다. 따라서 읽기 전에 Hook에 대한 기본적인 이해가 필요합니다. 최종 모습은 후크를 사용하여 간단한 redux와 유사한 상태 관리 모델을 작성하는 것과 약간 비슷합니다.
간단한 로그인 양식에는 사용자 이름, 비밀번호, 확인 코드라는 세 가지 입력 항목이 포함되어 있으며, 이는 양식의 세 가지 데이터 상태를 나타내기도 합니다. 사용자 이름, 비밀번호, Capacha에 대한 useState는 소위 상대적으로 세분화된 상태 분할인 상태 관계를 설정합니다. 코드도 매우 간단합니다. 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> ); };
然后我们在不输入任何内容的时候点击提交,就会触发错误提示:
到这一步,感觉我们的表单差不多了,功能好像完成了。但是这样就没问题了吗,我们在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 函数的标识是稳定的,并且不会在组件重新渲染时改变
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; } }
Coarse-grained
사용자 이름, 비밀번호, capacha를 상태로 정의합니다. 이는 소위 대략적인 상태 분할입니다.
const LoginForm = () => { const [state, dispatch] = useReducer(reducer, initialState); const submit = ... return ( <p> <field></field> ... <button>提交</button> </p> ); };
보시다시피 setXXX 메소드가 줄어들고 setState의 이름이 지정됩니다. 더 적합하지만 이 setState는 자동으로 상태 항목을 병합하지 않습니다. 병합하려면 수동으로 병합해야 합니다.
물론 완전한 양식에는 오류 발생 시 입력 아래에 오류 메시지를 표시하기 위해 먼저 하위 구성 요소 필드를 추출합니다.
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);
스키마를 사용합니다. 일부 필드 정의 및 유효성 검사를 수행하기 위한 형식화된 라이브러리입니다. API는 React의 PropType과 유사합니다. 다음과 같은 필드 확인을 정의합니다.
// 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);
그런 다음 상태에 오류를 추가하고 확인을 위해 제출 메서드에서 model.check
를 트리거합니다.
// LoginForm.js import { Store } from "./store"; const LoginFormContainer = () => { return ( <store.provider> <loginform></loginform> </store.provider> ); };
그런 다음 아무것도 입력하지 않고 제출을 클릭하면 오류 메시지가 트리거됩니다:
console.log(placeholder, "rendering")
를 인쇄하면 사용자 이름을 입력하면 모든 Field 구성 요소가 다시 렌더링됩니다. 이는 최적화될 수 있습니다. 🎜어떻게 하나요? 먼저, props가 변경되지 않은 상태에서 Field 구성 요소가 다시 렌더링되지 않도록 하세요. React.memo를 사용하여 Field 구성 요소를 래핑합니다. 🎜🎜React.memo는 상위 컴포넌트입니다. React.PureComponent와 매우 유사하지만 기능적 구성 요소에서만 작동합니다. 함수 구성 요소가 동일한 props를 사용하여 동일한 결과를 렌더링하는 경우 구성 요소의 렌더링 결과를 React.memo로 래핑하고 호출하여 구성 요소의 성능을 향상시킬 수 있습니다.🎜🎜export default React.memo(Filed );🎜🎜그러나 이 경우 모든 Field 구성 요소는 여전히 다시 렌더링됩니다. 이는 onChange 함수가 매번 새로운 함수 객체를 반환하여 메모가 무효화되기 때문입니다. 🎜Filed의 onChange 함수를 useCallback
으로 래핑할 수 있으므로 구성 요소가 렌더링될 때마다 새 함수 개체를 생성할 필요가 없습니다. 🎜// 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);
디스패치 함수의 식별자가 다음과 같다는 것입니다. 안정적이며 구성요소가 다시 렌더링될 때 변경되지 않습니다
. 그러면 하위 구성 요소가 다시 렌더링될 염려 없이 안전하게 하위 구성 요소에 디스패치를 전달할 수 있습니다. 🎜먼저 상태를 작동하기 위한 리듀서 함수를 정의합니다. 🎜// 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;
// 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);
// 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 }; }
我们首先新建一个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);
使用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教程》
위 내용은 후크를 사용하여 로그인 양식 작성 - Frontier Development Team의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!