How to create custom middleware for async request and redux? - javascript

Help to understand what the problem is. When I write mapDispatchToProps like this:
const mapDispatchToProps = (dispatch: any) => {
return {
getPostByIdAction: (post: any) => dispatch ({type: GET_ID, payload: post})
}
};
everything is working fine. But when I try to dispatch the function in this way:
const mapDispatchToProps = (dispatch: any) => {
return {
getPostByIdAction: (post: any) => dispatch (getPostById (post))
}
};
I get an error: Actions must be plain objects. Use custom middleware for async actions.
What could have gone wrong?
my actions:
export const getPostById = async (id: any) => {
const myResponse = await fetch (`https://jsonplaceholder.typicode.com/posts/$ {id}`);
const myJson = await myResponse.json ();
const post = myJson.body
}
my reducer:
import {combineReducers} from 'redux'
import {pageReducer} from './page'
export const rootReducer = combineReducers ({
page: pageReducer
})
import {GET_ID} from '../actions/PageActions'
const initialState = {
post: "Click on article to read it"
}
export function pageReducer (state = initialState, action: any) {
switch (action.type) {
case GET_ID:
return {... state, post: action.payload};
default:
return state
}
}

export const getPostById = async (id: any) => {
const myResponse = await fetch (`https://jsonplaceholder.typicode.com/posts/$ {id}`);
const myJson = await myResponse.json ();
const post = myJson.body
}
you are not returning anything. you should return an object here to pass it to dispatch. dispatch needs an object and that object has to have "action" property. you can add more properties but "action" property is a must.

The main problem with redux is that.., it is synchronous.
To handle async operations, we use middlewares. There are many libraries available to handle async operations.
But, if you want to create one, lets create a custom middleware for async operations
I've defined some types for type safety (since we're doing it in typescript).
Here we added another field 'api' for our convenience to differentiate the async operations with non-async ones
import { Middleware, Dispatch, MiddlewareAPI } from 'redux'
export interface DispatchType {
type: string
payload?: any
meta?: DispatchMeta
}
export interface DispatchMeta {
async: boolean
api: {
url: string
onComplete: string
// extra information you want for the request can be passes here:
// eg
// params, method, data
}
}
The object type that we dispatch looks something like this:
dispatch({type: 'TYPE_OF_DISPATCH', payload: 'any type of payload'})
Now lets create the Middleware to intercept those actions(object) that has meta field with async set to 'true'.
// Middleware to intercept those actions(object) that has meta field with async set to true;
export const asyncMiddleware: Middleware = ({ getState }: MiddlewareAPI) => (
next: Dispatch
) => async (action: DispatchType) => {
// Call the next dispatch method in the middleware chain.
next(action);
if (action.meta && action.meta.async && action.meta.api) {
const res = await fetch(action.meta.api.url);
const json = await res.json();
const post = json.body;
next({
type: action.meta.api.onComplete,
data: post
})
}
}
Here our middleware intercepts the action with async flag 'true' and then once completed, its dispatches the 'onComplete' action sent to the meta.
To use this middleware, Your dispatch action should look something like this
dispatch({
type: 'FETCH',
meta: {
async: true,
api: {
url: 'https://jsonplaceholder.typicode.com/posts/1',
onComplete: 'FETCHED_POST'
}
}
})
Here you may create a wrapper function which dispatches the above action
const getpost = (id: string) =>
dispatch({
type: 'FETCH',
meta: {
async: true,
api: {
url: `https://jsonplaceholder.typicode.com/posts/${id}`,
onComplete: 'FETCHED_POST'
}
}
})
Lastly, don't forget to apply the middleware to your redux store.
const store = createStore(
RootReducer,
applyMiddleware(asyncMiddleware)
)

Related

Is it wrong to use an action's payload inside a component with react-redux?

I'd like to keep track of API requests that I make with react-redux. To do this I'd like to generate a request Id inside the action and pass that along to middleware and reducers through the payload. Then when I'm dispatching the action from my component I can capture the request Id and use it for updating the component as the request progresses.
Here's some example code
State
export interface State {
[requestId: number]: Request;
}
export interface Request {
status: string;
error?: string;
}
Action
export function createRequest(): Action {
return {
type: "request",
payload: {
requestId: Math.random () // Make a random Id here
}
};
}
Reducer
export function createRequestReducer(state: State): State {
return {
...state,
...{ state.payload.requestId: { status: "pending" } }
};
}
Component
interface props {
getRequestById: (id: number) => Request;
createRequest: () => number;
}
const component = (props: testProps): JSX.Element => {
const getRequestById = props.getRequestById;
const [requestId, setRequestId] = useState(null);
const [request, setRequest] = useState(null);
useEffect(() => {
if (requestId !== null) {
setRequest(getRequestById(requestId));
}
}, [requestId]);
return <div>The request status is {(request && request.status) || "Not started"}</div>;
}
function mapStateToProps(state: State) {
return {
getRequestById: (requestId: number): Request => {
getRequestById(state, requestId)
}
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
createRequest: (): number => {
const action = createRequest();
dispatch(action);
return action.payload.requestId;
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(component);
I expect this will work but it may be a massive anti pattern. Is this not advised and, if so, is there an alternative?
I think your approach works technically totally fine. Only "logically" it might make sense to make a some changes:
Yes, the "action" is something that is supposed to be sent to the reducer (and not used anywhere else, although there is technically no problem with that).
But what you can do:
1. separate action and values
Inside the action creator function, you can do whatever you want.
So you can create and use the action and the requestId seperately.
This is technically exact the same as what you did, but logically separated.
E.g.:
function createRequest(){
const requestId = createUniqueId();
const action = { type: "request", payload: { requestId: requestId } };
return {
requestId: requestId, // <-- request id independent of the action
action: action, // <-- action independent of the request id
};
}
function mapDispatchToProps( dispatch: Dispatch ){
return {
createRequest: (): number => {
const { requestId, action } = createRequest();
dispatch( action ); // <-- action independent of the request id
return requestId; // <-- request id independent of the action
}
};
}
2. "action dispatchers"
I (and apparently others as well) like to use what I call "action dispatchers".
This is an extra step and more code, but I think when you got used to this concept, it eliminates any doubts where code like that has to be put.
E.g.:
// Create the action, and nothing else:
const createRequestActionCreator = function( requestId ){
return { type: "request", payload: { requestId: requestId } };
};
// Preper some data needed to create the action:
const createRequestActionDispatcher = function( dispatch ){
return function(){
const requestId = createUniqueId();
dispatch( createRequestActionCreator( requestId ) );
return requestId;
};
};
//
function mapDispatchToProps( dispatch: Dispatch ) {
return {
createRequest: (): number => {
const requestId = createRequestActionDispatcher( dispatch )();
return requestId;
}
};
}
2.a
Additionally you could pass such an "action dispatcher" directly as a prop, if you want.
In this case it basically replaces your function in mapDispatchToProps, but is reusable, e.g.:
function mapDispatchToProps( dispatch: Dispatch ) {
return {
createRequest: createRequestActionDispatcher( dispatch ),
};
}
2.b
Some people prefer to use a fat-arrow-function here, which I find more confusing, not less, but it looks cleaner as soon as you got used to that pattern:
const createRequestActionDispatcher = (dispatch: Dispatch) => (maybeSomeValue: MyType) => {
const requestId = createUniqueId();
dispatch( createRequestActionCreator( requestId ) );
return requestId;
};
Remark:
I generally prefer to be consistent, for which I should always (or never) use these "action dispatchers",
but I found that most of the time I don't need one, but sometimes I find them very useful.
So I'm actually using dispatch( myAction ) in some places and myActionDispatcher(value)(dispatch) in others.
I don't like that, but it works well, and I don't have a better idea.

Async does wait for data to be returned in a redux-thunk function

I've being trying populate my redux store with data that comes from my mongo-db realm database.
Whenever I run the function below it will execute fine but the problem is data will be delayed and ends up not reaching my redux store.
My thunk function:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
try {
return await Realm.open(config).then(async (projectRealm) => {
let syncItems = await projectRealm.objects("Item");
await syncItems.addListener((x, changes) => {
x.map(async (b) => {
console.log(b);
return b;
});
});
});
} catch (error) {
console.log(error);
throw error;
}
}
);
and my redux reducer:
extraReducers: (builder) => {
builder.addCase(addItemsTest.fulfilled, (state, { payload }: any) => {
try {
console.log("from add Items");
console.log(payload);
state.push(payload);
} catch (error) {
console.log(error)
}
});
}
Expected Results:
My redux store should have these data once addItemsTest return something:
[{
itemCode: 1,
itemDescription: 'Soccer Ball',
itemPrice: '35',
partition: 'partitionValue',
},
{
itemCode: 2,
itemDescription: 'Base Ball',
itemPrice: '60',
partition: 'partitionValue',
}
]
Actual Results:
Mixed Syntaxes
You are combining await/async and Promise.then() syntax in a very confusing way. It is not an error to mix the two syntaxes, but I do not recommend it. Stick to just await/async
Void Return
Your action actually does not return any value right now because your inner then function doesn't return anything. The only return is inside of the then is in the x.map callback. await syncItems is the returned value for the mapper, not for your function.
Right now, here's what your thunk does:
open a connection
get items from realm
add a listener to those items which logs the changes
returns a Promise which resolves to void
Solution
I believe what you want is this:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
try {
const projectRealm = await Realm.open(config);
const syncItems = await projectRealm.objects("Item");
console.log(syncItems);
return syncItems;
} catch (error) {
console.log(error);
throw error;
}
}
);
Without the logging, it can be simplified to:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
const projectRealm = await Realm.open(config);
return await projectRealm.objects("Item");
}
);
You don't need to catch errors because the createAsyncThunk will handle errors by dispatching an error action.
Edit: Listening To Changes
It seems that your intention is to sync your redux store with changes in your Realm collection. So you want to add a listener to the collection that calls dispatch with some action to process the changes.
Here I am assuming that this action takes an array with all of the items in your collection. Something like this:
const processItems = createAction("processItems", (items: Item[]) => ({
payload: items
}));
Replacing the entire array in your state is the easiest approach. It will lead to some unnecessary re-renders when you replace item objects with identical versions, but that's not a big deal.
Alternatively, you could pass specific properties of the changes such as insertions and handle them in your reducer on a case-by-case basis.
In order to add a listener that dispatches processItems, we need access to two variables: the realm config and the redux dispatch. You can do this in your component or by calling an "init" action. I don't think there's really much difference. You could do something in your reducer in response to the "init" action if you wanted.
Here's a function to add the listener. The Realm.Results object is "array-like" but not exactly an array so we use [...x] to cast it to an array.
FYI this function may throw errors. This is good if using in createAsyncThunk, but in a component we would want to catch those errors.
const loadCollection = async (config: Realm.Configuration, dispatch: Dispatch): Promise<void> => {
const projectRealm = await Realm.open(config);
const collection = await projectRealm.objects<Item>("Item");
collection.addListener((x, changes) => {
dispatch(processItems([...x]));
});
}
Adding the listener through an intermediate addListener action creator:
export const addListener = createAsyncThunk(
"init",
async (config: Realm.Configuration, { dispatch }) => {
return await loadCollection(config, dispatch);
}
);
// is config a prop or an imported global variable?
const InitComponent = ({config}: {config: Realm.Configuration}) => {
const dispatch = useDispatch();
useEffect( () => {
dispatch(addListener(config));
}, [config, dispatch]);
/* ... */
}
Adding the listener directly:
const EffectComponent = ({config}: {config: Realm.Configuration}) => {
const dispatch = useDispatch();
useEffect( () => {
// async action in a useEffect need to be defined and then called
const addListener = async () => {
try {
loadCollection(config, dispatch);
} catch (e) {
console.error(e);
}
}
addListener();
}, [config, dispatch]);
/* ... */
}

How can I pass state to action to fetch API?

so im new in redux and I need bit of help with my homework. I have a drop down with couple of choices and the choice that user select needs to be passed to state (already have this working and state is updating when user select something new) and then to action that can fetch data with '/stats/${userChoice}'. But i have no idea how to do this at all.
actions/index.js:
export const fetchAuthorsStats = () => async dispatch => {
const response = await myAPI.get(`/stats/${userChoice}`);
dispatch({ type: 'FETCH_AUTHORS_STATS', payload: response.data })
};
components/Dropdown.js:
onAuthorSelect = (e) => {
this.setState({selectAuthor: e.target.value})
};
.
.
.
const mapStateToProps = state => {
return {
authors: state.authors,
selectAuthor: state.selectAuthor,
authorsStats: state.authorsStats
}
};
export default connect(mapStateToProps, { fetchAuthors, selectAuthor, fetchAuthorsStats })(Dropdown)
under "selectAuthor" I have my state that I need to pass to this action API
You already map dispatch to fetchAuthorsStats thunk in your component so that means you can just use it in onAuthorSelect (or anywhere else you need - like on form submit) and pass it a parameter with the selectedAuthor.
// Added a userChoice param here:
export const fetchAuthorsStats = (userChoice) => async dispatch => {
const response = await myAPI.get(`/stats/${userChoice}`);
dispatch({ type: 'FETCH_AUTHORS_STATS', payload: response.data })
};
onAuthorSelect = (e) => {
this.setState({selectAuthor: e.target.value})
this.props.fetchAuthorsStats(e.target.value);
};
You can achieve this by calling the API directly with the event target value :
/// first you update your API call to receive the selected author
export const fetchAuthorsStats = (userChoice) => async dispatch => {
const response = await myAPI.get(`/stats/${userChoice}`);
dispatch({ type: 'FETCH_AUTHORS_STATS', payload: response.data })
};
//then you update your handler function
onAuthorSelect = (e) =>{
this.props.fetchAuthorsStats(e.target.value)
}
if you wish to still save it on the react state you can do the setState first and then the API call with (this.state.selectedAuthor) instead of (e.target.value)

Action must be plain object. Use custom middleware

What would be the problem?
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
Configure Store:
export default configureStore = () => {
let store = compose(applyMiddleware(ReduxThunk))(createStore)(reducers);
return store;
}
Action
export const menulist = async ({ navigation }) => {
return async dispatch => {
try {
dispatch({ type: types.MENULIST_REQUEST_START })
let response = await menuListByCategories();
dispatch({ type: types.MENULIST_SUCCESS })
} catch (error) {
dispatch({ type: types.MENULIST_FAILED })
}
}
}
You are using it the wrong way,
in Redux every action must return an object, and this is a must!
so, your dispatch, which is a function, should be called this way.
Besides you only need to declare async the function which returns dispatch. The async keyword determines that the following function will return a promise. As your first function (menulist) is returning the promise returned by the second function (dispatch one) you don't have to specify it.
export const menulist = ({ navigation }) => async (dispatch) => {
try {
dispatch({ type: types.MENULIST_REQUEST_START })
let response = await menuListByCategories();
dispatch({ type: types.MENULIST_SUCCESS })
} catch (error) {
dispatch({ type: types.MENULIST_FAILED })
}
}
}

How to unit test this Redux thunk?

So I have this Redux action creator that is using redux thunk middleware:
accountDetailsActions.js:
export function updateProduct(product) {
return (dispatch, getState) => {
const { accountDetails } = getState();
dispatch({
type: types.UPDATE_PRODUCT,
stateOfResidence: accountDetails.stateOfResidence,
product,
});
};
}
How do I test it? I'm using the chai package for testing. I have found some resources online, but am unsure of how to proceed. Here is my test so far:
accountDetailsReducer.test.js:
describe('types.UPDATE_PRODUCT', () => {
it('should update product when passed a product object', () => {
//arrange
const initialState = {
product: {}
};
const product = {
id: 1,
accountTypeId: 1,
officeRangeId: 1,
additionalInfo: "",
enabled: true
};
const action = actions.updateProduct(product);
const store = mockStore({courses: []}, action);
store.dispatch(action);
//this is as far as I've gotten - how can I populate my newState variable in order to test the `product` field after running the thunk?
//act
const newState = accountDetailsReducer(initialState, action);
//assert
expect(newState.product).to.be.an('object');
expect(newState.product).to.equal(product);
});
});
My thunk doesn't do any asynchronous actions. Any advice?
How to Unit Test Redux Thunks
The whole point of a thunk action creator is to dispatch asynchronous actions in the future. When using redux-thunk a good approach is to model the async flow of beginning and end resulting in success or an error with three actions.
Although this example uses Mocha and Chai for testing you could quite as easily use any assertion library or testing framework.
Modelling the async process with multiple actions managed by our main thunk action creator
Let us assume for the sake of this example that you want to perform an asynchronous operation that updates a product and want to know three crucial things.
When the async operation begins
When the async operation finishes
Whether the async operation succeeded or failed
Okay so time to model our redux actions based on these stages of the operation's lifecycle. Remember the same applies to all async operations so this would commonly be applied to http requests to fetch data from an api.
We can write our actions like so.
accountDetailsActions.js:
export function updateProductStarted (product) {
return {
type: 'UPDATE_PRODUCT_STARTED',
product,
stateOfResidence
}
}
export function updateProductSuccessful (product, stateOfResidence, timeTaken) {
return {
type: 'PRODUCT_UPDATE_SUCCESSFUL',
product,
stateOfResidence
timeTaken
}
}
export function updateProductFailure (product, err) {
return {
product,
stateOfResidence,
err
}
}
// our thunk action creator which dispatches the actions above asynchronously
export function updateProduct(product) {
return dispatch => {
const { accountDetails } = getState()
const stateOfResidence = accountDetails.stateOfResidence
// dispatch action as the async process has begun
dispatch(updateProductStarted(product, stateOfResidence))
return updateUser()
.then(timeTaken => {
dispatch(updateProductSuccessful(product, stateOfResidence, timeTaken))
// Yay! dispatch action because it worked
}
})
.catch(error => {
// if our updateUser function ever rejected - currently never does -
// oh no! dispatch action because of error
dispatch(updateProductFailure(product, error))
})
}
}
Note the busy looking action at the bottom. That is our thunk action creator. Since it returns a function it is a special action that is intercepted by redux-thunk middleware. That thunk action creator can dispatch the other action creators at a point in the future. Pretty smart.
Now we have written the actions to model an asynchronous process which is a user update. Let's say that this process is a function call that returns a promise as would be the most common approach today for dealing with async processes.
Define logic for the actual async operation that we are modelling with redux actions
For this example we will just create a generic function that returns a promise. Replace this with the actual function that updates users or does the async logic. Ensure that the function returns a promise.
We will use the function defined below in order to create a working self-contained example. To get a working example just throw this function in your actions file so it is in the scope of your thunk action creator.
// This is only an example to create asynchronism and record time taken
function updateUser(){
return new Promise( // Returns a promise will be fulfilled after a random interval
function(resolve, reject) {
window.setTimeout(
function() {
// We fulfill the promise with the time taken to fulfill
resolve(thisPromiseCount);
}, Math.random() * 2000 + 1000);
}
)
})
Our test file
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import chai from 'chai' // You can use any testing library
let expect = chai.expect;
import { updateProduct } from './accountDetailsActions.js'
const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)
describe('Test thunk action creator', () => {
it('expected actions should be dispatched on successful request', () => {
const store = mockStore({})
const expectedActions = [
'updateProductStarted',
'updateProductSuccessful'
]
return store.dispatch(fetchSomething())
.then(() => {
const actualActions = store.getActions().map(action => action.type)
expect(actualActions).to.eql(expectedActions)
})
})
it('expected actions should be dispatched on failed request', () => {
const store = mockStore({})
const expectedActions = [
'updateProductStarted',
'updateProductFailure'
]
return store.dispatch(fetchSomething())
.then(() => {
const actualActions = store.getActions().map(action => action.type)
expect(actualActions).to.eql(expectedActions)
})
})
})
Have a look at Recipe: Writing Tests from the official documentation. Also, what are you testing, the action creator or the reducer?
Action Creator Test Example
describe('types.UPDATE_PRODUCT', () => {
it('should update product when passed a product object', () => {
const store = mockStore({courses: []});
const expectedActions = [
/ * your expected actions */
];
return store.dispatch(actions.updateProduct(product))
.then(() => {
expect(store.getActions()).to.eql(expectedActions);
});
});
});
Reducer Test Example
Your reducer should be a pure function, so you can test it in isolation outside of the store environment.
const yourReducer = require('../reducers/your-reducer');
describe('reducer test', () => {
it('should do things', () => {
const initialState = {
product: {}
};
const action = {
type: types.UPDATE_PRODUCT,
stateOfResidence: // whatever values you want to test with,
product: {
id: 1,
accountTypeId: 1,
officeRangeId: 1,
additionalInfo: "",
enabled: true
}
}
const nextState = yourReducer(initialState, action);
expect(nextState).to.be.eql({ /* ... */ });
});
});
export const someAsyncAction = (param) => (dispatch, getState) => {
const { mock } = getState();
dispatch({
type: 'SOME_TYPE',
mock: mock + param,
})
}
it('should test someAsyncAction', () => {
const param = ' something';
const dispatch = jest.fn().mockImplementation();
const getState = () => ({
mock: 'mock value',
});
const expectedAction = {
type: 'SOME_TYPE',
mock: 'mock value something'
};
const callback = someAsyncAction(param);
expect(typeof callback).toBe('function');
callback.call(this, dispatch, getState);
expect(dispatch.mock.calls[0]).toEqual([expectedAction])
});

Categories