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);
}
})
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'm using Redux Thunk.
I got an async operation (updating message to db), and I want to wait for it to complete and then get the updated messages array from the db.
I tried:
const handleWriteMessage = async (e) => {
e.preventDefault()
await dispatch(
writeMessage({
sender: data.sender,
subject: data.subject,
receiver: data.receiver,
message: data.message,
created: date
})
)
dispatch(getMessages())
}
But it doesn't mind the await and runs getMessages() immediately when handleWriteMessage is called.
I tried to do it in the action itself after it's completed:
axios
.post('http://localhost:4000/api/messages/writeMessage', msg, config)
.then((res) => {
getMessages()
dispatch({
type: WRITE_MESSAGE_SUCCESS
})
})
But it's not working too.
What am I missing?
It seem that handleWriteMessage should not by async, it must return function that will accept dispatch and may execute async function, see redux-thunk docs.
See snippet below and its output.
var thunk = createThunkMiddleware();
var log = (state = [], action) => state.concat(action.message || action.type);
var store = Redux.createStore(log, [], Redux.applyMiddleware(thunk, logger));
(async() => {
store.dispatch(asyncAC('async message 1.a'))
.then(() => asyncAC('async message 1.b'))
.then(store.dispatch);
store.dispatch(syncAC('sync 1'));
await store.dispatch(asyncAC('await async message 2.a'));
store.dispatch(syncAC('sync 2'));
store.dispatch(asyncAC('await async message 3.a'))
.then(() => store.dispatch(asyncAC('then async message 3.b')));
})();
function syncAC(m) {
return {
type: 'log',
message: m
}
}
function asyncAC(m) {
return (dispatch) => {
return new Promise(resolve => setTimeout(resolve, 1000, syncAC(m)))
.then(dispatch);
}
}
// redux-thunk itself
function createThunkMiddleware(extraArgument) {
return function(_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function(next) {
return function(action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
// logger middleware
function logger({
getState
}) {
return next => action => {
console.log('will dispatch', action)
// Call the next dispatch method in the middleware chain.
const returnValue = next(action)
console.log('state after dispatch', getState())
// This will likely be the action itself, unless
// a middleware further in chain changed it.
return returnValue
}
}
<script src="https://unpkg.com/redux#4.0.5/dist/redux.js"></script>
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 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 want to pass in a boolean value as the 2nd argument to my actionCreator which would determine what my middleware dispatches, but how do I give my middleware access to this 2nd argument?
Do I have to dispatch an array or object instead of a promise?
export const fetchPokemon = function (pokemonName, booleanValue) {
return function (dispatch) {
dispatch({type: 'REQUESTING'})
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`
dispatch(fetch(requestURL))
}
}
Middleware
const fetchPromiseMiddleware = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
...
return response.json()
}).then(function (data) {
if booleanValue {
store.dispatch(receivePokemon(formatPokemonData(data)))
} else {
store.dispatch(fetchPokemonDescription(data.name))
}
})
}
it seems you have answered yourself, the action you dispatch should contain all the relevant data.
The simplest option seem to be to add a property (or properties) to your action, as a Promise is already an object.
export const fetchPokemon = function (pokemonName, booleanValue) {
return function (dispatch) {
dispatch({type: 'REQUESTING'})
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`
dispatch(Object.assign(fetch(requestURL), {
someNameForYourBooleanParameter: booleanValue
})
}
}
and
const fetchPromiseMiddleware = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
...
return response.json()
}).then(function (data) {
if (action.someNameForYourBooleanParameter) {
store.dispatch(receivePokemon(formatPokemonData(data)))
} else {
store.dispatch(fetchPokemonDescription(data.name))
}
})
}
If you want to continue this path, I'd recommend to put these values under a .payload property to prevent any collision with members of the Promise class
I'd take this approach further to avoid the multiple actions being dispatched for the same logical action:
export const fetchPokemon = function (pokemonName, booleanValue) {
return function (dispatch) {
const requestURL = `http://pokeapi.co/api/v2/pokemon/${pokemonName}/`;
dispatch({
type: 'REQUESTING',
promise: fetch(requestURL),
payload: {
someNameForYourBooleanParameter: booleanValue
}
})
}
}