I am trying to understand how redux middleware works, during my experiment I have noticed that dispatching an action from redux middleware may result in an unexpected behavior.
I will try to explain the problem by simulating file upload as follow:
we have 3 actions:
const setProgress = (progress) => ({ type: SET_UPLOAD_PROGRESS, progress });
const setThumbnail = (thumbnail) => ({ type: SET_THUMBNAIL, thumbnail });
const calculateTotal = () => ({ type: CALCULATE_TOTAL });
Middleware to calculate total:
export const testMiddleware = (store) => (next) => (action) => {
if (action.type === 'CALCULATE_TOTAL') {
return next(action);
}
const result = next(action);
store.dispatch(calculateTotal());
return result;
};
Reducer:
const initialState = {
progress: 0,
total: 0,
thumbnail: ''
};
export function uploadReducer(state = initialState, action) {
switch (action.type) {
case SET_UPLOAD_PROGRESS:
state.progress = action.progress;
return { ...state };
case SET_THUMBNAIL:
state.thumbnail = action.thumbnail;
return { ...state };
case CALCULATE_TOTAL:
state.total += state.progress * 5;
return { ...state };
default:
return state;
}
}
here is the code for simulating file upload:
let cnt = 0;
// simulate upload progress
const setNewProgress = () => {
cnt += 2;
if (cnt > 5) return;
setTimeout(() => {
store.dispatch(setProgress(cnt * 2));
setNewProgress();
}, 1000);
};
setNewProgress();
// simulate thumbnail generating
setTimeout(() => {
store.dispatch(setThumbnail('blob:http://thumbnail.jpg'));
}, 2500);
Here is the sequence of events:
the first action works as intended and sets the progress value:
the problem starts from here; thumbnail suppose to be set by 'setThumbnail' but devtools shows that it has been set by 'calculateTotal', and every dispatch after that is mismatched:
What am I doing wrong here? is it by design? how can I dispatch an action in middleware without causing above problem?
This unexpected behavior may be cause by your uploadReducer not being pure, i.e. it is directly operating on your state (e.g. state.progress = action.progress;). Reducers should only return the new state and not modify existing state injected into your reducer by redux. hence, your reducer needs to look like this:
export function uploadReducer(state = initialState, action) {
switch (action.type) {
case SET_UPLOAD_PROGRESS:
return { ...state, progress: action.progress };
case SET_THUMBNAIL:
return { ...state, thumbnail: action.thumbnail };
case CALCULATE_TOTAL:
return { ...state, total: state.total + state.progress * 5 };
default:
return state;
}
}
how can I dispatch an action in middleware without causing above problem?
Your middleware looks fine (you are correctly preventing recursion and also returning the next() result (which is not needed in your example but still makes sense in a real application). Your actions look good as well (a style remark: You could wrap your action's payload in a payload property, which is a common convention).
Related
Say for example when a login function calls an API and it returns an error because of something like invalid credentials. I have noticed that it still goes through the fulfilled case in the extra reducers part. Should I add an if statement to check if response code is 200 or is there a way for the thunk to go through the rejected case?
extraReducers: builder => {
builder.addCase(login.pending, (state, action) => {
state.fetchingError = null;
state.fetchingUser = true;
});
builder.addCase(login.fulfilled, (state, {payload}) => {
console.log(payload, 'hello?');
state.user = payload.data.user;
});
builder.addCase(login.rejected, (state, action) => {
state.fetchingUser = false;
state.fetchingError = action.error;
});
},
You can use rejectWithValue in createAsyncThunk to customize the reject action.
It also takes an argument which will be "action.payload" in the reject action.
In createAsyncThunk:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
// Use `err.response.data` as `action.payload` for a `rejected` action,
// by explicitly returning it using the `rejectWithValue()` utility
return rejectWithValue(err.response.data)
}
}
)
https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors
I am making a table of data that has users. One of the details in each row is their online availability. To do this I need to call two APIs (one for users and one for online availability). The online availability call needs to be called continuously at setintervals to make it update live. I am very confused about how and where to do this. I have merged the two API's by matching their Id's, but I do not know how to constantly call second API without calling the first continuously, as it would use alot of memory. Also I dont know where to do this within redux. Do i do it in the action? reducer? and why? I want this API state to be stored in the store so I can access this merged API across all components of the APP.
// action files
export const getUsers = ()=> async dispatch =>{
const usersResponse = await axios.get(users);
const userData = usersResponse.data.result;
dispatch({
type: GET_USERS,
payload: userData
})
}
export const getAvailability = () => async dispatch =>{
const availabilityResponse = await axios.get(availability);
const availabilityData = availabilityResponse.data.result;
dispatch({
type: GET_AVAILABILITY,
payload: availabilityData
})
}
// reducers
initial state = {
getUsers: [],
getAvailability: []
}
export default function(state = initialState, action) {
switch (action.type) {
case GET_USERS:
return {
...state,
getUsers: action.payload
};
case GET_AVAILABILITY:
return {
...state,
getAvailability: action.payload
};
default:
return state;
}
}
//component
async componentDidMount() {
await this.props.getUsers();
await this.props.getAvailability();
let availabilityObject = {};
this.props.availability.map(availability => {
availabilityObject[availability.uuid] = availability;
});
if (typeof this.props.users != "undefined") {
this.props.users.map(user => {
user.availability = availabilityObject[user.uuid];
});
}
console.log(this.props.users)
//this doesn't show a merged Object of users and availability
}
const mapStateToProps = (state) => ({
users: state.getUsers,
status: state.getAvailability
});
I am trying to implement subscriptions with redux-logic middleware.
The idea is following: when data is fetched from server, to call callback for each subscriber passing fetched data as arguments.
// logic/subscriptions.js
const fetchLatestLogic = createLogic({
type: FETCH_LATEST_DATA,
latest: true,
process({getState, action}, dispatch, done) {
const {seriesType, nextUpdateTime} = action.payload;
const callbacks = getState()[seriesType][nextUpdateTime].callbacks
apiFetch(seriesType)
.then(data => {
callbacks.forEach(callback => callback(seriesType, data));
done()
})
}
})
const subscribeLogic = createLogic({
type: SUBSCRIPTIONS_SUBSCRIBE,
cancelType: SUBSCRIPTIONS_REMOVE,
process({getState, action, cancelled$}, dispatch) {
const {seriesType, nextUpdateTime, updateInterval, subscriberId, callback} = action.payload;
const interval = setInterval(() => {
dispatch(fetchLatestData(seriesType, nextUpdateTime))
}, updateInterval);
cancelled$.subscribe(() => {
clearInterval(interval)
})
}
})
// reducers/subscriptions.js
import update from 'immutability-helper';
update.extend('$autoArray', (value, object) => (object ? update(object, value) : update([], value)));
const initialState = {
'SERIESTYPE1': {}
'SERIESTYPE2': {}
}
// state modifications using 'immutable-helpers'
const serieAddSubscriberForTime = (seriesSubscriptions, time, subscriber) =>
update(seriesSubscriptions, {
[time]: {
$autoArray: {
$push: [subscriber]
}
}
});
// state modifications using 'immutable-helpers'
const serieRemoveSubscriberForTime = (seriesSubscriptions, subscriptionTime, subscriber) => {
const subscriptions = seriesSubscriptions[subscriptionTime].filter(s => s.subscriberId !== subscriber.subscriberId);
if (subscriptions.length === 0) {
return update(seriesSubscriptions, { $unset: [subscriptionTime] });
}
return { ...seriesSubscriptions, ...{ [subscriptionTime]: subscriptions }
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case SUBSCRIPTIONS_SUBSCRIBE: {
const { seriesType, nextUpdateTime, subscriber} = action.payload;
const newSubscriptionAdded = serieAddSubscriberForTime(state[seriesType], nextUpdateTime, subscriber);
const oldSubscriptionRemoved = serieRemoveSubscriberForTime(state[seriesType], nextUpdateTime, subscriber);
return update(state, { [seriesType]: { ...oldSubscriptionRemoved, ...newSubscriptionAdded } });
}
default:
return state;
}
}
How would it be possible to cancel running interval for given subscriber only? *(Without dispatching intervalID to reducer and saving it in state?)
Because by just dispatching action
cancelType: SUBSCRIPTIONS_REMOVE
will remove all intervals for all subscriptions with my current implementation.
UPDATE: actually there is much more smooth way to do the cancellation logic.
cancelled$
is an observable, and the RxJS .subscribe() accepts three functions as arguments:
[onNext] (Function): Function to invoke for each element in the observable sequence.
[onError] (Function): Function to invoke upon exceptional termination of the observable sequence.
[onCompleted] (Function): Function to invoke upon graceful termination of the observable sequence.
so the argument of onNext function is an emited value, and since in our case its the SUBSCRIPTIONS_REMOVE action, we can access its payload and do the cancellation depending on that payload:
cancelled$.subscribe(cancellAction => {
if (cancellAction.payload.subscriberId === subscriberId &&
cancellAction.payload.seriesType === seriesType) {
clearTimeout(runningTimeout);
}
})
I have an async action, which fetch data from REST API:
export const list = (top, skip) => dispatch => {
dispatch({ type: 'LIST.REQUEST' });
$.get(API_URL, { top: top, skip: skip })
.done((data, testStatus, jqXHR) => {
dispatch({ type: 'LIST.SUCCESS', data: data });
});
};
A sync action, which changes skip state:
export const setSkip = (skip) => {
return {
type: 'LIST.SET_SKIP',
skip: skip
};
};
Initial state for top = 10, skip = 0. In component:
class List extends Component {
componentDidMount() {
this.list();
}
nextPage() {
let top = this.props.list.top;
let skip = this.props.list.skip;
// After this
this.props.onSetSkip(skip + top);
// Here skip has previous value of 0.
this.list();
// Here skip has new value of 10.
}
list() {
this.props.List(this.props.list.top, this.props.list.skip);
}
render () {
return (
<div>
<table> ... </table>
<button onClick={this.nextPage.bind(this)}>Next</button>
</div>
);
}
}
When button Next at first time clicked, value of skip which uses async action not changed.
How I can to dispatch action after sync action?
If you are using redux thunk, you can easily combine them.
It's a middleware that lets action creators return a function instead of an action.
Your solution might have worked for you now if you don't need to chain the action creators and only need to run both of them.
this.props.onList(top, newSkip);
this.props.onSetSkip(newSkip);
If you need chaining(calling them in a synchronous manner) or waiting from the first dispatched action's data, this is what I'd recommend.
export function onList(data) {
return (dispatch) => {
dispatch(ONLIST_REQUEST());
return (AsyncAPICall)
.then((response) => {
dispatch(ONLIST_SUCCESS(response.data));
})
.catch((err) => {
console.log(err);
});
};
}
export function setSkip(data) {
return (dispatch) => {
dispatch(SETSKIP_REQUEST());
return (AsyncAPICall(data))
.then((response) => {
dispatch(SETSKIP_SUCCESS(response.data));
})
.catch((err) => {
console.log(err);
});
};
}
export function onListAndSetSkip(dataForOnList) {
return (dispatch) => {
dispatch(onList(dataForOnList)).then((dataAfterOnList) => {
dispatch(setSkip(dataAfterOnList));
});
};
}
Instead of dispatching an action after a sync action, can you just call the function from the reducer?
So it follows this flow:
Sync action call --> Reducer call ---> case function (reducer) ---> case function (reducer)
Instead of the usual flow which is probably this for you:
Sync action call --> Reducer call
Follow this guide to split the reducers up to see what case reducers are.
If the action you want to dispatch has side affects though then the correct way is to use Thunks and then you can dispatch an action after an action.
Example for Thunks:
export const setSkip = (skip) => {
return (dispatch, getState) => {
dispatch(someFunc());
//Do someFunc first then this action, use getState() for currentState if you want
return {
type: 'LIST.SET_SKIP',
skip: skip
};
}
};
also check this out redux-sequence-action
Thanks for the replies, but I made it this way:
let top = this.props.list.top;
let skip = this.props.list.skip;
let newSkip = skip + top;
this.props.onList(top, newSkip);
this.props.onSetSkip(newSkip);
First I calculate new skip and dispatch an async action with this new value. Then I dispatch a syns action, which updates skip in state.
dispatch({ type: 'LIST.SUCCESS', data: data, skip: The value you want after sync action });
I'm making an update request on my React Native app to my Firebase with redux in mind.
Here's my redux snippet
export function buyTicket(eventID) {
const { currentUser } = firebase.auth();
return (dispatch) => {
firebase.database().ref(`/Users/${currentUser.uid}/joinedEvent`).update({ [eventID]: true })
.then(() => dispatch({ type: BUY_TICKET_SUCCESS }))
.catch(() => dispatch({ type: BUY_TICKET_FAIL }));
};
};
When the buyTicket function gets called, only the then() method should be expected but both then() and catch() got called.
According to the Firebase docs, update() produces a promise but its optional.
Here's the error I'm getting
This is my reducer
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case PULL_EVENT_DATA:
return action.payload;
case PULL_TRENDING_DATA:
return action.payload;
case BUY_TICKET_SUCCESS:
return {
message: 'Yay, see you there!'
}
case BUY_TICKET_FAIL:
return {
message: 'shit'
}
default:
return state;
}
}
Perhaps the console log might gives a clue?
As Jan pointed out the part I was missing, I was able to relocate the error and make it right.
The error I was making is mutability. I did not consider the concept of mutability when making the app, hence the error.
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case PULL_EVENT_DATA:
return action.payload;
case PULL_TRENDING_DATA:
return action.payload;
case BUY_TICKET_SUCCESS:
return { ...state, message: action.payload}
case BUY_TICKET_FAIL:
return { ...state, message: action.payload}
default:
return state;
}
}
You may find more about mutability here