如何使用反应挂钩在组件之间共享资产请求?

时间:2019-08-08 18:11:35

标签: javascript reactjs react-hooks

我正在寻找一种使用钩子在任意数量的消费者之间共享单个资产请求/响应的好方法。

在提供的代码段中,我使用URL跟踪请求同一资产的组件数量。我存储了请求,如果组件2在卸载组件1之前发出了相同的请求,则组件2将使用缓存的请求。如果所有组件都已卸载,我们可以中止请求并将其从缓存中删除。

/*
 * Share asset requests between components.
 * const { data, loaded, total, error } = useAssetLoader('largeFile.tiff', 'arraybuffer');
 */

import { useEffect, useState } from 'react';
import request from 'superagent';

// Keep track of how many instances are using a each request
// when all instances have been unmounted, we can abort the request
const instances = {};

// Keep track of requests to share between instances
const requests = {};

export default function useAssetLoader(url, responseType) {
  const [data, setData] = useState();
  const [loaded, setLoaded] = useState();
  const [total, setTotal] = useState();
  const [error, setError] = useState();

  useEffect(() => {
    if (!url) {
      return () => {};
    }

    const key = url + responseType;

    instances[key] = (instances[key] || 0) + 1;

    if (!requests[key]) {
      requests[key] = request(url);

      if (responseType) {
        requests[key].responseType(responseType);
      }
    }

    requests[key].on('progress', (event) => {
      if (event.direction === 'download') {
        setLoaded(event.loaded);
        setTotal(event.total);
      }
    });

    requests[key].on('error', setError);

    req.then(response => setData(response.body || response.text));

    return () => {
      instances[key] -= 1;

      // When all components are unmounted we can abort request
      if (instances[key] === 0) {
        delete instances[key];

        if (requests[key]) {
          requests[key].abort();
          delete requests[key];
        }
      }
    };
  }, [url, responseType]);

  return { data, loaded, total, error };
}

是否有更好的方法?我也想将这种模式用于其他异步任务。

2 个答案:

答案 0 :(得分:0)

您可以使用上下文共享对所有组件“全局”的数据。 AssetLoaderContext.js

import React, { Component } from 'react'

//Add all states that you want to share over here. 
const defaultState = { data: {}, instances: {}, requests: {} }
const AssetLoaderContext = React.createContext(defaultState)

class AssetLoaderProvider extends Component {
  state = {
    data: {},
    instances: {},
    requests: {}
  }
  setContextState = args => {
    this.setState(args)
  }
  render() {
    return (
      <AssetLoaderContext
        value={{
          setContextState: this.setContextState,
          ...this.state
        }}>
        {children}
      </AssetLoaderContext>
    )
  }
}

//We're making a different file so the index.js/App.js doesn't get cluttered.
//You can also put all your context files in one folder and name it "context" so all 
//the "global" and shared state is in one place. 

export default AssetLoaderContext
export { AssetLoaderProvider }

SomeComponent.js

import AssetLoaderContext from './context/AssetLoaderContext'
//other imports

class SomeComponent extends React {
  static contextType = AssetLoaderContext
  state = {
    date: {}
    // ....etc
  }
  componentDidMount() {
    console.log(this.context)
  }
  componentDidUpdate() {
    const assetLoader = this.context
    // do stuff with this.props.url, this.props.responseType.
    //Then set the AssetLoaderContext state. 
    const data = this.state.data
    assetLoader.setContextState({
      data
      // etc.
    })
  }
  render() {
    //you can just get the context in OtherComponent without passing it here. 
    return <OtherComponent />
  }
}

App.js

import AssetLoaderProvider from './context/AssetLoaderContext'

//At the top level wrap your components in AssetLoaderProvider so each
//component can be a "consumer" of the data provded by the context. 
ReactDOM.render(
  <Router>
    <AssetLoaderProvider>
      <App />
    </AssetLoaderProvider>
  </Router>,
  document.getElementById('root')
)

要清除上下文,您可以仅调用setContextState({data: {}, instances: {}, requests: {})或编写指定的clearContextData方法。

答案 1 :(得分:0)

您肯定走在正确的轨道上,但是您需要使用useContext钩子以及useStateuseEffect。让我们从一个带有上下文和请求的助手挂钩开始,并返回一个最终将提供数据的上下文提供者:

export const useFetchContext = (
  Ctx,
  request,
  initialData = null,
) => {
  const [data, setData] = useState(initialData);
  useEffect(() => {
    const getData = async () => {
      try {
        const response = await fetch(request);
        const json = await response.json();
        setData(json);
      } catch (err) {
        // handle error
      }
    };

    if (!data) getData();

  // currently only runs once. Add request and data as dependencies
  // in the array below if you'll be feeding it a stateful request.
  }, []);

  const CtxProvider = ({ children }) => {
    return <Ctx.Provider value={data}>{children}</Ctx.Provider>;
  };

  return CtxProvider;
};

现在,我们有了一些可重用的逻辑来将结果提取并将其粘贴到上下文提供程序中,我们需要向其提供一些东西以便可以使用它:

import { useFetchContext } from './path/to/file';

const dataContext = React.createContext();
export const useDataContextProvider = () => useFetchContext(dataContext, url);
export const useDataContext = () => useContext(dataContext);

冲洗并重复执行要全局获取要获取的文件的每个文件。您甚至可以将步骤向上移至index.js并将您的<App />组件包装在提供程序中,这样它们就不会阻塞您的JSX。

因此,现在您有了一个辅助程序,当您向其提供上下文时将获取数据并返回提供者,使用它的钩子(useDataContextProvider),以及一个“解包”全局上下文数据的钩子(useDataContext)。

因此,在App.js中:

import { useDataContextProvider } from './path/to/file';

export default () => {
  const CtxProvider = useDataContextProvider();
  return (
    <CtxProvider>
      <YourComponentsHere />
    </CtxProvider>
  );
};

提供者的子代现在将可以通过另一个钩子访问数据(一旦获取完成)。因此,在组件中您要使用上下文:

import { useDataContext } from './path/to/file';

export default ({
  someProp
}) => {
  // This will be undefined, or whatever you set as the default,
  // until the fetch completes. Don't try to e.g. destructure it.
  const contextData = useDataContext();
  return <SomeJSX />;
};
相关问题