是否可以在功能组件之外使用 React Hooks,或者我必须使用 mobx 或 redux?

Is it possible to use React Hooks outside of functional component, or i have to use mobx or redux?

我是 React 的新手,在阅读文档时,我发现有两种方法可以实现 React 组件,基于功能的和基于 class 的。我知道在 React 16.8 之前无法在功能组件中管理状态,但在那之后就有了 React Hooks。

问题是,React Hooks 似乎有一个限制,它们只能在功能组件内部使用。以server-client为例,需要在收到401的时候改变一个isAuthenticated状态

//client.js
import { useUserDispatch, signOut } from "auth";

export function request(url, args) {
  var dispatch = useUserDispatch();
  return fetch(url, args).then(response => {
  if (response.status === 401) {
    logout(dispatch);
  }
}
);

//auth.js
import React from "react";

var UserStateContext = React.createContext();
var UserDispatchContext = React.createContext();

function userReducer(state, action) {
  ...
}

function UserProvider({ children }) {
  var [state, dispatch] = React.useReducer(userReducer, {
    isAuthenticated: false,
  });

  return (
    <UserStateContext.Provider value={state}>
      <UserDispatchContext.Provider value={dispatch}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
}

function useUserState() {
  return React.useContext(UserStateContext);
}

function useUserDispatch() {
  return React.useContext(UserDispatchContext);
}

function signOut(dispatch) {
  dispatch({});
}

export { UserProvider, useUserState, useUserDispatch, loginUser, signOut };

上面的客户端代码会产生错误 "Hooks can only be called inside of the body of a function component"。 所以也许我必须将行 var dispatch = useUserDispatch() 向上移动到调用 request 的组件,并将 dispatch 作为道具传递给 request。 我觉得这是不对的,不仅 request 被迫关心一些无意义的(对它来说) dispatch,而且这个 dispatch 会传播到组件需要的任何地方 request.

对于基于class的组件,this.state也没有解决这个问题,但至少我可以使用mobx。

那么还有其他一些理想的方法可以解决这个问题吗?

Hooks 是基于 class 的组件的替代品,你应该为你的项目选择一个并坚持使用它,不要混淆它。创建钩子有一些动机,正如在文档中更好地说明的那样:hook motivation.

您可以单独创建挂钩函数,但它们应该由组件使用。这有点像将 HOC(高阶组件)与基于 class 的组件一起使用。

const myHook = () => {
  [foo, setFoo] = useState('john')
  // use effect for example if you need to run something at state updates
  useEffect(() => { 
    // do something on foo changes
  }, [foo])

  return [foo, setFoo] // returning state and setState you can use them by your component
}

现在你有了一个可重复使用的钩子,你可以使用你的组件:

const myComponent = (props) => {
  [foo, setFoo] = myHook()

  const handleFoo = () => { 
    // some logic
    setFoo(someValue)
  }      
  return (
    <div>
      <span>{foo}<span>
      <button onClick={handleFoo}>click</button>
    </div>
  )
}

obs:现在你应该避免将变量声明为 var,大多数情况下选择 const,如果它是一个需要更新的值变量(如数字)使用 let

是的,你必须使用 Redux 或 MobX 来解决这个问题。你必须在 Redux 或 MobX 的全局状态中维护 isAuthenticated 状态。然后做一个可以命名为 toggleAuthState 的动作,然后传递给 child 组件并从那里切换状态。

您也可以在这种情况下使用功能组件。 Class 基于组件不是必须使用 MobX 或 Redux。如果您将 HOC 维护为 Container,那么您可以将操作和状态传递给 child。

我展示了一个使用容器作为 HOC 的例子:

// Container
import React from "react"
import * as actions from "../actions" 
import ChildComponent from "../components/ChildComponent"
import { connect } from "react-redux"
import { bindActionCreators } from "redux"

const Container = props => <ChildComponent { ...props } />

const mapStateToProps = state => ({ ...state })

const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(Container)

然后在 ChildComponent 中,您可以随时使用您的状态和调度操作。

创建挂钩时必须参考 Rules of Hooks

您只能从 React 函数中调用钩子。 不要从常规 JavaScript 函数中调用 Hooks。相反,您可以:

✅ 从 React 函数组件调用 Hooks。 ✅ 从自定义 Hook 调用 Hook(了解它们 on this page)。

如果您想创建可重复使用的挂钩,则可以为您的函数创建自定义挂钩。 您可以在一个钩子中调用尽可能多的函数。

例如,这里我将 request 函数重构为一个钩子。

export function useRequest(url, args) {
  var userDispatch = useUserDispatch();
  const fetcher = React.useCallback(() => {
    return new Promise((resolve, reject) =>
      fetch(url, args)
        .then((response) => {
          if (response.status === 401) {
            logout();
            reject();
          }

          resolve(response);
        })
        .catch(reject)
    );
  }, [url, args]);

  return [fetcher, userDispatch];
}

然后消费它。

function App() {
  const [fetch, userDispatch] = useRequest("/url", {});
  React.useEffect(() => {
    fetch().then((response) => {
      userDispatch({ type: "USER_REQUEST", payload: response });
    });
  }, []);
  return <div>Hello world</div>;
}

Mobx 和 hooks 在实现上非常相似。两者都使用某种意义上“全局”的渲染上下文。 React 将渲染上下文与组件渲染上下文联系起来,但 Mobx 将渲染上下文分开。因此,这意味着必须在组件渲染生命周期内 创建 挂钩(但有时可以在该上下文之外调用)。 Mobx-react 将 Mobx 渲染生命周期与反应生命周期联系起来,当观察到的对象发生变化时触发反应重新渲染。所以 Mobx-react 将 react 渲染上下文嵌套在 Mobx 渲染上下文中。

React 在内部根据组件渲染周期内调用钩子的次数和顺序跟踪钩子。另一方面,Mobx 使用代理包装任何“可观察”对象,让 Mobx 上下文知道在 Mobx“运行 上下文”期间是否引用了它的任何属性(autorun 调用,本质上).然后,当 属性 被改变时,Mobx 知道“运行 上下文”关心那个 属性,并重新 运行 那些上下文。这意味着你可以在任何地方访问一个可观察对象,你可以改变它的 属性 并且 Mobx 会对其做出反应。

对于 React 状态挂钩,React 为状态对象提供了自定义 setter 函数。 React 然后使用对该 setter 的调用来知道何时需要重新渲染组件。 setter 可以在任何地方使用,甚至可以在 React 渲染之外使用,但是您只能 create 在渲染调用中挂钩,因为否则 React 无法判断将钩子绑定到哪个组件。创建一个钩子隐式地将它连接到当前的渲染上下文,这就是为什么必须在渲染调用中 创建 钩子的原因:钩子构建器在渲染调用之外没有任何意义,因为它们无法知道它们连接到什么组件——但是一旦绑定到一个组件,它们就需要在任何地方都可用。事实上,像 onClickfetch 回调这样的动作不会在渲染上下文中发生,尽管回调通常是在该上下文中 created - 动作回调在 React 完成渲染后发生(因为 javascript 是单线程的,所以渲染函数必须在其他任何事情发生之前完成)。

我也是这个时候过来的。长话短说,如果您想自己手动完成所有工作,您需要将 Redux 和 Thunk 与 Async Logic 结合使用,如 [1] 下面的 link 中的示例详细描述。

[1] https://redux.js.org/tutorials/essentials/part-5-async-logic

还有另一种解决方案可以提供 开箱即用的体验 异步 API(可以与 OpenAPI 和 GraphQL 一起使用,处理请求,提供具有生命周期等的缓存)包装来自 [1] 的内容及其称为 RTK 查询 [2].

[2]https://redux-toolkit.js.org/rtk-query/overview

下图解释了 [1] 过程的可视化.. 但我认为 RTK 查询 [2] 将所有内容都放在一个地方,可能是更好的解决方案。有一个快速入门指南 [3]。我会试一试:-)

[3] https://redux-toolkit.js.org/tutorials/rtk-query/