23

in this react example from https://reactjs.org/docs/hooks-custom.html, a custom hook is used in 2 different components to fetch online status of a user...

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

then its used in the 2 functions below:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

my question is, will the function be executed individually everywhere where it is imported into a component? Or is there some thing like sharing the state between components, if it is defined as a separate exported function? e.g I execute the function only once, and the "isOnline" state is the same in all components?

And if its individually fetched, how would I have to do it to fetch data only once globally, and then pass it to different components in my React app?

MMMM
  • 3,320
  • 8
  • 43
  • 80

5 Answers5

19

To share state data across multiple components in a large project, I recommend to use Redux or React Context.

Nevertheless, you can implement a global isOnline state using the Observer pattern (https://en.wikipedia.org/wiki/Observer_pattern):

// file: isOnline.tsx
import { useEffect, useState } from 'react';

// use global variables
let isOnline = false;
let observers: React.Dispatch<React.SetStateAction<boolean>>[] = [];

// changes global isOnline state and updates all observers
export const setIsOnline = (online: boolean) => {
  isOnline = online;
  observers.forEach((update) => update(isOnline));
};

// React Hook
export const useIsOnline = (): [boolean, (online: boolean) => void] => {
  const [isOnlineState, setIsOnlineState] = useState<boolean>(isOnline);

  useEffect(() => {
    // add setIsOnlineState to observers list
    observers.push(setIsOnlineState);

    // update isOnlineState with latest global isOnline state
    setIsOnlineState(isOnline);

    // remove this setIsOnlineState from observers, when component unmounts
    return () => {
      observers = observers.filter((update) => update !== setIsOnlineState);
    };
  }, []);

  // return global isOnline state and setter function
  return [isOnlineState, setIsOnline];
};
import { useIsOnline } from './isOnline';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useIsOnline();

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

function FriendStatus(props) {
  const isOnline = useIsOnline()[0];

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useIsOnline()[0];

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

Edit: Inside useIsOnline return isOnlineState created with useState instead of the isOnline variable because otherwise React can't pass the changes down to the component.

Edit 2: Make useEffect independent of isOnlineState so the hook does not unsubscribe and resubscribe on each variable change.

Sivert
  • 5
  • 4
neumann
  • 1,105
  • 10
  • 10
  • Superb. Why the '[0]' though? const isOnline = useIsOnline()[0]; – P Savva Jan 25 '23 at 17:48
  • 1
    @PSavva it's the same effect as destructuring `const [isOnline, setIsOnline] = useIsOnline`. Since it's an array that's returned, he's just referencing the first item, *isOnline* – bourgeois247 Apr 10 '23 at 13:21
4

In the case you mention, the function is executed at every component's render. So each component keeps a state value independently from the others. For this specific example it's what I would probably use.

If you need some state data to be shared globally (like authentication status), or between several components at different levels in DOM tree, one option is to use the React context.

First you define a new Context, by using the React.createContext() function. Check this link for more info: https://reactjs.org/docs/context.html

Then, you must use the Context.Provider (a component which keep context value and manages the updates) at top of your DOM hierarchy and then you can use the hook useContext() to refer to context value (provided from the Context provider) in descendant components at any level.

Check this link for that: https://reactjs.org/docs/hooks-reference.html#usecontext

marcodt89
  • 371
  • 1
  • 6
3

You can use this library to convert any custom hook into singleton https://www.npmjs.com/package/react-singleton-hook . This library creates a wrapper around your custom hook. The original hook is mounted only once into a hidden component. Other components and custom hooks consume wrapper and it delegates calls into your hook.

//useFriendStatusGlobal is a custom hook with globally shared data

const useFriendStatusGlobal = singletonHook(null, useFriendStatus);
light_keeper
  • 627
  • 5
  • 15
2

Whenever you use a custom hook, there will be separate instances of the hook within your App and they won't share the data unless you are using context API within them which is common across multiple instances or your ChatAPI holds data in one place eg in a singleton class instance or within browserStorage/using API.

useState or useReducers will have separate instances within your App.

You can simply think of this as useState and useEffect being written multiple times within your code app in individual component

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Here the `ChatAPI` observable presumably holds the online statuses in one place and updates subscribers on changes. So the context API isn't strictly the only way to share state between hooks, any shared instance of an object would do? – thathat Aug 22 '19 at 06:08
1

As others have mentioned, you need to have a state management in your React app. Context is not recommended for that: It is designed to avoid prop drilling to mid-components, not to be a state management library. All components that consume the context gets re-rendered when it updates, and this behavior is inefficient for a state management.

Redux might be a choice, but sometimes it brings to much boilerplate or concepts.

For this, I would recommend you Recoil, a simple state management library.

In Recoil, you have outsourced your state. Each piece of state is called an Atom. Then you bring the atom and its setState function by using the useRecoilState. Pretty straightforward if you already know how to use hooks.

Give a look at this example:

import React from 'react';
import {
  RecoilRoot,
  atom,
  useRecoilState
} from 'recoil';

const isOnlineState = atom(null);

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

Then, when you try to fetch the friends:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useRecoilState(isOnlineState);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

Now you can bring it anywhere:

const [isOnline, setIsOnline] = useRecoilState(isOnlineState)
Jonatan Kruszewski
  • 1,027
  • 3
  • 23