Is it possible to share states between components using the useState() hook in React?
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?
Solution 1:
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. Your state will then live in a store outside of React and components can connect/subscribe to the store to receive updates.
Solution 2:
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
Solution 3:
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.
Solution 4:
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.
Solution 5:
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>
);
}