【问题标题】:In React, how to have a single instance of a custom hook in all components?在 React 中,如何在所有组件中拥有一个自定义钩子实例?
【发布时间】: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 请求之前是否已经获取了东西。干杯!

标签: reactjs react-redux react-hooks react-context


【解决方案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 中维护状态。

【讨论】:

    猜你喜欢
    • 2020-05-07
    • 1970-01-01
    • 2021-08-08
    • 2020-07-22
    • 1970-01-01
    • 2021-12-30
    • 2020-08-27
    • 1970-01-01
    • 2021-10-14
    相关资源
    最近更新 更多