I have a problem with redux-observables. In my situation one epic wait for ending of another epic. The second epic can make a request or return data from the cache.
When second makes the request all work as expected, but when it returns cache the first one doesn't continue.
const { Observable } = Rx;
const FETCH_USER = 'FETCH_USER';
const FETCH_USER_FULFILLED = 'FETCH_USER_FULFILLED';
const FETCH_USER2 = 'FETCH_USER2';
const FETCH_USER_FULFILLED2 = 'FETCH_USER_FULFILLED2';
const FETCH_USER_REJECTED = 'FETCH_USER_REJECTED';
const FETCH_USER_CANCELLED = 'FETCH_USER_CANCELLED';
const fetchUser = id => ({ type: FETCH_USER, payload: id });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
const fetchUser2 = id => ({ type: FETCH_USER2, payload: id });
const fetchUserFulfilled2 = payload => ({ type: FETCH_USER_FULFILLED2, payload });
const cancelFetchUser = () => ({ type: FETCH_USER_CANCELLED });
let isFetchced = false;
const fakeAjax = url =>
Observable.of({
id: url.substring(url.lastIndexOf('/') + 1),
firstName: 'Bilbo',
lastName: 'Baggins'
}).delay(1000);
const fakeAjax2 = url =>
Observable.of({
id: url.substring(url.lastIndexOf('/2') + 1),
firstName: 'Bilbo2',
lastName: 'Baggins2'
}).delay(1000);
const fetchUserEpic = (action$, store) =>
action$.ofType(FETCH_USER)
.mergeMap(action => {
const observable = isFetchced ? Observable.of({
id: 2,
firstName: 'Bilbo',
lastName: 'Baggins'
}) : fakeAjax(`/api/users/${action.payload}`);
isFetchced = true;
console.log(action);
return observable
.map(response => fetchUserFulfilled(response))
.takeUntil(action$.ofType(FETCH_USER_CANCELLED))
});
const fetchUserEpic2 = action$ =>
action$.ofType(FETCH_USER2)
.switchMap(() => action$.ofType(FETCH_USER_FULFILLED)
.take(1)
.mergeMap(() => {
console.log("First epic");
return fakeAjax2(`/api/users/${1}`)
.map(response => fetchUserFulfilled2(response))
}).startWith(fetchUser('redux-observable')));
const users = (state = {}, action) => {
switch (action.type) {
case FETCH_USER_FULFILLED:
return {
...state,
[action.payload.id]: action.payload
};
default:
return state;
}
};
const isFetchingUser = (state = false, action) => {
switch (action.type) {
case FETCH_USER:
return true;
case FETCH_USER_FULFILLED:
case FETCH_USER_CANCELLED:
return false;
default:
return state;
}
};
Here is emulation https://jsbin.com/qitutixuqu/1/edit?html,css,js,console,output. After clicking on the button "Fetch user info" in the console you can see "First epic", after the second click on the button there is no message in console. If you add delay to
Observable.of({
id: 2,
firstName: 'Bilbo',
lastName: 'Baggins'
}).delay(10)
it starts work as expected.
Short answer: The first click is asynchronous by returning a delay of 1000 ms in fetchUserEpic. The second click is a fully synchronous execution of fetchUserEpic which results in the inner actions$.ofType(FETCH_USER_FULFILLED) missing the action in fetchUserEpic2.
Explanation:
Tracing fetchUserEpic in the first click we get this:
fetchUserEpic src: FETCH_USER2
fetchUserEpic2 src: FETCH_USER2
fetchUserEpic2 in: FETCH_USER2
fetchUserEpic2 out: FETCH_USER
fetchUserEpic src: FETCH_USER
fetchUserEpic in: FETCH_USER
fetchUserEpic2 src: FETCH_USER <- Notice location
fetchUserEpic out: FETCH_USER_FULFILLED
fetchUserEpic src: FETCH_USER_FULFILLED
fetchUserEpic2 src: FETCH_USER_FULFILLED
fetchUserEpic2-inner src: FETCH_USER_FULFILLED <- now subscribed
fetchUserEpic2-inner in: FETCH_USER_FULFILLED
First epic
fetchUserEpic2 out: FETCH_USER_FULFILLED2
fetchUserEpic src: FETCH_USER_FULFILLED2
fetchUserEpic2 src: FETCH_USER_FULFILLED2
Tracing the second time we get:
fetchUserEpic src: FETCH_USER2
fetchUserEpic2 src: FETCH_USER2
fetchUserEpic2 in: FETCH_USER2
fetchUserEpic2 out: FETCH_USER
fetchUserEpic src: FETCH_USER
fetchUserEpic in: FETCH_USER
fetchUserEpic out: FETCH_USER_FULFILLED
fetchUserEpic src: FETCH_USER_FULFILLED
fetchUserEpic2 src: FETCH_USER_FULFILLED
fetchUserEpic2 src: FETCH_USER <- Notice location
Since fetchUserEpic2 subscribes to to actions$ in the switchMap statement, it does not receive actions that were already dispatched. redux-observable uses a regular Subject, not a ReplaySubject or similar so if the action is dispatched before the subscription, that actions$ subscription will miss the action. For this reason you need to be careful to guarantee that actions are dispatched asynchronously when you are depending on inner subscriptions like fetchUserEpic2 is using.
Here is the modified source with the tracing logging statements:
const fetchUserEpic = (action$, store) =>
action$
.do(a => console.log(`fetchUserEpic src: ${a.type}`))
.ofType(FETCH_USER)
.do(a => console.log(`fetchUserEpic in: ${a.type}`))
.mergeMap(action => {
const observable = isFetchced ? Observable.of({
id: 2,
firstName: 'Bilbo',
lastName: 'Baggins'
}) : fakeAjax(`/api/users/${action.payload}`);
return observable
.map(response => (isFetchced = true,fetchUserFulfilled(response)))
.takeUntil(action$.ofType(FETCH_USER_CANCELLED))
})
.do(a => console.log(`fetchUserEpic out: ${a.type}`));
const fetchUserEpic2 = action$ =>
action$
.do(a => console.log(`fetchUserEpic2 src: ${a.type}`))
.ofType(FETCH_USER2)
.do(a => console.log(`fetchUserEpic2 in: ${a.type}`))
.switchMap(() =>
action$
.do(a => console.log(`fetchUserEpic2-inner src: ${a.type}`))
.ofType(FETCH_USER_FULFILLED)
.do(a => console.log(`fetchUserEpic2-inner in: ${a.type}`))
.take(1)
.do(() => console.log("First epic"))
.mergeMap(() =>
fakeAjax2(`/api/users/${1}`)
.map(response => fetchUserFulfilled2(response))
).startWith(fetchUser('redux-observable')))
.do(a => console.log(`fetchUserEpic2 out: ${a.type}`));
Related
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]);
i have this action:
export const connectToServer = (url, config, method) => {
return (dispatch) => {
dispatch({type: CONNECTION_START});
axios({
method: method,
url: url,
data: config
})
.then((response) => {
dispatch({type: CONNECTION_LOADING_SUCCESS, payload: response.data});
})
.catch((error) => {
dispatch({type: CONNECTION_LOADING_ERROR, payload: error.response.data});
})
}
};
And 2 identical reducers:
const initialState = {
data: null,
isLoading: false,
error: null
};
export const connectToServerReducer = (state = initialState, action) => {
switch (action.type) {
case CONNECTION_START :
return {...state, isLoading: true};
case CONNECTION_LOADING_SUCCESS :
return {...state, isLoading: false, data: action.payload, error: null};
case CONNECTION_LOADING_ERROR:
return {...state, isLoading: false, data: null, error: action.payload};
default :
return state
}
};
export const differentUrlConnectToServerReducerTest = (state = initialState, action) => {
switch (action.type) {
case CONNECTION_START :
return {...state, isLoading: true};
case CONNECTION_LOADING_SUCCESS :
return {...state, isLoading: false, data: action.payload, error: null};
case CONNECTION_LOADING_ERROR:
return {...state, isLoading: false, data: null, error: action.payload};
default :
return state
}
};
My store looks like this:
const rootReducer = combineReducers({
allUsersData: connectToServerReducer,
testData: differentUrlConnectToServerReducerTest
});
const configureStore = () => createStore(rootReducer, applyMiddleware(thunk));
export default configureStore
Then i use redux hooks to get a state with data in my components
const allUsersData = useSelector(state => state.allUsersData);
const testData = useSelector(state => state.testData);
const dispatch = useDispatch();
Finally i dispatch them
dispatch(connectToServer(`${BASE_URL}user/allUsersWithPets`, null, 'get'));
dispatch(connectToServer(`${BASE_URL}fakeUrl`, null, 'get'));
I receive a correct data in allUsersData, but also i receive it in testData but i should receive in testData an initial state(empty object), because url is a fake
Where am i wrong?
You need to separate the reducers, use different initial states for example:
connectToServer.js
connectToServerTest.js
Or you can try to add the test object to the initial state of connectToServerReducer.(not a good solution though)
const initialState = {
data: null,
testData: null,
isLoading: false,
error: null
};
Remember that arrays affections won't assign values but addresses, so the "data" array is the same array in both the connectToServerReducer and connectToServerReducerTest.
Second problem, you are calling the same action name in both reducers, this causes them not only to share the same variable from the previous problem I told you, but they share the same value assigned to them as well.
Just change them to:
CONNECTION_TEST_LOADING_SUCCESS
CONNECTION_TEST_LOADING_ERROR
CONNECTION_TEST_START
PS:
instead of using:
export const connectToServer = (url, config, method) => {
return (dispatch) => {
...
}
}
Use:
export const connectToServer = (url, config, method) => (dispatch) => {
...
}
Firstly, I'm working with React Native. I'm getting a data from Firebase and want to write to store (by Redux) quickly. But It doesn't work. You can find my all of codes below:
Function:
async getTumData (uid) {
const {selectedGroupDetail, getSelectedGroupDetail} = this.props;
var yeniGrupDetayi = {};
await firebase.database().ref("/groups/"+uid).once('value').then(
function(snapshot){
yeniGrupDetayi = {...snapshot.val(), uid: uid};
}).catch(e => console.log(e.message));
console.log("FONKSIYON ICERISINDEKI ITEM ==>", yeniGrupDetayi);
this.props.getSelectedGroupDetail(yeniGrupDetayi);
console.log("ACTION'DAN GELEN ITEM ===>", selectedGroupDetail);
}
Action:
export const getSelectedGroupDetail = (yeniGrupDetayi) => {
return {
type: GET_SELECTED_GROUP_DETAIL,
payload: yeniGrupDetayi
}
};
Reducer:
case GET_SELECTED_GROUP_DETAIL:
return { ...state, selectedGroupDetail: action.payload}
Çıktı:
FONKSIYON ICERISINDEKI ITEM ==> {admin: {…}, groupDescription: "Yaygın inancın tersine, Lorem Ipsum rastgele sözcü…erini incelediğinde kesin bir kaynağa ulaşmıştır.", groupName: "İnsan Kaynakları", groupProfilePic: "", members: {…}, …}
ACTION'DAN GELEN ITEM ===> {}
There is a FlatList in my page and I defined a button in renderItem of FlatList. When i click to this button, getTumData() function is working.
When i click to this button first time, selectedGroupDetail is null. Second time, it shows previous data.
How can i write a data to Store quickly and fast?
Thanks,
The thing is:
- You're using both async/await, and then/catch in your code.
- you're calling getSelectedGroupDetail before your async code resolves.
Fast Solution
getTumData = (uid) => {
const {selectedGroupDetail, getSelectedGroupDetail} = this.props;
var yeniGrupDetayi = {};
firebase.database().ref("/groups/"+uid).once('value').then(
(snapshot) => {
yeniGrupDetayi = {...snapshot.val(), uid: uid};
this.props.getSelectedGroupDetail(yeniGrupDetayi);
}).catch(e => console.log(e.message));
};
Better Solution:
1st: use Redux-Thunk middleware.
2nd: Move your Async code into your action creator: I mean this
async getTumData (uid) {
const {selectedGroupDetail, getSelectedGroupDetail} = this.props;
var yeniGrupDetayi = {};
await firebase.database().ref("/groups/"+uid).once('value').then(
function(snapshot){
yeniGrupDetayi = {...snapshot.val(), uid: uid};
}).catch(e => console.log(e.message));
console.log("FONKSIYON ICERISINDEKI ITEM ==>", yeniGrupDetayi);
this.props.getSelectedGroupDetail(yeniGrupDetayi);
console.log("ACTION'DAN GELEN ITEM ===>", selectedGroupDetail);
}
3rd: Your reducer should have another piece of data as an indicator for the time-gap before your selectedGroupDetail resolves:
// reducer initial state:
const INITIAL_STATE = { error: '', loading: false, selectedGroupDetail: null }
4th: Inside your action creator, you should dispatch 3 actions:
ACTION_NAME_START // This should should only set loading to true in your reducer.
ACTION_NAME_SUCCESS // set loading to false, and selectedGroupDetail to the new collection retured
ACTION_NAME_FAIL // in case op failed set error
5th: Your React component, should display a loading indicator (spinner or somthing), and maybe disable FlatList button during the loading state.
// Action creator
export const myAction = () => (dispatch) => {
dispatch({ type: ACTION_NAME_START });
firebase.database().ref("/groups/"+uid).once('value').then(
function(snapshot){
yeniGrupDetayi = {...snapshot.val(), uid: uid};
dispatch({ type: ACTION_NAME_SUCCESS, payload: yeniGrupDetayi });
}).catch(e => {
dispatch({ type: ACTION_NAME_FAIL, payload: e.message });
});
};
// Reducer
const INITIAL_STATE = {
loading: false,
error: '',
data: null,
};
export default (state = INITIAL_STATE, { type, payload }) => {
switch (type) {
case ACTION_NAME_START:
return {
...state,
error: '',
loading: true,
data: null,
};
case ACTION_NAME_SUCCESS:
return {
...state,
error: '',
loading: false,
data: payload,
};
case ACTION_NAME_FAIL:
return {
...state,
error: payload,
loading: false,
data: null,
};
default:
return state;
}
};
I'm trying to create a simple middleware to handle socket events.
const join = (channel) => (dispatch) => {
dispatch({
type: 'ACTION-1',
socketChannel: {...},
events: [...],
});
};
I dispatch this action that triggers it. And now when the dispatch method was called in my middleware with type 'ACTION-2' and received socketData as a payload, I see in my console what 'ACTION-1' was triggered twice and in the last time it is came with my socketData payload.
I wonder why 'ACTION-1' was registered instead 'ACTION-2' and how I can fix it? I would appreciate your help.
import { socket } from 'services/socket';
const socketMiddleware = ({ dispatch }) => next => (action) => {
const {
channel,
events, // an array of events for the channel
...rest
} = action;
if (typeof action === 'function' || !channel) {
return next(action);
}
const {
type,
name,
} = channel;
const channelInstance = socket.instance[type](name);
events.forEach((event) => {
const handleEvent = (socketData) => {
dispatch({ type: 'ACTION-2', socketData, ...rest });
};
channelInstance.listen(event.name, handleEvent);
});
return next(action);
};
export {
socketMiddleware
};
looks like you are not pathing the channel in your initial dispatch and you are failing your middleware finishes inside this if:
if (typeof action === 'function' || !channel) {
return next(action);
}
in order to fix this you should add channel in your dispatch:
const join = (channel) => (dispatch) => {
dispatch({
type: 'ACTION-1',
socketChannel: {...},
events: [...],
channel: { type: '...', name: '...' }
});
};
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.