>웹 프론트엔드 >JS 튜토리얼 >후크를 사용하여 로그인 양식 작성 - Frontier Development Team

후크를 사용하여 로그인 양식 작성 - Frontier Development Team

hzc
hzc앞으로
2020-06-22 18:02:372468검색

최근에는 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>
  );
};

然后我们在不输入任何内容的时候点击提交,就会触发错误提示:
후크를 사용하여 로그인 양식 작성 - Frontier Development Team

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 函数的标识是稳定的,并且不会在组件重新渲染时改变

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

그런 다음 아무것도 입력하지 않고 제출을 클릭하면 오류 메시지가 트리거됩니다:

🎜🎜useReducer rewrite🎜🎜이 시점에서 우리의 양식이 거의 완성되었고 기능도 완료된 것 같습니다. 하지만 괜찮을까요? Field 구성 요소에 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);
🎜다른 솔루션이 있나요? useReducer를 발견했습니다. 🎜🎜useReducer는 여러 하위 값이 포함된 상태 개체를 관리하는 데 더 적합한 또 다른 옵션입니다. useState의 대안입니다. (state, action) => newState 형식의 리듀서를 수신하고 현재 상태와 일치하는 전달 메서드를 반환합니다. 게다가 콜백 함수 대신 하위 구성 요소에 디스패치를 ​​전달할 수 있기 때문에 useReducer를 사용하면 심층 업데이트를 트리거하는 구성 요소의 성능을 최적화할 수도 있습니다.🎜🎜useReducer의 중요한 기능은 반환되는 디스패치 함수의 식별자가 다음과 같다는 것입니다. 안정적이며 구성요소가 다시 렌더링될 때 변경되지 않습니다. 그러면 하위 구성 요소가 다시 렌더링될 염려 없이 안전하게 하위 구성 요소에 디스패치를 ​​전달할 수 있습니다. 🎜먼저 상태를 작동하기 위한 리듀서 함수를 정의합니다. 🎜
// 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;
🎜 이에 따라 LoginForm에서 userReducer를 호출하고 리듀서 함수와initialState를 전달합니다.🎜
// 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 };
}
🎜 이런 식으로 디스패치를 ​​전달함으로써 하위 구성 요소가 내부적으로 변경 이벤트를 처리하고 onChange 함수 전달을 피하도록 합니다. 동시에 양식의 상태 관리 로직이 감속기로 마이그레이션됩니다. 🎜🎜Global store🎜🎜컴포넌트 계층이 상대적으로 깊고 디스패치 방법을 사용하려면 레이어별로 props를 전달해야 하는데 이는 분명히 불편합니다. 이때 React에서 제공하는 Context API를 사용하여 구성 요소 간에 상태와 메서드를 공유할 수 있습니다. 🎜🎜Context는 각 구성 요소 레이어에 대한 prop을 수동으로 추가하지 않고도 구성 요소 트리 간에 데이터를 전송하는 방법을 제공합니다. 🎜🎜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></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);

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>
    <field></field>
      ....
    <button> dispatch(login())}>提交</button>
  </p>
);
}

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

推荐教程:《JS教程

위 내용은 후크를 사용하여 로그인 양식 작성 - Frontier Development Team의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제