Conditionally fetching data in epic - redux

Say I have the following epic:
const getPostsEpic = (action$, store) => {
return action$.ofType(actionTypes.REQUEST_POSTS)
.switchMap(action =>
ajax.getJSON(`api/posts?key=${action.key}`)
.map(response =>
receivePosts({type: RECEIVE_POSTS, posts: response})
).takeUntil(
action$.ofType(actionTypes.ABORT_GET_POSTS)
)
};
and say my reducer is something like
function reducer(
state = {
isFetching: false,
didInvalidate: true,
items: []
},
action
) {
switch (action.type) {
case INVALIDATE_POSTS:
return Object.assign({}, state, {
didInvalidate: true
})
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
})
default:
return state
}
}
I want to make sure that posts are only fetched if my state's didInvalidate === true, is there a good way to make this work with my epic? Could do something like this, but it's not that pretty IMO:
const getPostsEpic = (action$, store) => {
return action$.ofType(actionTypes.REQUEST_POSTS)
.switchMap(action => {
const state = store.getState();
if (state.didInvalidate) {
return ajax.getJSON(`api/posts?key=${action.key}`)
.map(response =>
receivePosts({type: RECEIVE_POSTS, posts: response})
).takeUntil(
action$.ofType(actionTypes.ABORT_GET_POSTS)
)
else {
return Observable.of({type: RECEIVE_POSTS, posts: state.items});
}
}
};
Btw, I'm using this with React. I'm sure this is a pretty common problem, so maybe there's a better way of handling this outside my epics?

You can use if for branching, like this:
const mockAjax = () => Promise.resolve({posts: [4, 5, 6, 7]});
const fetchPost = (action$) => Rx.Observable.fromPromise(mockAjax())
.map(({posts}) => ({type: RECEIVE_POSTS, posts}))
.takeUntil(action$.ofType(ABORT_GET_POSTS))
const defaultPosts = (action$, store) => Rx.Observable.of({type: RECEIVE_POSTS, posts: store.getState().items});
const getPostsEpic = (action$, store) =>
action$.ofType(USER_REQUEST)
.mergeMap(() => Rx.Observable.if(
() => store.getState().didInvalidate, // condition
fetchPost(action$), // if true
defaultPosts(action$, store) // if false
)
.do(x => console.log(x))
)
Check the demo in her: http://jsbin.com/jodaqopozo/edit?js,console,output
Clicking valid/invalid button and then click 'Post Request' will log different value.
Hope this helps.

Related

Redux state doesn't update immediately

I have super simple question
Why my redux state doesn't update immediately?
const { reducer, actions } = createSlice({
name: "professionals",
initialState: {
loading: false,
lastFetchList: undefined,
list: undefined,
professional: undefined,
filters: {
virtual: false
}
},
reducers: {
professionalsListRequested: (professionals, action) => {
if (action.payload.withLoading) professionals.loading = true;
},
professionalsListRequestFailed: (professionals, action) => {
professionals.loading = false;
},
professionalsListReceived: (professionals, action) => {
professionals.lastFetchList = Date.now();
professionals.list = action.payload.data.dataArr;
professionals.loading = false;
},
virtualUpdated: (categories, action) => {
categories.filters.virtual = action.payload;
}
},
});
export const { virtualUpdated } = actions;
export default reducer;
it is my slice.
and here is code of the component :
const dispatch = useDispatch();
const filters = useSelector((state) => state.professionals.filters);
const handlePressOnVirtual = async () => {
console.log("Before" , filters.virtual)
await dispatch(virtualUpdated(!filters.virtual));
console.log("after" , filters.virtual)
};
when handlePressOnVirtual function is called the both console.log(s) print previous value of the state.
When you are still in handlePressOnVirtual function, you are still in a closure, so all the references will still be your existing filters
So you would need to wait for another re-render for useSelector to invoke again then the new values will come.
One way to see the latest changes is to put your log inside a useEffect:
useEffect(() => {
console.log("after" , filters.virtual)
},[filters.virtual]);

NGRX EFFECTS Type 'Observable<unknown>' is not assignable to type 'EffectResult<Action>'

what do i do wrong?
fetchEmail$ = createEffect(() => this.actions$.pipe(
ofType(RDX_EMAIL_CONFIRM_FETCH),
switchMap(ac => axios.post(axiosInstance.post('/api/email-confirm/check-email', {
email: ac.payload.email
}).then(res => {
return {
type: RDX_EMAIL_CONFIRM_FETCH_SUCCESS,
};
}).catch(err => {
return {
type: RDX_EMAIL_CONFIRM_FETCH_ERROR
};
})))
))
would noah like to call some api and return an action based on that?
unfortunatley has the following error
Type 'Observable<unknown>' is not assignable to type 'EffectResult<Action>'.
Type 'Observable<unknown>' is not assignable to type 'Observable<Action>'.
Type 'unknown' is not assignable to type 'Action'
here's my reducer maby the problem occurs here
Is it the actions payload definition?
import { createAction, createReducer, on, props } from '#ngrx/store';
import { tassign } from 'tassign';
export const RDX_EMAIL_CONFIRM_FETCH = 'RDX_EMAIL_CONFIRM_FETCH';
export const RDX_EMAIL_CONFIRM_FETCH_SUCCESS = 'RDX_EMAIL_CONFIRM_FETCH_SUCCESS';
export const RDX_EMAIL_CONFIRM_FETCH_ERROR = 'RDX_EMAIL_CONFIRM_FETCH_ERROR';
export const rdxEmailConfirmFetch = createAction(
RDX_EMAIL_CONFIRM_FETCH,
props<{email: string}>()
);
export const rdxEmailConfirmFetchSuccess = createAction(RDX_EMAIL_CONFIRM_FETCH_SUCCESS);
export const rdxEmailConfirmFetchError = createAction(RDX_EMAIL_CONFIRM_FETCH_ERROR);
const initialState = {
isFetch: false
}
export const emailConfirmReducer = createReducer(
initialState,
on(rdxEmailConfirmFetch, (state) => tassign(state, {
isFetch: true
})),
on(rdxEmailConfirmFetchSuccess, (state) => tassign(state, {
isFetch: false
})),
on(rdxEmailConfirmFetchError, (state) => tassign(state, {
isFetch: false
}))
)
You have to use RxJS of operator to convert it to observable, just like below.
fetchEmail$ = createEffect(() => this.actions$.pipe(
ofType(RDX_EMAIL_CONFIRM_FETCH),
switchMap(ac => axios.post(axiosInstance.post('/api/email-confirm/check-email', {
email: ac.payload.email
}).then(res => {
return of({
type: RDX_EMAIL_CONFIRM_FETCH_SUCCESS,
});
}).catch(err => {
return of({
type: RDX_EMAIL_CONFIRM_FETCH_ERROR
});
})))
))

shouldn't I dispatch an action inside a .then statement?

I found a code on git which I'm trying to understand and in the code the guy have this function:
export function startAddTodo(text) {
return (dispatch, getState) => {
const UID = firebase.auth().currentUser.uid;
const todo = {
text,
isDone: false,
isStarred: false
};
const todoRef = firebaseRef.child(`todos/${UID}`).push(todo);
dispatch(addTodo({
id: todoRef.key,
...todo
}));
todoRef.then(snapshot => {
return;
}, error => {
Alert.alert(JSON.stringify(error.message));
});
};
}
Why shouldn't it be like
const todoRef = firebaseRef.child(`todos/${UID}`).push(todo);
todoRef.then(snapshot => {
dispatch(addTodo({
id: snapshot.key,
...todo
}));
})
I think this because the promise may be rejected, but in the first code he may get an error when trying to call todoRef.key inside the dispatch method.

Redux observable cancel next operator execution?

I am using redux-observable with redux for async actions. Inside epic's map operator i am doing some pre processing because its the central place.
My app calling same action from multiple container components with different values.
So basically i have to cancel my ajax request/next operator execution if deepEqual(oldAtts, newAtts) is true
code -
export default function getProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.debounceTime(500)
.map(function(action) {
let oldAtts = store.getState().catalog.filterAtts
let newAtts = Object.assign({}, oldAtts, action.atts)
if (deepEqual(oldAtts, newAtts)) {
// Don't do new ajax request
}
const searchString = queryString.stringify(newAtts, {
arrayFormat: 'bracket'
})
// Push new state
pushState(newAtts)
// Return new `action` object with new key `searchString` to call API
return Object.assign({}, action, {
searchString
})
})
.mergeMap(action =>
ajax.get(`/products?${action.searchString}`)
.map(response => doFetchProductsFulfilled(response))
.catch(error => Observable.of({
type: FETCH_PRODUCTS_FAILURE,
payload: error.xhr.response,
error: true
}))
.takeUntil(action$.ofType(FETCH_PRODUCTS_CANCEL))
);
}
Not sure whether its right way to do it from epic.
Thanks in advance.
You can do this:
export default function getProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.debounceTime(500)
.map(action => ({
oldAtts: store.getState().catalog.filterAtts,
newAtts: Object.assign({}, oldAtts, action.atts)
}))
.filter(({ oldAtts, newAtts }) => !deepEqual(oldAtts, newAtts))
.do(({ newAtts }) => pushState(newAtts))
.map(({ newAtts }) => queryString.stringify(newAtts, {
arrayFormat: 'bracket'
}))
.mergeMap(searchString => ...);
}
But most likely you do not need to save the atts to state to do the comparison:
export default function getProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.debounceTime(500)
.map(action => action.atts)
.distinctUntilChanged(deepEqual)
.map(atts => queryString.stringify(atts, { arrayFormat: 'bracket' }))
.mergeMap(searchString => ...);
}

Action creators handling axios get.request with state access for param

I'm trying to pass some value from a component to a action creators which is doing a get request with axios. I'm trying to follow this pattern from Dan Abramov :
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return (dispatch, getState) => {
const {items} = getState().otherReducer;
dispatch(anotherAction(items));
}
}
However I can't make it work. I think I have trouble on two level : my component and my action creator. Would be great to have some helps.
my component :
const timeR = ({
selectedTimeRange,
timeRange = [],
onTimeChange }) => {
return (
<div>
<div>
Filters:
<div>
Year:
<select
defaultValue={selectedTimeRange}
onChange={onTimeChange}>
<option value="all" >All</option>
{timeRange.map((y, i) =>
<option key={i} value={y}>{y}</option>
)}
</select>
</div>
</div>
</div>
);
}
function mapStateToProps(state) {
var range = ['30daysAgo', '15daysAgo', '7daysAgo'];
return {
selectedTimeRange: state.timeReducer.timerange[0],
timeRange: range
};
};
const mapDispachToProps = (dispatch) => {
return {
onTimeChange: (e) => {dispatch (onSetTimeRange(e.target.value));},
};
};
const TimeRange = connect(mapStateToProps, mapDispachToProps)(timeR);
export default TimeRange;
This component give me a dropdown menu. When selecting a timerange, for example '30daysAgo', it update my redux store state so I can access the value from my reducer.
Here is the action associated to my dropdown menu :
export function onSetTimeRange(timerange) {
return {
type: 'SET_TIME_RANGE',
timerange
}
}
and here is the action dealing with axios.get :
export const fetchgadata = () => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null
});
var VIEW_ID = "ga:80820965";
return axios.get("http://localhost:3000/gadata", {
params: {
id: VIEW_ID
}
}).then(response => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching: false,
data: response.data.rows.map( ([x, y]) => ({ x, y }) )
});
})
.catch(err => {
dispatch({
type: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
};
My question :
How do I bring these two actions together. At the end I would like to be able, when doing onChange on my drop-down menu, to call a action with the value selected from my menu as a param for my axios.get request.
I feel like I need to nest two actions creators. I've tried this but doesn't work ("fetchgadata" is read-only error in my terminal)
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export function onSetTimeRange() {
return (dispatch, getState) => {
const {VIEW_ID} = getState().timerange;
dispatch(fetchgadata = (VIEW_ID) => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null,
id:VIEW_ID,
});
});
return axios.get("http://localhost:3000/gadata", {
params: {
id: VIEW_ID
}
}).then(response => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching: false,
data: response.data.rows.map( ([x, y]) => ({ x, y }) )
});
})
.catch(err => {
dispatch({
ype: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
}
}
Edit:
reducers for API call :
const initialState = {data:null,isFetching: false,error:null};
export const gaData = (state = initialState, action)=>{
switch (action.type) {
case 'FETCH_DATA_REQUEST':
case 'FETCH_DATA_FAILURE':
return { ...state, isFetching: action.isFetching, error: action.error };
case 'FETCH_DATA_SUCCESS':
return Object.assign({}, state, {data: action.data, isFetching: action.isFetching,
error: null });
default:return state;
}
};
reducers for Drop-down :
const items = [{timerange: '30daysAgo'},{timerange: '15daysAgo'},{timerange: '7daysAgo'}]
const timeReducer = (state = {
timerange: items
}, action) => {
switch (action.type) {
case 'SET_TIME_RANGE':
console.log(state,action);
return {
...state,
timerange: action.timerange,
};
default:
return state;
}
}
I see a little typo in the catch of your axios.get request, it reads ype: FETCH_DATA_FAILURE. Otherwise, can you add in your reducer for me, I don't see it up there? If I understand correctly, you want one action to update two different pieces of state, in which case you would simply dispatch an action and add it to both reducers. Really it's best to just demonstrate:
//axios call
axios.get("some route", { some params } )
.then(response => {
dispatch({
type: UPDATE_TWO_THINGS,
payload: some_value
})
}) .... catch, etc
//reducer 1
import { UPDATE_TWO_THINGS } from 'types';
const INITIAL_STATE = { userInfo: '' };
export default function (state = INITIAL_STATE, action) {
switch(action.type) {
case UPDATE_TWO_THINGS:
return {...state, userInfo: payload };
}
return state;
}
//reducer 2
import { UPDATE_TWO_THINGS } from 'types';
const INITIAL_STATE = { businessInfo: '' };
export default function (state = INITIAL_STATE, action) {
switch(action.type) {
case UPDATE_TWO_THINGS:
return {...state, businessInfo: payload };
}
return state;
}
Hopefully this helps, but let me know if not, I'll do my best to get this working with you! Thanks for asking!

Resources