简体   繁体   中英

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

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM