Backpressure implementation with redux-observable - reactjs

I'm trying to implement backpressure logic in my react application.
I find a nice post about this here and trying to add this to my app.
Now i have some code:
// epic.ts
import { ofType } from 'redux-observable';
import { mapTo, tap, delay, switchMap } from 'rxjs/operators';
import { createIteratorSubject } from './createIteratorSubject';
// fake generator which in real application is supposed to pull data from a server
function* generator() {
yield 1;
yield 2;
yield Promise.resolve(3);
yield Promise.resolve(4);
yield 5;
}
const iterator$ = createIteratorSubject(generator());
export function epic(action$: any): any {
return action$.pipe(
ofType('TAKE'),
switchMap(() => {
return iterator$
.pipe(
tap((value) => console.info('INCOMING VALUE', value)),
delay(1000), // Some hard calculations here
tap((value) => console.info('DONE PROCESSING VALUE', value))
)
.subscribe({
next: iterator$.push,
complete: () => {
console.info('DONE PROCESSING ALL VALUES');
},
});
}),
mapTo((value: number) => {
return { type: 'PUT', payload: value };
})
);
}
// createIteratorSubject.ts
import { BehaviorSubject } from 'rxjs';
export function createIteratorSubject(iterator: any) {
const iterator$ = new BehaviorSubject();
const pushNextValue = async ({ done, value }: any) => {
if (done && value === undefined) {
iterator$.complete();
} else {
iterator$.next(await value);
}
};
iterator$.push = (value: any) => {
return pushNextValue(iterator.next(value));
};
iterator$.push();
return iterator$;
}
The problem i'm faced with is i don't know how to dispatch result value to redux. And now i have following error.

You're returning a Subscription in your switchMap where an ObservableInput is expected. You can change your code like this to make it work:
export function epic(action$: any): any {
return action$.pipe(
ofType('TAKE'),
switchMap(() => {
return iterator$
.pipe(
tap((value) => console.info('INCOMING VALUE', value)),
delay(1000), // Some hard calculations here
tap((value) => console.info('DONE PROCESSING VALUE', value)),
tap({
next: iterator$.push,
complete: () => {
console.info('DONE PROCESSING ALL VALUES');
}
})
);
}),
map((value: number) => {
return { type: 'PUT', payload: value };
})
);
}

Related

Testing Optimistic update in react query

I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?
import { act, renderHook } from "#testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");
describe('Testing custom hooks of react query', () => {
it('Should add a new color', async () => {
axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
await act(() => {
result.current.mutate({ label: 'Grey' })
})
await waitFor(() => result.current.isSuccess);
})
})
export const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
}
});
export function createWrapper() {
const testQueryClient = createTestQueryClient();
return ({ children }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
export const useAddColorHook = (page) => {
const queryClient = useQueryClient()
return useMutation(addColor, {
// onSuccess: () => {
// queryClient.invalidateQueries(['colors', page])
// }
onMutate: async color => {
// newHero refers to the argument being passed to the mutate function
await queryClient.cancelQueries(['colors', page])
const previousHeroData = queryClient.getQueryData(['colors', page])
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
return { previousHeroData }
},
onSuccess: (response, variables, context) => {
queryClient.setQueryData(['colors', page], (oldQueryData) => {
console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
return {
...oldQueryData,
data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
}
})
},
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['colors', page], context.previousHeroData)
},
onSettled: () => {
queryClient.invalidateQueries(['colors', page])
}
})
}
The error that you are getting actually shows a bug in the way you've implemented the optimistic update:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.
This is what happens in your test because you start with a fresh query cache for every test.
An easy way out would be, since you have previousHeroData already:
const previousHeroData = queryClient.getQueryData(['colors', page])
if (previousHeroData) {
queryClient.setQueryData(['colors', page], {
...previousHeroData,
data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
}
}
If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return oldQueryData ? {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
} : undefined
})
This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.

Why my dispatch action doesn't work in use effect after request?

I need help. I don't understand why my dispatch action doesn't work. I've redux store currency list and current currency.
My reducer:
export const currencyReducer = (
state: typeState = initialState,
action: TypeActionCurrency
): typeState => {
switch (action.type) {
case types.CURRENCY_FILL_LIST:
return { ...state, list: action.payload }
case types.CURRENCY_SET_CURRENT:
return {
...state,
current:
state.list.find(currency => currency._id === action.payload) ||
({} as ICurrency),
}
default:
return state
}
}
My actions:
export const setCurrencyList = (currencies: ICurrency[]) => ({
type: types.CURRENCY_FILL_LIST,
payload: currencies,
})
export const setCurrentCurrency = (_id: string) => ({
type: types.CURRENCY_SET_CURRENT,
payload: _id,
})
My useEffect:
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
}
}
fetchCurrencies()
}
}, [])
I want make request when load page and write currency list to Redux store, if we don't have current currency we write default currency from data.
There is one more strange thing, my redux extension shows that the state has changed, but when I receive it via the log or useSelector, it is empty
enter image description here
Thanks!
I am not 100% sure but it should work.
const [loader, setLoader] = useState(false);
const list = useSelector(state => state.list)
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
setLoader(true)
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
} finally {
setLoader(false)
}
}
fetchCurrencies()
}
}, [])
useEffect(() => {
console.log(list);
}, [loader])

React Redux -possible to have a call back in dispatch function

Guys i am having some trouble or quite doubtful.
am having one component and one reducer.
Reducer.js
import {
ASSET_POPUP_GET_ENDPOINT,
} from 'apiCollection';
import { performGet } from 'services/rest-service/rest-service';
export const GET_ASSETS_LIST = 'stories/GET_ASSETS_LIST';
const initialState = {
imgGroup: [],
isLoading: false,
};
const modalUploadReducer = (state = initialState, action) => {
switch (action.type) {
case GET_ASSETS_LIST: {
return {
...state,
ImageJson:action.payload.imageGroup,
};
}
case GET_ASSETS_LIST_ERROR: {
return {
...state,
isLoading:false,
};
}
default:
return state;
}
};
export const getModalClose = () => (dispatch) => {
dispatch({ type: CLOSE_MODAL });
}
export const getListActionDispactcher = () => (dispatch) => {
performGet(`${ASSET_POPUP_GET_ENDPOINT}`)
.then((response) => {
const payload = response.data;
dispatch({ type: GET_ASSETS_LIST,
payload: {
...payload,
data: payload.results,
} });
})
.catch((err) => {
dispatch({ type: GET_ASSETS_LIST_ERROR, payload: err });
throw err;
});
};
export default modalUploadReducer;
and my component look like
it do have mapStateToProps and mapDispatchToProps
and one of the function
const mapDispatchToProps = dispatch => ({
getCollection: () => dispatch(getListActionDispactcher()),
});
addDocumentClick = () =>{
this.props.getAssetsCollection();
}
and is it possible to have some setState/manipulation of response after api response got from reducer in the component
based on the response i need to do some changes in addDocumentClick.
Means something like this
addDocumentClick = () =>{
this.props.getAssetsCollection().then(...based on response;
}
The correct way for solving this is setting a global loading flag and in your componentDidUpdate() method, checking for the value to determine that the action has just succeeded. You already seem to have the isLoading flag. Just set it when the action's dispatched, and unset it after it succeeds/fails. And in componentDidUpdate():
function componentDidUpdate(prevProps) {
if (prevProps.isLoading && !this.props.isLoading) {
// do something
}
}
Of course, you need to connect() your loading flag to your component to achieve this.
If all you care about is whether the assets list has changed, you can simply check for the change of that prop in componentDidUpdate():
function componentDidUpdate(prevProps) {
if (prevProps.ImageJson !== this.props.ImageJson) {
// do something
}
}
Another solution is sending a callback to your action dispatcher, which makes your code more tightly coupled and I don't recommend, but it does work too. So, when you connect(), you can:
getCollection: (onSuccess) => dispatch(getListActionDispactcher(onSuccess)),
In your action dispatcher:
export const getListActionDispactcher = (onSuccess) => (dispatch) => {
// ...once API finished/failed
onSuccess(someData);
}
Finally, in your component:
this.props.getCollection((result) => {
console.log('succeeded!', result);
// hide modal, etc..
}
You are using redux-thunk, and calling thunk will return a promise which will resolve in whatever you return in your thunk. Therefore, all you need to do is to add return value to getListActionDispactcher
export const getListActionDispactcher = () => (dispatch) => {
// return this promise
return performGet(`${ASSET_POPUP_GET_ENDPOINT}`)
.then((response) => {
const payload = response.data;
dispatch({ type: GET_ASSETS_LIST,
payload: {
...payload,
data: payload.results,
} });
// return whatever you want from promise
return payload
})
.catch((err) => {
dispatch({ type: GET_ASSETS_LIST_ERROR, payload: err });
throw err;
});
};
.
addDocumentClick = () => {
this.props.getAssetsCollection().then(payload => console.log(payload))
}
You should, however, look for ways to avoid this pattern to have your components decoupled from actions as much as possible for the sake of modularity

Using setInterval to get seek position in a song in Redux Action Creators

I build a player using React.js and Howler.js and I use Redux/Redux-Thunk to dispatch the player state and controls to the entire app.
I want to get the seek position in "realtime" to know where the seek is in the song and maybe store the value in the future. Now I'm using setInterval to dispatch an other action that get the current position every 200ms. Because it dispatch an action it updates every components connected to the store every 200ms.
Is it a good practice to use setInterval in Action Creators ?
If I have to do in the UI, so in my Player component I have to catch events like Play Pause Prev and Next track to set or kill the interval.
How do you recommand me to do this ?
My action creator :
export const PLAYER_INITIALIZE = 'PLAYER_INITIALIZE'
export const initialize = (trackId) => {
return {
type: PLAYER_INITIALIZE,
}
}
export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
export const setTrack = (track) => {
return (dispatch, getState) => {
const { player } = getState();
let animationId = null;
if (player.audioObj && player.audioObj.state() != 'unloaded') {
dispatch({
type: PLAYER_UNLOAD,
});
}
dispatch({
type: PLAYER_SET_TRACK,
audioObj: new Howl({
src: API.API_STREAM_TRACK + track.id,
html5: true,
preload: true,
autoplay: true,
format: track.format,
volume: player.volume,
onplay: () => {
dispatch({
type: PLAYER_STARTED_PLAYBACK,
});
animationId = setInterval(() => {
dispatch({
type: PLAYER_GET_SEEK_POS,
});
}, 200);
dispatch({
type: PLAYER_STARTED_SEEK_TRACKING,
animationId: animationId,
});
},
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS,
});
},
onloaderror: (id, error) => {
dispatch({
type: PLAYER_LOAD_ERROR,
error: error,
});
},
onend: () => {
dispatch(nextTrack());
},
}),
trackMetadata: track,
duration: track.duration,
});
}
}
export const PLAYER_LOAD_SUCCESS = 'PLAYER_LOAD_SUCCESS'
export const loadSuccess = () => {
return {
type: PLAYER_LOAD_SUCCESS,
}
}
export const PLAYER_LOAD_ERROR = 'PLAYER_LOAD_ERROR'
export const loadError = () => {
return {
type: PLAYER_LOAD_ERROR,
}
}
export const PLAYER_PLAY_TRACK = 'PLAYER_PLAY_TRACK'
export const playTrack = () => {
return {
type: PLAYER_PLAY_TRACK,
}
}
export const PLAYER_STARTED_PLAYBACK = 'PLAYER_STARTED_PLAYBACK'
export const startedPlayback = () => {
return {
type: PLAYER_STARTED_PLAYBACK,
}
}
export const PLAYER_PAUSE_TRACK = 'PLAYER_PAUSE_TRACK'
export const pauseTrack = () => {
return {
type: PLAYER_PAUSE_TRACK,
}
}
export const PLAYER_NEXT_TRACK = 'PLAYER_NEXT_TRACK'
export const nextTrack = () => {
return (dispatch, getState) => {
const { playQueue, queuePos } = getState().player;
if (queuePos < playQueue.length - 1) {
dispatch (setTrack(playQueue[queuePos + 1]));
dispatch (setQueuePos(queuePos + 1));
} else {
dispatch (unload());
}
}
}
export const PLAYER_PREV_TRACK = 'PLAYER_PREV_TRACK'
export const prevTrack = () => {
return (dispatch, getState) => {
const { playQueue, queuePos } = getState().player;
if (queuePos > 0) {
dispatch (setTrack(playQueue[queuePos - 1]));
dispatch (setQueuePos(queuePos - 1));
} else {
dispatch (unload());
}
}
}
export const PLAYER_SET_SEEK_POS = 'PLAYER_SET_SEEK_POS'
export const setSeek = (pos) => {
return {
type: PLAYER_SET_SEEK_POS,
seekPos: pos
}
}
export const PLAYER_GET_SEEK_POS = 'PLAYER_GET_SEEK_POS'
export const getSeekPos = () => {
return {
type: PLAYER_GET_SEEK_POS,
}
}
export const PLAYER_STARTED_SEEK_TRACKING = 'PLAYER_STARTED_SEEK_TRACKING'
export const startedSeekTracking = (animationId) => {
return {
type: PLAYER_SET_VOLUME,
animationId: animationId,
}
}
export const PLAYER_SET_VOLUME = 'PLAYER_SET_VOLUME'
export const setVolume = (value) => {
return {
type: PLAYER_SET_VOLUME,
volume: value
}
}
export const PLAYER_SET_PLAY_QUEUE = 'PLAYER_SET_PLAY_QUEUE'
export const setPlayQueue = (queue) => {
return {
type: PLAYER_SET_PLAY_QUEUE,
playQueue: queue || {},
}
}
export const PLAYER_SET_QUEUE_POSITION = 'PLAYER_SET_QUEUE_POSITION'
export const setQueuePos = (pos) => {
return {
type: PLAYER_SET_QUEUE_POSITION,
queuePos: pos,
}
}
export const PLAYER_UNLOAD = 'PLAYER_UNLOAD'
export const unload = () => {
return {
type: PLAYER_UNLOAD,
}
}
Is it a good practice to use setInterval in Action Creators ?
No. Redux has large feedback loop and overhead. So this technique is unefficient since it triggers a lot if unnecesary operations and is not responsive enough.
In general Redux is good for state management based on "static" states. And audio playback on the other hand is a stream-like process. So my advice is to put "static info" (like song name) inside Redux. And stream-like operations should be executed directly from Howler's api.

Redux - how to call an action and wait until it is resolved

I'm using react native + redux + redux-thunk
I do not have much experience with redux and react native
I'm calling an action inside my component.
this.props.checkClient(cliente);
if(this.props.clienteIsValid){
...
}
and within that action there is a call to an api that takes a few seconds.
export const checkClient = (cliente) => {
return dispatch => {
axios.get(`${API_HOST}/api/checkclient`, header).then(response => {
dispatch({type: CHECK_CLIENT, payload: response.data }); //valid or invalid
}).catch((error) => { });
}
}
My question is how can I delay the return of the action until the api response is completed? I need the api response to know if the client is valid or invalid. That is, I need the action to be resolved and then verify that the client is valid or invalid.
You can return a promise from the action, so that the call becomes thenable:
// Action
export const checkClient = (cliente) => {
return dispatch => {
// Return the promise
return axios.get(...).then(res => {
...
// Return something
return true;
}).catch((error) => { });
}
}
class MyComponent extends React.Component {
// Example
componentDidMount() {
this.props.checkClient(cliente)
.then(result => {
// The checkClient call is now done!
console.log(`success: ${result}`);
// Do something
})
}
}
// Connect and bind the action creators
export default connect(null, { checkClient })(MyComponent);
This might be out of scope of the question, but if you like you can use async await instead of then to handle your promise:
async componentDidMount() {
try {
const result = await this.props.checkClient(cliente);
// The checkClient call is now done!
console.log(`success: ${result}`)
// Do something
} catch (err) {
...
}
}
This does the same thing.
I don't understand the problem, but maybe this could help
export const checkClient = (cliente) => {
return dispatch => {
dispatch({type: CHECK_CLIENT_PENDING });
axios.get(`${API_HOST}/api/checkclient`, header).then(response => {
dispatch({type: CHECK_CLIENT, payload: response.data }); //valid or invalid
}).catch((error) => { });
}
}
...
this.props.checkClient(cliente);
if(this.props.clienteIsPending){
...
}
if(this.props.clienteIsValid){
...
}
I have written a full code if there is still confusion. The promise should work for a sequence of asynchronous redux action calls
Actions
export const buyBread = (args) => {
return dispatch => {
return new Promise((resolve, reject) => {
dispatch({type: BUY_BREAD_LOADING });
// or any other dispatch event
// your long running function
dispatch({type: BUY_BREAD_SUCCESS, data: 'I bought the bread'});
// or any other dispatch event
// finish the promise event
resolve();
// or reject it
reject();
});
}
export const eatBread = (args) => {
return dispatch => {
return new Promise((resolve, reject) => {
dispatch({type: EAT_BREAD_LOADING });
// or any other dispatch event
// your long running function
dispatch({type: EAT_BREAD_SUCCESS, data: 'I ate the bread'});
// or any other dispatch event
// finish the promise event
resolve();
// or reject it
reject();
});
}
Reducer
const initialState = {}
export const actionReducer = (state = initialState, payload) => {
switch (payload.type) {
case BUY_BREAD_LOADING:
return { loading: true };
case BUY_BREAD_SUCCESS:
return { loading: false, data: payload.data };
case EAT_BREAD_LOADING:
return { loading: true };
case EAT_BREAD_SUCCESS:
return { loading: false, data: payload.data };
}
Component class
import React, {Component} from 'react';
class MyComponent extends Component {
render() {
return (
<div>
<button onClick={()=>{
this.props.buyBread().then(result =>
this.props.eatBread();
// to get some value in result pass argument in resolve() function
);
}}>I am hungry. Feed me</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
actionReducer: state.actionReducer,
});
const actionCreators = {
buyBread: buyBread,
eatBread: eatBread
};
export default connect(mapStateToProps, actionCreators)(MyComponent));

Resources