In React, how to have a single instance of a custom hook in all components?
Asked Answered
D

1

9

I have several components that require access to the same list of players. This list of players won't change, but I do not know what component will required it first. My idea here is that whoever component requires it first (axios request) updates my redux store and then the other ones would just use this same custom hook for that.

export default function usePlayersList() {
  const {list, loading, error} = useSelector(state => state.accounts.players)
  const dispatch = useDispatch()
  
  useEffect(() => {
    try {
      const response = authAxios.get(endpoints.players.retrieve)
      response.then(res => {
          dispatch({
            type: "SET_PLAYERS_LIST",
            payload: {
              error: false,
              loading: false,
              list: res.data.payload
            }
          })
        })
    } catch (e) {
      console.log(e)
      dispatch({
        type: "SET_PLAYERS_LIST", 
        payload: {
          error: true,
          loading: false,
          list: []
        }
      })
    }
  }, [dispatch])
  return {list, loading, error}
}

This is my custom hook for that. Wondering if there is any inconvenience with doing that or even better patterns for such. Thanks in advance.

BTW... Even better, I would set loading and error as useState variables inside my usePlayersList (as opposed to sending them to redux state). Since this lead me to error (loading and error became different on each component, assuming that this state became individual to each component) I sent them to store.

Best regards, Felipe.

Dipper answered 24/12, 2020 at 14:12 Comment(4)
This looks good, but where are you checking if the list is already fetched?Derron
Well, since the useEffect only will be executed once, since there are no dependencies (except dispatch) I thought that the list would be fetched just on the first time it usePlayersList is invoked?Dipper
That's true but if this custom hook is being used in three components each instance would be different so the code inside useEffect will be executed three times. See this example I've created, notice how there are two logs in the console as the hook is used in two components. That's why I suggested context API in my answer.Derron
Thanks, Ramesh! Therefore, I will go with the Context API. If I create a custom hook like this, I would always have to check if things have already been fetched before my axios request. Cheers!Dipper
D
15

TL;DR use Context

Explanation:

Each component gets its instance of the custom hook.

In the example below, we can see that calling setState changes the state in the Bla component but not in the Foo component:

const { Fragment, useState, useEffect } = React;

const useCustomHook = () => {
  const [state, setState] = useState('old');
  return [state, setState];
};

const Foo = () => {
  const [state] = useCustomHook();
  return <div>state in Foo component: {state}</div>;
};

const Bla = () => {
  const [state, setState] = useCustomHook();
  return (
    <div>
      <div>state in Bla component: {state}</div>
      <button onClick={() => setState("new")}>setState</button>
    </div>
  );
};

function App() {
  return (
    <Fragment>
      <Foo />
      <Bla />
    </Fragment>
  );
}

ReactDOM.render(<App />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>

A combination of Context and custom hook can be used to fix the above issue:

const { createContext, useContext, useState, useEffect } = React;

const Context = createContext();
const ContextProvider = ({ children }) => {
  const value = useState("old");
  return <Context.Provider value={value} children={children} />;
};

const useCustomHook = () => {
  const [state, setState] = useContext(Context);
  return [state, setState];
};

const Foo = () => {
  const [state] = useCustomHook();
  return <div>state in Foo component: {state}</div>;
};

const Bla = () => {
  const [state, setState] = useCustomHook();
  return (
    <div>
      <div>state in Bla component: {state}</div>
      <button onClick={() => setState("new")}>setState</button>
    </div>
  );
};

function App() {
  return (
    <ContextProvider>
      <Foo />
      <Bla />
    </ContextProvider>
  );
}


ReactDOM.render(<App />, document.querySelector('#root'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>

Original answer that's specific to what OP wants:

Defining the context:

import React from 'react';

const PlayersContext = React.createContext();
const PlayersProvider = PlayersContext.Provider;

export const usePlayersContext = () => React.useContext(PlayersContext);

export default function PlayersProvider({ children }) {
  // add all the logic, side effects here and pass them to value
  const { list, loading, error } = useSelector(
    (state) => state.accounts.players
  );
  const dispatch = useDispatch();

  useEffect(() => {
    try {
      const response = authAxios.get(endpoints.players.retrieve);
      response.then((res) => {
        dispatch({
          type: 'SET_PLAYERS_LIST',
          payload: {
            error: false,
            loading: false,
            list: res.data.payload,
          },
        });
      });
    } catch (e) {
      console.log(e);
      dispatch({
        type: 'SET_PLAYERS_LIST',
        payload: {
          error: true,
          loading: false,
          list: [],
        },
      });
    }
  }, [dispatch]);

  return <PlayersProvider value={{ list, loading, error }}>{children}</PlayersProvider>;
}

Add the PlayersProvider as a parent to the components that need access to the players.

and inside those child components:

import {usePlayersContext} from 'contextfilepath'

function Child() {
  
  const { list, loading, error } = usePlayersContext()
  return ( ... )
}

You can also maintain the state in the Provider itself instead of in the redux.

Derron answered 24/12, 2020 at 15:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.