redux-thunk and in app architecture - want to render only views in views and dispatch GET actions in separate component

纵然是瞬间 提交于 2020-07-09 12:48:12

问题


I am using react-redux and redux-thunk in my application and there are two things I am trying to do:

  1. I want to be able to share the results of a GET request in two components. I know you can do this by connecting the two components to the store, but I want to make it so if the user lands on X page, then Y page cannot make the same GET request again (these two components are Thumbnail and Carousel). In other words, the GET request should be made once (not 100% sure what best practice is here for redux-thunk), and each component should be able to access the store and render the results in the component (this is easy and I can do)

  2. currently the GET request is the parent of the two children view components, which (I think) doesn't make sense. I only want to render a child view component in the parent view, not a GET request. If unclear it will make more sense if you read my code below

This is parent view (Gallery), which has a child component which dispatches an action to redux (using redux-thunk) that makes an 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>

    </>
  )  
}

This is FetchImages, which is dispatching the action (fetchImages) which makes the API call

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)

I think it makes more sense to have something like this:

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. What are some best practices to follow if two components can dispatch an action which makes a GET request but the dispatch should only be made once per time the user is on the website?

  2. Using redux-thunk, what are some best practices for separating concerns so that children view components are within parent view components and the smarter components which are shared between children view components (such as dispatching actions that make GET requests) are dispatched when the user lands on the page without the views and smarter components being directly together?

I'm a noob so thank you for any help


回答1:


Your first question: your component container should just dispatch the action that it needs data. How you should store async result in state and later handle result from state is something not covered in this answer but the later example uses a component named List that just dispatches getting a data page, selects the data page and dumps the data page in UI. The tunk action does an early return if the data is already in state.

In production application you probably want to store async api result with loading, error, requested and a bunch of extra info instead of assuming it is there or not there.

Your second question is partly answered by the first answer. Component containers should just dispatch an action indicating they need data and not have to know about the data already being there, already being requested or any of that stuff.

You can group functions that return a promise with the following code:

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

Now that we have code to group functions that return promises (function is not called when called again with same argument) we can try to apply this to thunk action creators.

Because a trunk action creator is not (...args)=>result but (...args)=>(dispatch,getState)=>result we can't pass the action creator directly to createGroup I created createGroupedThunkAction that adopts the function to group from (...args)=>(dispatch,getState)=>result to ([args],dispatch,getState)=>result while still returning a function with the right signature: (...args)=>(dispatch,getState)=>result.

Here is the example snippet:

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>

In that example there are 4 List components rendered, two for page 1 and two for page 2. All 4 will dispatch groupedGetData(page) but if you check the redux dev tools (or the console) you see MAKE_REQUEST and resulting SET_DATA is only dispatched twice (once for page 1 and once for page 2)

Relevant grouping functions with permanent memory cache is less than 50 lines and can be found here



来源:https://stackoverflow.com/questions/62207567/redux-thunk-and-in-app-architecture-want-to-render-only-views-in-views-and-dis

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!