百分百源码网-让建站变得如此简单! 登录 注册 签到领金币!

主页 | 如何升级VIP | TAG标签

当前位置: 主页>网站教程>JS教程> 用hooks写个登录表单 - 前沿开发团队
分享文章到:

用hooks写个登录表单 - 前沿开发团队

发布时间:09/01 来源:未知 浏览: 关键词:
比来尝试用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 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>
  );
};

然后我们在不输入任何内容的时候点击提交,就会触发错误提醒:
Jietu20200530-150144.jpg

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 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中。

全局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 />
    </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);

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 className="login-form">
    <Field
      name="username"
      placeholder="会员名"
    />
      ....
    <button onClick={() => dispatch(login())}>提交</button>
  </p>
);
}

一个支撑异步action的dispatch就完成了。

引荐教程:《JS教程》

以上就是用hooks写个登录表单 - 前沿开发团队的具体内容,更多请关注百分百源码网其它相关文章!

打赏

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

百分百源码网 建议打赏1~10元,土豪随意,感谢您的阅读!

共有152人阅读,期待你的评论!发表评论
昵称: 网址: 验证码: 点击我更换图片
最新评论

本文标签

广告赞助

能出一分力是一分吧!

订阅获得更多模板

本文标签

广告赞助

订阅获得更多模板