首页 > 解决方案 > redux-thunk 和应用架构中 - 只想在视图中呈现视图并在单独的组件中调度 GET 操作

问题描述

我在我的应用程序中使用 react-redux 和 redux-thunk ,我正在尝试做两件事:

  1. 我希望能够在两个组件中共享 GET 请求的结果。我知道你可以通过将两个组件连接到商店来做到这一点,但我想这样做,如果用户登陆 X 页面,那么 Y 页面不能再次发出相同的 GET 请求(这两个组件是缩略图和轮播)。换句话说,GET 请求应该发出一次(不是 100% 确定 redux-thunk 的最佳实践是什么),并且每个组件都应该能够访问存储并在组件中呈现结果(这很容易,我可以做)

  2. 目前 GET 请求是两个子视图组件的父级,(我认为)这没有意义。我只想在父视图中呈现子视图组件,而不是 GET 请求。如果不清楚,如果您阅读下面的代码会更有意义

这是父视图(Gallery),它有一个子组件,它向 Redux(使用 redux-thunk)分派一个操作,从而生成一个 API(FetchImages):

import ...

export default function Gallery() {

  return(
    <>

      <GalleryTabs />
      <GalleryText />

      <div className="gallery-images-container">
        <FetchImages /> ----> this is making an API request and rendering two child view components
      </div>

    </>
  )  
}

这是 FetchImages,它正在调度进行 API 调用的操作 (fetchImages)

import ...

function FetchImages({ fetchImages, imageData }) {

  useEffect(() => {
    fetchImages()
  }, [])

    return imageData.loading ? (
      <h2>Loading</h2>
    ) : imageData.error ? (
      <h2>Something went wrong {imageData.error}</h2>
    ) : (
      <>
      <Thumbnail /> -----> these two are views that are rendered if GET request is successful
      <Carousel />
      </>
    )
}

const mapStateToProps = state => {
    return {
        imageData: state.images
    }
}

const mapDispatchToProps = dispatch => {  
    return {
        fetchImages: () => dispatch(fetchImages())
    }
}

export default connect(
  mapStateToProps, 
  mapDispatchToProps
  )(FetchImages)

我认为有这样的东西更有意义:

import ...

export default function Gallery() {

  return(
    <>

      <GalleryTabs />
      <GalleryText />

      <div className="gallery-images-container">
        <Thumbnail />  -----> Thumbnail should be rendered here but not Carousel ( FetchImages here adds unnecessary complexity )   
      </div>

    </>
  )  
}

tldr

  1. 如果两个组件可以发送一个发出 GET 请求的操作,但每次用户访问网站时只能发送一次,那么应该遵循哪些最佳实践?

  2. 使用 redux-thunk,分离关注点的最佳实践是什么,以便子视图组件位于父视图组件中,并且在子视图组件之间共享的更智能的组件(例如发出 GET 请求的调度操作)在用户登陆时调度在页面上没有视图和更智能的组件直接在一起?

我是菜鸟,所以谢谢你的帮助

标签: reduxreact-reduxredux-thunkseparation-of-concerns

解决方案


您的第一个问题:您的组件容器应该只调度它需要数据的操作。您应该如何将异步结果存储在状态中并稍后处理来自状态的结果是此答案中未涵盖的内容,但后面的示例使用了一个名为 List 的组件,该组件仅调度获取数据页面,选择数据页面并将数据页面转储到 UI 中。如果数据已经处于状态,则 tunk 操作会提前返回。

在生产应用程序中,您可能希望存储带有加载、错误、请求和一堆额外信息的异步​​ api 结果,而不是假设它存在或不存在。

您的第二个问题部分由第一个答案回答。组件容器应该只发送一个动作来表明它们需要数据,而不必知道数据已经存在、已经被请求或任何类似的东西。

您可以使用以下代码对返回 Promise 的函数进行分组:

//resolves a promise later
const later = (time, result) =>
  new Promise((resolve) =>
    setTimeout(() => resolve(result), time)
  );
//group promise returning function
const createGroup = (cache) => (
  fn,
  getKey = (...x) => JSON.stringify(x)
) => (...args) => {
  const key = getKey(args);
  let result = cache.get(key);
  if (result) {
    return result;
  }
  //no cache
  result = Promise.resolve(fn.apply(null, args)).then(
    (r) => {
      cache.resolved(key); //tell cache promise is done
      return r;
    },
    (e) => {
      cache.resolve(key); //tell cache promise is done
      return Promise.reject(e);
    }
  );
  cache.set(key, result);
  return result;
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
  return {
    get: (key) => cache.get(key),
    set: (key, value) => cache.set(key, value),
    resolved: (x) => x,//will not remove cache entry after promise resolves
  };
};
//temporary memory cache store creator when the promise is done
//  the cache key is removed
const createTmpMemCache = () => {
  const map = new Map();
  const cache = createPermanentMemoryCache(map);
  cache.resolved = (key) => map.delete(key);
  return cache;
};

//tesgting function that returns a promise
const testPromise = (m) => {
  console.log(`test promise was called with ${m}`);
  return later(500, m);
};
const permanentCache = createPermanentMemoryCache();
const groupTestPromise = createGroup(permanentCache)(
  testPromise,
  //note that this causes all calls to the grouped function to
  //  be stored under the key 'p' no matter what the arguments
  //  passed are. In the later List example I leave this out
  //  and calls with different arguments are saved differently
  () => 'p'
);
Promise.all([
  //this uses a permanent cache where all calls to the function
  //  are saved under the same key so the testPromise function
  //  is only called once
  groupTestPromise('p1'),//this creates one promise that's used
                         //  in all other calls
  groupTestPromise('p2'),
])
  .then((result) => {
    console.log('first result:', result);
    return Promise.all([
      //testPromise function is not called again after first calls
      //  resolve because cache key is not removed after resolving
      //  these calls just return the same promises that
      //  groupTestPromise('p1') returned
      groupTestPromise('p3'),
      groupTestPromise('p4'),
    ]);
  })
  .then((result) => console.log('second result', result));
const tmpCache = createTmpMemCache();
const tmpGroupTestPromise = createGroup(tmpCache)(
  testPromise,
  //all calls to testPromise are saved with the same key
  //  no matter what arguments are passed
  () => 'p'
);
Promise.all([
  //this uses a temporary cache where all calls to the function
  //  are saved under the same key so the testPromise function
  //  is called twice, the t2 call returns the promise that was
  //  created with the t1 call because arguments are not used
  //  to save results
  tmpGroupTestPromise('t1'),//called once here
  tmpGroupTestPromise('t2'),//not called here using result of t1
])
  .then((result) => {
    console.log('tmp first result:', result);
    return Promise.all([
      //called once here with t3 becuase cache key is removed
      //  when promise resolves
      tmpGroupTestPromise('t3'),
      tmpGroupTestPromise('t4'),//result of t3 is returned
    ]);
  })
  .then((result) =>
    console.log('tmp second result', result)
  );
const tmpUniqueKeyForArg = createGroup(createTmpMemCache())(
  testPromise
  //no key function passed, this means cache key is created
  //  based on passed arguments
);
Promise.all([
  //this uses a temporary cache where all calls to the function
  //  are saved under key based on arguments
  tmpUniqueKeyForArg('u1'), //called here
  tmpUniqueKeyForArg('u2'), //called here (u2 is different argument)
  tmpUniqueKeyForArg('u1'), //not called here (already called with u1)
  tmpUniqueKeyForArg('u2'), //not called here (already called with u2)
])
  .then((result) => {
    console.log('unique first result:', result);
    return Promise.all([
      tmpUniqueKeyForArg('u1'), //called with u1 tmp cache removes key
                                // after promise is done
      tmpUniqueKeyForArg('u3'), //called with u3
      tmpUniqueKeyForArg('u3'), //not called, same argument
    ]);
  })
  .then((result) =>
    console.log('unique second result', result)
  );

现在我们已经有了代码来对返回 Promise 的函数进行分组(使用相同的参数再次调用函数时不会调用该函数),我们可以尝试将其应用于 thunk 动作创建者。

因为没有主干动作创建者,(...args)=>result(...args)=>(dispatch,getState)=>result我们不能将动作创建者直接传递给 createGroup 我创建了 createGroupedThunkAction ,它采用从到分组的函数,(...args)=>(dispatch,getState)=>result同时([args],dispatch,getState)=>result仍然返回具有正确签名的函数:(...args)=>(dispatch,getState)=>result

这是示例片段:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;

//resolves a promise later
const later = (time, result) =>
  new Promise((resolve) =>
    setTimeout(() => resolve(result), time)
  );
//group promise returning function
const createGroup = (cache) => (
  fn,
  getKey = (...x) => JSON.stringify(x)
) => (...args) => {
  const key = getKey(args);
  let result = cache.get(key);
  if (result) {
    return result;
  }
  //no cache
  result = Promise.resolve(fn.apply(null, args)).then(
    (r) => {
      cache.resolved(key); //tell cache promise is done
      return r;
    },
    (e) => {
      cache.resolve(key); //tell cache promise is done
      return Promise.reject(e);
    }
  );
  cache.set(key, result);
  return result;
};
//thunk action creators are not (...args)=>result but
//  (...args)=>(dispatch,getState)=>result
//  so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
  const group = createGroup(
    cache
  )((args, dispatch, getState) =>
    thunkAction.apply(null, args)(dispatch, getState)
  );

  return (...args) => (dispatch, getState) => {
    return group(args, dispatch, getState);
  };
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
  return {
    get: (key) => cache.get(key),
    set: (key, value) => cache.set(key, value),
    resolved: (x) => x,//will not remove cache entry after promise is done
  };
};
const initialState = {
  data: {},
};
//action types
const MAKE_REQUEST = 'MAKE_REQUEST';
const SET_DATA = 'SET_DATA';
//action creators
const setData = (data, page) => ({
  type: SET_DATA,
  payload: { data, page },
});
const makeRequest = (page) => ({
  type: MAKE_REQUEST,
  payload: page,
});
//standard thunk action returning a promise
const getData = (page) => (dispatch, getState) => {
  console.log('get data called with page:',page);
  if (createSelectDataPage(page)(getState())) {
    return; //do nothing if data is there
  }
  //return a promise before dispatching anything
  return Promise.resolve()
    .then(
      () => dispatch(makeRequest(page)) //only once
    )
    .then(() =>
      later(
        500,
        [1, 2, 3, 4, 5, 6].slice(
          (page - 1) * 3,
          (page - 1) * 3 + 3
        )
      )
    )
    .then((data) => dispatch(setData(data, page)));
};
//getData thunk action as a grouped function
const groupedGetData = createGroupedThunkAction(
  getData,//no getKey function so arguments are used as cache key
  createPermanentMemoryCache()
);
const reducer = (state, { type, payload }) => {
  console.log('action:', JSON.stringify({ type, payload }));
  if (type === SET_DATA) {
    const { data, page } = payload;
    return {
      ...state,
      data: { ...state.data, [page]: data },
    };
  }
  return state;
};
//selectors
const selectData = (state) => state.data;
const createSelectDataPage = (page) =>
  createSelector([selectData], (data) => data[page]);
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(
      //improvided thunk middlere
      ({ dispatch, getState }) => (next) => (action) => {
        if (typeof action === 'function') {
          return action(dispatch, getState);
        }
        return next(action);
      }
    )
  )
);
//List is a pure component using React.memo
const List = React.memo(function ListComponent({ page }) {
  const selectDataPage = React.useMemo(
    () => createSelectDataPage(page),
    [page]
  );
  const data = useSelector(selectDataPage);
  const dispatch = useDispatch();
  React.useEffect(() => {
    if (!data) {
      dispatch(groupedGetData(page));
    }
  }, [data, dispatch, page]);
  return (
    <div>
      <pre>{data}</pre>
    </div>
  );
});
const App = () => (
  <div>
    <List page={1} />
    <List page={1} />
    <List page={2} />
    <List page={2} />
  </div>
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>


<div id="root"></div>

在该示例中,渲染了 4 个 List 组件,两个用于第 1 页,两个用于第 2 页。所有 4 个都将分派groupedGetData(page),但如果您检查您看到的 redux 开发工具(或控制台)MAKE_REQUEST,结果SET_DATA只会分派两次(第 1 页一次一次用于第 2 页)

与永久内存缓存相关的分组函数少于 50 行,可以在这里找到


推荐阅读