Is it possible to share states between components using the useState() hook in React?
Asked Answered
T

8

128

I was experimenting with the new Hook feature in React. Considering I have the following two components (using 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 claim to solve the problem of sharing stateful logic between components but I found that the states between HookComponent and HookComponent2 are not sharable. For example the change of count in HookComponent2 does not render a change in the HookComponent.

Is it possible to share states between components using the useState() hook?

Tut answered 23/11, 2018 at 18:40 Comment(0)
K
159

If you are referring to component state, then hooks will not help you share it between components. Component state is local to the component. If your state lives in context, then useContext hook would be helpful.

Fundamentally, I think you misunderstood the line "sharing stateful logic between components". Stateful logic is different from state. Stateful logic is stuff that you do that modifies state. For e.g., a component subscribing to a store in componentDidMount() and unsubscribing in componentWillUnmount(). This subscribing/unsubscribing behavior can be implemented in a hook and components which need this behavior can just use the hook.

If you want to share state between components, there are various ways to do so, each with its own merits:

1. Lift State Up

Lift state up to a common ancestor component of the two components.

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

This state sharing approach is not fundamentally different from the traditional way of using state, hooks just give us a different way to declare component state.

2. Context

If the descendants are too deep down in the component hierarchy and you don't want to pass the state down too many layers, you could use the Context API.

There's a useContext hook which you can leverage on within the child components.

3. External State Management Solution

State management libraries like Redux or Mobx or Zustand. Your state will then live in a store outside of React and components can connect/subscribe to the store to receive updates.

Korrie answered 24/11, 2018 at 5:35 Comment(7)
Stateful logic is different from state πŸ‘ – Crossway
Technically you can create a useGlobalState function that works the way OP ostensibly wants (it uses Context behind-the-scenes): github.com/dai-shi/react-hooks-global-state – Lyons
in your example 1, useState works for passing to downstream components, but it doesnt allow for two-way state setting – Rabia
Yes. You can pass the setState callback down into the components, but that's not within the scope of this question. – Korrie
Have updated my answer to include that in case it'll be helpful to readers. Thanks Ridhwaan! – Korrie
I think you missed the best answer. You should probably use "stores". Each store will handle the state of one functional entity (for instance "users" or "products"). There are tons of libraries to do this, like Recoil. But it is simple enough so you implement yourself a light weight solution. That is how every other framework handle state (in backend like Spring or .NET, in Angular etc...) – Gunnysack
@Gunnysack Isn't "External state management solution" the same as stores? I hesitate to recommend using external solutions as it's usually better to have fewer dependencies and rely on what React provides. – Korrie
R
92

It is possible without any external state management library. Just use a simple observable implementation:

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

And then create a store and hook it to react by using subscribe in useEffect:

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

And that should work. No need for React.Context or lifting state up

Rondon answered 25/5, 2020 at 12:3 Comment(11)
This is solid, should have more upvotes... Anyone see any issues with this? – Chelsiechelsy
Agreed, this is a solid, solid answer. I'll be bookmarking this for future use. – Beauvais
@MichaelJosephAubry very elegant indeed ! Yet it's good to notice that it won't work if you render on server side, because of useEffect. – Retreat
I wish I could upvote this thousand times :D – Breath
I improved upon this and created a typescript version of the Observable here at gist.github.com/SgtPooki/477014cf16436384f10a68268f86255b – Adverse
it's a "stores pattern". I don't understand why react team doesn't give something like this out of the box. It's simple, it is like "services" (used in backend or in Angular). They are tons of libraries for this "stores pattern". This better than "context" because it alows to split logic in different files. It is simple, not like Redux. And it is better than composition which should never be used for more than 2 components deep, and usually force to add a variable in a component where it makes no sense (for instance, I previously had to add a "selectedProject" in my top "Layout" component). – Gunnysack
@MichaelJosephAubry This approach has the same drawback of using context: each time you update any of the properties, all subscribed components will re-render. – Subdeacon
@Subdeacon true. I'll update the answer soon to handle this issue – Rondon
Observer Pattern has been working well for me for over a decade. And has been working well for people in general for much longer than that. To anyone getting started in Software Engineering out there--look into "The Gang of Four", what they wrote, and keep it nearby. Everything new is old again. – Jayme
I had an issue with the above pattern. Can put the entire thing here so I posed below. Anyone have an idea why it might not work with async calls? – Spillman
You could also inject the get/set using proxies so you can get and set a variable like you normally would (i.e. x = 1) without having to call the set or get function. This is the basic groundwork for MobX. – Dambrosio
S
24

This is possible using the useBetween hook.

See in codesandbox

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

We move React hooks stateful logic from HookComponent to useShareableState. We call useShareableState using useBetween in each component.

useBetween is a way to call any hook. But so that the state will not be stored in the React component. For the same hook, the result of the call will be the same. So we can call one hook in different components and work together on one state. When updating the shared state, each component using it will be updated too.

Disclaimer: I'm the author of the use-between package.

S answered 26/5, 2020 at 6:18 Comment(9)
Thanks for your contribution! Please also add some explanation to the code and quote the most relevant content of what you linked, as links may expire! :) – Polypropylene
I like this the most, replace my observable implementation because have subscribe to events and set the local state in order to re-render. Better than useContext too, which is over engineered. – Stertorous
This is like React Context but without the boilerplate, not sure if there's any downside so far? – Lobeline
Thanks! There is one. There is no possibility of using React Context inside. (useContext is not supported, since the shared logic is not inside the one React component, but used between multiple them) Therefore, you cannot use, for example, useSelector from Redux inside ShareableState logic. – S
But you can use useBetween instead of Redux to organize the state of your application and the logic to control it. – S
@SlavaBirch this library has the best API so far, it's like immer, very simple yet powerful. However I would sadly say that it's not stable yet. Encountered 3 bugs myself so far. Would love to contact you to know more about the original architecture of the project so that I can contribute. – Lobeline
Thanks to Loi Nguyen Huynh for your great contributions to the latest release! Everything works excellent now. – S
I want to update this example to show off how easy it is to use across pages / but example kinda falls over. Well done - this is a brilliant simple solution. – Analogy
OMG... it's tiny but a life saver for me – Pagepageant
D
5

the doc states:

We import the useState Hook from React. It lets us keep local state in a function component.

it is not mentioned that the state could be shared across components, useState hook just give you a quicker way to declare a state field and its correspondent setter in one single instruction.

Ditch answered 23/11, 2018 at 18:55 Comment(2)
Is there a hook way for sharing the state across components? Not necessarily with useState() – Tut
take a look at @Yangshun Tay very precise answer, those are the options you have to share state across components – Ditch
B
4

I've created hooksy that allows you to do exactly this - https://github.com/pie6k/hooksy

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

And later in any component

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>
  );
}
Breastwork answered 23/6, 2019 at 18:38 Comment(1)
Well, Let's give it a try. – Distillation
S
4

I'm going to hell for this:

// src/hooks/useMessagePipe.ts
import { useReducer } from 'react'

let message = undefined

export default function useMessagePipe() {
  const triggerRender = useReducer((bool) => !bool, true)[1]
  function update(term: string) {
    message = term.length > 0 ? term : undefined
    triggerRender()
  }

  return {message: message, sendMessage: update}
}

Full explanation over at: https://mcmap.net/q/175677/-how-to-make-a-shared-state-between-two-react-components


Yes, this is the dirtiest and most concise way i could come up with for solving that specific use case. And yes, for a clean way, you probably want to learn how to useContext, or alternatively take a look at react-easy-state or useBetween for low-footprint solutions, and flux or redux for the real thing.

Snail answered 8/7, 2022 at 22:47 Comment(0)
M
2

With hooks its not directly possible. I recommend you to take a look at react-easy-state. https://github.com/solkimicreb/react-easy-state

I use it in big Apps and it works like a charm.

Megass answered 12/3, 2019 at 16:26 Comment(0)
C
1

You will still need to lift your state up to an ancestor component of HookComponent1 and HookComponent2. That's how you share state before and the latest hook api doesnt change anything about it.

Conscience answered 23/11, 2018 at 20:7 Comment(1)
In the hook RFC - github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md, it says about "reusing logic between components", does hook provide an alternative to lifting the state to ancestor component? – Tut

© 2022 - 2024 β€” McMap. All rights reserved.