在 React 中,如何在所有组件中拥有一个自定义钩子实例?

Posted

技术标签:

【中文标题】在 React 中,如何在所有组件中拥有一个自定义钩子实例?【英文标题】:In React, how to have a single instance of a custom hook in all components? 【发布时间】:2021-04-02 23:19:27 【问题描述】:

我有几个组件需要访问相同的玩家列表。 这个球员名单不会改变,但我不知道哪个组件首先需要它。 我的想法是,首先需要它的组件(axios 请求)更新我的 redux 存储,然后其他组件将为此使用相同的自定义钩子。

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

这是我的自定义钩子。想知道这样做是否有任何不便,甚至更好的模式。提前致谢。

顺便说一句...更好的是,我会在我的 usePlayersList 中将加载和错误设置为 useState 变量(而不是将它们发送到 redux 状态)。由于这会导致我出错(加载和错误在每个组件上变得不同,假设这种状态对每个组件都是独立的)我将它们发送到存储。

最好的问候, 费利佩。

【问题讨论】:

这看起来不错,但是您在哪里检查列表是否已获取? 好吧,因为 useEffect 只会执行一次,因为没有依赖项(除了调度),我认为该列表将在第一次调用 usePlayersList 时获取? 没错,但是如果这个自定义钩子在三个组件中使用,每个实例都会不同,因此 useEffect 中的代码将被执行 3 次。请参阅我创建的example,注意控制台中有两个日志,因为挂钩用于两个组件。这就是我在回答中建议使用上下文 API 的原因。 谢谢,拉梅什!因此,我将使用 Context API。如果我创建一个这样的自定义钩子,我总是必须检查在我的 axios 请求之前是否已经获取了东西。干杯! 【参考方案1】:

TL;DR 使用Context

说明:

每个组件都有它的自定义钩子实例。

在下面的示例中,我们可以看到调用setState 会更改Bla 组件中的状态,但不会更改Foo 组件中的状态:

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>

Context 和自定义钩子的组合可以用来解决上述问题:

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>

针对 OP 想要的原始答案:

定义上下文:

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

PlayersProvider 作为父组件添加到需要访问播放器的组件中。

在那些子组件内部:

import usePlayersContext from 'contextfilepath'

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

您还可以在 Provider 本身而不是在 redux 中维护状态。

【讨论】:

以上是关于在 React 中,如何在所有组件中拥有一个自定义钩子实例?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 react-markdown 中使用自定义组件

如何写自定义 React Hook?

如何在 TypeScript v2.x 中从自定义 React 组件公开“通用”事件

如何在 React ui-fabric 库中自定义 GroupedList 组件中的标题

Apollo 客户端:如何对自定义事件执行查询

如何在通过 NPM 分发时管理 React 组件(Redux、CSS)的自定义方面