I am using react-redux and redux-thunk in my application and there are two things I am trying to do:
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)
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
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?
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
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
I have a Redux app and it is working perfectly without any errors. Now I am trying to test it with Enzyme, Jest and Sinon:
it('calls constructor', () => {
sinon.spy(SavedVariantsComponent.prototype, 'constructor')
const store = configureStore()(STATE1)
wrapper = mount(<SavedVariantsComponent store={store} match={{ params: {} }} />)
expect(SavedVariantsComponent.prototype.constructor).toHaveProperty('callCount', 1)
})
In SavedVariantsComponent I have mapDispatchToProps:
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onSubmit: (updates) => {
dispatch(updateSavedVariantTable(updates))
const { match, analysisGroup } = ownProps
const { familyGuid, variantGuid, tagArray, gene } = match.params
const familyGuids = familyGuid ? [familyGuid] : (analysisGroup || {}).familyGuids
const combineVariants = /combined_variants/.test(match.url)
dispatch(loadSavedVariants(combineVariants, familyGuids, variantGuid, tagArray, gene))
},
loadSavedVariants: (...args) => dispatch(loadSavedVariants(...args)),
}
}
And loadSavedVariants look like that:
export const loadSavedVariants = (combineVariants, familyGuids, variantGuid, tagArray, gene = '') => {
return (dispatch, getState) => {
...
...
and the error while running jest is:
Actions must be plain objects. Use custom middleware for async actions.
Which makes an HTTP Request that may not work in the current case. How to fix this error? I need to test that the constructor was called, but later on will also need to see how the inner Components are rendered, so need to have mount there. I suppose I am doing something wrong in testing and not in the real code since the latter is working without any errors, warnings or issues.
You probably need to configure your mock store to work with redux-thunk. See: https://github.com/dmitry-zaets/redux-mock-store#asynchronous-actions
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk] // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares)
I am trying to test my mapDispatchToProps actions when an async function is dispatched. I almost tried every possible solution I found and nothing worked so far. I'm always getting the same error:
I'm getting this error:
TypeError: store.dispatch(...).then is not a function
I tried the solution included in redux-mock-store https://github.com/dmitry-zaets/redux-mock-store. I included my middlewares to my mockStore, but it didn't fix the issue.
I tried the solution proposed by Michael Peyper here Testing dispatched actions in Redux thunk with Jest.
We created a function to build the mockStore so I tried to create my mockStore directly within my test file instead, but they both returned the same error.
I can't put all the solutions I tried here because it would take me weeks, but it gives you an idea.
Here's the code for my test:
describe('Component async actions', () => {
const middlewares = [thunk, queryMiddleware];
const createMockStore = configureStore(middlewares);
const store = createMockStore();
afterEach(() => {
jest.clearAllMocks();
});
const someData = {};
const expectedActions = {
type: ADD_DATA,
payload: someData
};
it('should handle addData', () => {
return store.dispatch(actions.addData(someData)).then(() => {
expect(store.getActions()[0]).toEqual(expectedAction);
});
});
});
Here's my mapDispatchToProps:
function mapDispatchToProps(dispatch) {
return {
addData: data => dispatch(addData(data))
.then(({ status }) => {
dispatch(showNotification({ status }));
}),
};
};
I would like to at least be able to get to the expect part and fix this if there's any error in my test, but I can't get passed the dispatch().then
Again, here's the error I get each time: TypeError: store.dispatch(...).then is not a function
Thanks in advance!
I don't know if anyone will get this problem, but I found a solution.
First of all, I had to add my thunk middleware to my createStore from redux-mock-store.
import thunk from 'redux-thunk';
...
const createMockStore = createStore([thunk]);
Then I did a mock of my addData function like this:
import { addData } from 'path/to/addData';
...
jest.mock('path/to/addData');
and I added this code within my test:
addData.mockReturnValue(() =>
new Promise((resolve) => resolve({ status: 200 }));
));
It works!
I have a function (a Redux action) which calls 2 functions. I can't figure how how to test this with Jest:
This is my function:
import doSomething from 'redux/do-something';
import doSomethingElse from 'redux/do-something-else';
export default () => async dispatch => {
await dispatch(doSomething());
dispatch(doSomethingElse());
};
This is my test:
import doSomething from 'redux/do-something';
import doSomethingElse from 'redux/do-something-else';
import functionToTest from 'redux/function-to-test'
describe("functionToTest", ()=>{
jest.mock('redux/do-something');
jest.mock('redux/do-something-else');
const dispatch = jest.fn();
test('my test', ()=>{
functionToTest()(dispatch);
console.log(dispatch.mock.calls); // This returns an anonymous function
console.log(doSomething) // This returns undefined
})
})
It looks like you are wanting to mock the default export for do-something and do-something-else and test that they get dispatched by the code under test.
If that is the case then you can do it like this:
import functionToTest from 'redux/function-to-test'
jest.mock('redux/do-something', () =>
() => 'do something mock'
);
jest.mock('redux/do-something-else', () =>
() => 'do something else mock'
);
describe("functionToTest", () => {
test('my test', async () => { // <= async test function
const dispatch = jest.fn();
await functionToTest()(dispatch); // <= await the Promise
expect(dispatch.mock.calls[0][0]).toBe('do something mock'); // Success!
expect(dispatch.mock.calls[1][0]).toBe('do something else mock'); // Success!
});
});
Details
You can pass a module factory function as the second paramter to jest.mock and Jest will use the result of calling the function as what it gives back when the module is imported during the test.
jest.mock calls get hoisted by babel-jest and run before everything else in the code file. The hoisting doesn't work right when jest.mock is defined in a test function so the jest.mock calls should be moved to the top level scope of the test file.
The function under test is async so use an async test function and await the Promise to make sure it has completed before asserting.
My question is why middlewareAPI can't use :
const middlewareAPI = {
getState: store.getState,
dispatch: dispatch
}
to replace the definition in the source code as below:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args) // why not just use `dispatch: dispatch`
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
Anyone can tell me the difference ? Thanks.
It's a somewhat complicated combination of JS variable scoping/hosting, and needing to ensure that the passed-in dispatch method actually points back to the start of the middleware chain.
Please see the newly-added (and not yet published) Redux FAQ entry on why applyMiddleware uses a closure for more details.