是否可以使用 React 中的 useState() 钩子在组件之间共享状态?

IT技术 javascript reactjs react-hooks
2021-03-13 00:23:25

我正在尝试使用 React 中的新 Hook 功能。考虑到我有以下两个组件(使用 React Hooks) -

const HookComponent = () => {
  const [username, setUsername] = useState('Abrar');
  const [count, setState] = useState();
  const handleChange = (e) => {
    setUsername(e.target.value);
  }

  return (
    <div>
      <input name="userName" value={username} onChange={handleChange}/>
      <p>{username}</p>
      <p>From HookComponent: {count}</p>
    </div>
  )
}


const HookComponent2 = () => {
  const [count, setCount] = useState(999);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Hooks 声称解决了组件之间共享有状态逻辑的问题,但我发现HookComponent之间的状态HookComponent2不可共享。例如,countin的变化HookComponent2不会呈现HookComponent.

是否可以使用useState()钩子在组件之间共享状态

6个回答

如果您指的是组件状态,那么钩子不会帮助您在组件之间共享它。组件状态是组件本地的。如果您的状态存在于上下文中,那么useContexthook 会有所帮助。

从根本上说,我认为您误解了“在组件之间共享有状态逻辑”这一行。有状态逻辑与状态不同。有状态逻辑是你所做的修改状态的事情。例如,一个组件订阅了一个 store incomponentDidMount()和 unsubscribing in componentWillUnmount()这种订阅/取消订阅行为可以在钩子中实现,需要这种行为的组件可以只使用钩子。

如果您想在组件之间共享状态,有多种方法可以实现,每种方法都有自己的优点:

1. 提升状态

将状态提升到两个组件的共同祖先组件。

function Ancestor() {
    const [count, setCount] = useState(999);
    return <>
      <DescendantA count={count} onCountChange={setCount} />
      <DescendantB count={count} onCountChange={setCount} />
    </>;
  }

这种状态共享的方式与传统的状态使用方式没有本质的区别,钩子只是给了我们一种不同的方式来声明组件状态。

2. 背景

如果后代在组件层次结构中太深,并且您不想将状态向下传递太多层,则可以使用Context API

useContext您可以在子组件中利用一个钩子。

3.外部状态管理解决方案

状态管理库,如 Redux 或 Mobx。然后,您的状态将存在于 React 之外的商店中,并且组件可以连接/订阅该商店以接收更新。

Stateful logic is different from state 👍
2021-04-27 00:23:25
从技术上讲,您可以创建一个useGlobalState按照 OP 表面上想要的方式工作的函数(它在幕后使用 Context):github.com/dai-shi/react-hooks-global-state
2021-04-27 00:23:25
在您的示例 1 中,useState 适用于传递给下游组件,但它不允许进行双向状态设置
2021-05-08 00:23:25
是的。您可以将 setState 回调传递给组件,但这不在本问题的范围内。
2021-05-09 00:23:25
更新了我的答案以包含它,以防它对读者有所帮助。谢谢里德瓦恩!
2021-05-15 00:23:25

没有任何外部状态管理库是可能的。只需使用一个简单的observable实现:

function makeObservable(target) {
  let listeners = []; // initial listeners can be passed an an argument aswell
  let value = target;

  function get() {
    return value;
  }

  function set(newValue) {
    if (value === newValue) return;
    value = newValue;
    listeners.forEach((l) => l(value));
  }

  function subscribe(listenerFunc) {
    listeners.push(listenerFunc);
    return () => unsubscribe(listenerFunc); // will be used inside React.useEffect
  }

  function unsubscribe(listenerFunc) {
    listeners = listeners.filter((l) => l !== listenerFunc);
  }

  return {
    get,
    set,
    subscribe,
  };
}

然后创建一个 store 并使用subscribein钩住它以做出reactuseEffect

const userStore = makeObservable({ name: "user", count: 0 });

const useUser = () => {
  const [user, setUser] = React.useState(userStore.get());

  React.useEffect(() => {
    return userStore.subscribe(setUser);
  }, []);

  const actions = React.useMemo(() => {
    return {
      setName: (name) => userStore.set({ ...user, name }),
      incrementCount: () => userStore.set({ ...user, count: user.count + 1 }),
      decrementCount: () => userStore.set({ ...user, count: user.count - 1 }),
    }
  }, [user])

  return {
    state: user,
    actions
  }
}

这应该有效。不需要React.Context或提升状态

我希望我能投票一千次:D
2021-04-18 00:23:25
同意,这是一个可靠的,可靠的答案。我将把它加入书签以备将来使用。
2021-04-24 00:23:25
@MichaelJosephAubry 确实非常优雅!然而,很高兴注意到,如果您在服务器端渲染,它将无法工作,因为useEffect.
2021-05-07 00:23:25
这是可靠的,应该有更多的赞成......有人看到这有什么问题吗?
2021-05-08 00:23:25
我对此进行了改进,并在gist.github.com/SgtPooki/477014cf16436384f10a68268f86255b 上创建了 Observable 的typescript版本
2021-05-16 00:23:25

使用useBetween钩子可以做到这一点

在代码和框中查看

import React, { useState } from 'react';
import { useBetween } from 'use-between';

const useShareableState = () => {
  const [username, setUsername] = useState('Abrar');
  const [count, setCount] = useState(0);
  return {
    username,
    setUsername,
    count,
    setCount
  }
}


const HookComponent = () => {
  const { username, setUsername, count } = useBetween(useShareableState);

  const handleChange = (e) => {
    setUsername(e.target.value);
  }

  return (
    <div>
      <input name="userName" value={username} onChange={handleChange}/>
      <p>{username}</p>
      <p>From HookComponent: {count}</p>
    </div>
  )
}


const HookComponent2 = () => {
  const { count, setCount } = useBetween(useShareableState);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我们将 React hooks 有状态逻辑从HookComponentuseShareableState我们在每个组件中调用useShareableStateusing useBetween

useBetween是一种调用任何钩子的方法。但是这样状态就不会存储在 React 组件中。对于同一个钩子,调用的结果是一样的。所以我们可以在不同的组件中调用一个钩子并在一种状态上协同工作。当更新共享状态时,使用它的每个组件也将被更新。

免责声明:我是该use-between软件包的作者

@SlavaBirch 这个库拥有迄今为止最好的 API,就像immer,非常简单但功能强大。但是我很遗憾地说它还不稳定。到目前为止,我自己遇到了 3 个错误。很乐意与您联系以了解有关该项目原始架构的更多信息,以便我可以做出贡献。
2021-04-24 00:23:25
我最喜欢这个,替换我的 observable 实现,因为已经订阅事件并设置本地状态以便重新渲染。也比过度设计的 useContext 更好。
2021-05-10 00:23:25
但是您可以使用 useBetween 而不是 Redux 来组织应用程序的状态和控制它的逻辑。
2021-05-10 00:23:25
感谢您的贡献!还请在代码中添加一些解释并引用与您链接的内容最相关的内容,因为链接可能会过期!:)
2021-05-11 00:23:25
谢谢!有一个。内部不可能使用 React Context。(不支持useContext,因为共享逻辑不是在一个React组件内部,而是在多个组件之间使用)因此,您不能在ShareableState逻辑内部使用例如来自Redux的useSelector。
2021-05-14 00:23:25

文档指出:

我们从 React 导入 useState Hook。它让我们在函数组件中保持本地状态。

没有提到状态可以跨组件共享,useState钩子只是给你一种在一条指令中声明状态字段及其对应设置器的更快方法。

看看@Yangshun Tay 非常精确的答案,这些是您必须跨组件共享状态的选项
2021-05-01 00:23:25
是否有一种钩子方式可以跨组件共享状态?不一定与useState()
2021-05-09 00:23:25

我已经创建了 hooksy,可以让你做到这一点 - https://github.com/pie6k/hooks

import { createStore } from 'hooksy';

interface UserData {
  username: string;
}

const defaultUser: UserData = { username: 'Foo' };

export const [useUserStore] = createStore(defaultUser); // we've created store with initial value.
// useUserStore has the same signature like react useState hook, but the state will be shared across all components using it

然后在任何组件中

import React from 'react';

import { useUserStore } from './userStore';

export function UserInfo() {
  const [user, setUser] = useUserStore(); // use it the same way like useState, but have state shared across any component using it (eg. if any of them will call setUser - all other components using it will get re-rendered with new state)

  function login() {
    setUser({ username: 'Foo' })
  }

  return (
    <div>
      {!user && <strong>You're logged out<button onPress={login}>Login</button></strong>}
      {user && <strong>Logged as <strong>{user.username}</strong></strong>}
    </div>
  );
}
好吧,让我们试一试。
2021-05-11 00:23:25