dependabot[bot] on npm_and_yarn
jayphelps on master
chore(deps): bump trim-off-newl… (compare)
dependabot[bot] on npm_and_yarn
chore(deps): bump trim-off-newl… (compare)
jayphelps on master
docs(examples): update to use `… (compare)
dependabot[bot] on npm_and_yarn
dependabot[bot] on npm_and_yarn
dependabot[bot] on npm_and_yarn
import { mergeMap, map } from "rxjs/operators";
import { ofType } from "redux-observable";
import { ajax } from "rxjs/ajax";
import { Observable } from "rxjs";
import {
FETCH_BOOKS,
fetchBooksFailure,
fetchBooksSuccess,
} from "../actions/index";
let api = "https://the-one-api.herokuapp.com/v1/book";
function fetchBooksEpic(action$) {
try {
return action$.pipe(
ofType(FETCH_BOOKS),
mergeMap(() => {
return ajax.get(api).pipe(
map((data) => Array.from(data.docs))
);
}),
map((books) => fetchBooksSuccess(books))
);
} catch (error) {
Observable.of(fetchBooksFailure(error.message));
}
}
export default fetchBooksEpic;
import { API_TOKEN, api } from "../constants/demoConstants";
import { switchMap, mergeMap, map, flatMap } from "rxjs/operators";
import { ofType } from "redux-observable";
import { ajax } from "rxjs/ajax";
import { Observable } from "rxjs";
import {
FETCH_CHARACTERS,
fetchCharactersSuccess,
fetchCharactersFailure,
} from "../actions/index";
let xhr = new XMLHttpRequest();
xhr.open("GET", api, true);
xhr.setRequestHeader("Authorization", "Bearer " + API_TOKEN);
xhr.send();
function fetchCharactersEpic(action$) {
try {
return action$.pipe(
ofType(FETCH_CHARACTERS),
mergeMap(() => {
return ajax.getJSON(api).pipe(
flatMap((data) => Array.from(data.docs.text())),
map((characters) =>
characters.map((character) => ({
id: character._id,
name: character.name,
wikiUrl: character.wikiUrl,
spouse: character.spouse,
}))
)
);
}),
map((characters) => fetchCharactersSuccess(characters))
);
} catch (error) {
Observable.of(fetchCharactersFailure(error.message));
}
}
export default fetchCharactersEpic;
import { combineEpics } from "redux-observable";
import fetchWhiskiesEpic from "./fetchWhiskiesEpic";
import fetchBooksEpic from "./fetchBooksEpic";
import fetchCharactersEpic from "./fetchCharactersEpic";
export default combineEpics(
fetchBooksEpic,
fetchWhiskiesEpic,
fetchCharactersEpic
);
import {
FETCH_CHARACTERS,
FETCH_CHARACTERS_FAILURE,
FETCH_CHARACTERS_SUCCESS,
} from "../actions/index";
const initialState = {
isLoaded: false,
characters: [],
error: null,
};
const charactersReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_CHARACTERS:
return {
...state,
// whenever we want to fetch the CHARACTERS, set isLoading to true to show a spinner
isLoading: true,
error: null,
};
case FETCH_CHARACTERS_SUCCESS:
return {
characters: [...action.payload],
// whenever the fetching finishes, we stop showing the spinner and then show the data
isLoading: false,
error: null,
};
case FETCH_CHARACTERS_FAILURE:
return {
characters: [],
isLoading: false,
// same as FETCH_CHARACTERS_SUCCESS, but instead of data we will show an error message
error: action.payload,
};
default:
return state;
}
};
export default charactersReducer;
import { combineReducers } from "redux";
import { connectRouter } from "connected-react-router";
import whiskysReducer from "./whiskysReducer";
import booksReducer from "./booksReducer";
import charactersReducer from "./charactersReducer";
const rootReducer = (history) =>
combineReducers({
router: connectRouter(history),
whiskies: whiskysReducer,
books: booksReducer,
characters: charactersReducer,
});
export default rootReducer;
import {
FETCH_BOOKS,
FETCH_BOOKS_FAILURE,
FETCH_BOOKS_SUCCESS,
} from "../actions/index";
const initialState = {
isLoaded: false,
books: [],
error: null,
};
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_BOOKS:
return {
...state,
// whenever we want to fetch the BOOKS, set isLoading to true to show a spinner
isLoading: true,
error: null,
};
case FETCH_BOOKS_SUCCESS:
return {
books: [...action.payload],
// whenever the fetching finishes, we stop showing the spinner and then show the data
isLoading: false,
error: null,
};
case FETCH_BOOKS_FAILURE:
return {
books: [],
isLoading: false,
// same as FETCH_BOOKS_SUCCESS, but instead of data we will show an error message
error: action.payload,
};
default:
return state;
}
};
export default booksReducer;
function fetchCharactersEpic(action$) {
return action$.pipe(
ofType(FETCH_CHARACTERS),
mergeMap(() =>
ajax.getJSON(api, { Authorization: `Bearer ${API_TOKEN}` }).pipe(
flatMap((data) => Array.from(data.docs.text())),
map((characters) =>
characters.map((character) => ({
id: character._id,
name: character.name,
wikiUrl: character.wikiUrl,
spouse: character.spouse,
}))
),
map((characters) => fetchCharactersSuccess(characters)),
catchError((error) =>
Observable.of(fetchCharactersFailure(error.message))
)
)
)
);
}
That's a good question. This is actually a React problem.
Server-side React doesn't run useEffect
and will not work async.
The only way you can get this to work as you expect is to subscribe to store
and wait for the values you want to exist. After that, render React server-side.
I know it's weird, but React doesn't have a way to run and run again when certain changes are made server-side. The way it works, it renders HTML and that's it. So you have to do all the work before React runs, then feed React all the data necessary to get the correct server-side output you're wanting.
Actions must be plain objects. Use custom middleware for async actions.
const fetchMenu = (action$, _state$) => {
return action$.pipe(
ofType(Types.FETCH_MENU_REQUESTED),
flatMap((async ({ resID }) => {
const firestore = firebase.firestore();
const menuRef = firestore.collection('menus').where('resID', '==', resID);
return collectionData(menuRef, 'id')
.pipe(
map(val => {
console.log('vals');
console.log(val);
return Creators.fetchMenuSuccess(val);
})
)
}
)),
);
};
Hi everyone, I have a question about combineLatest in Epic, after my app has been initiated, it will dispatch initAppCompleted only once, after that every time I jump to the focus page, it will dispatch fetchFocusPage action and request API.
Based on the info in https://stackoverflow.com/questions/42426908/how-to-delay-one-epic-until-another-has-emitted-a-value, using combineLatest must be alright.
But what happened is: after clicking the focus page, only the first time it outputs 'focus page epic entering'. Only If I manually dispatch initAppCompleted action after clicking the page, otherwise there is no request happening. I really don't know why.
here is my code(using Rxjs 6.5.4 & redux-observable 1.2.0):
export const focusPageFetchEpic: Epic<AnyAction, AnyAction, any> = (
action$: ActionsObservable<Action>,
state$: StateObservable<any>
) =>
combineLatest([
action$.pipe(filter(isActionOf(initAppCompleted)), tap(() => console.log('init action comming')), take(1)),
action$.pipe(filter(isActionOf(fetchFocusPage)), tap(() => console.log('fetch action comming')))
]).pipe(
tap(() => console.log('focus page epic entering'))
)
@fisherspy It shouldn't matter about the payload. If both observables fire once, then it outputs values in your pipe
. That's how combineLatest
works. It waits for observables to output something once. After that happens, then you start receiving values in the combineLatest().pipe
.
If you want something to emit when either observable emits, use merge
instead of combineLatest
.
import { ActionsObservable } from 'redux-observable'
// ...
export const mockAjaxResponse = (
mockedResponse,
) => ({
ajax: () => (
ActionsObservable
.of({
response: mockedResponse,
})
),
})
// ...
fetchUserEpic(
// action$
ActionsObservable
.of(
fetchUser({ // this is a Redux action creator
username: 'test@test.com',
password: 'test',
})
),
// state$
new BehaviorSubject({
user: null,
}),
// Dependency injection
mockAjaxResponse(),
)
@xavier7179 I'd need a bit of code to work with. Can't visualize what you mean.
You can always do something like dispatching an action from those epics making API calls and then have a single epic do something like:
combineLatest(
action$.pipe(ofType('FIRST_API_RESPONSE')),
action$.pipe(ofType('SECOND_API_RESPONSE')),
)
@Sawtaytoes sure!
The schema I'm depicting in my mind is something like (avoiding parallelism and keeping the order):
So far, based on your previous suggestion, I did something like this (which it is not fully including all scenarios but epic error):
export function fetchEpics(action$) {
return action$.pipe(
ofType(FETCH_EPIC1),
take(1),
mergeMap(
(original_action) =>
concat(
of(fetchEPIC1(original_action.payload)),
action$.ofType(FETCH_EPIC2).pipe(
take(1),
map(() => fetchEPIC2(original_action.payload)
)
),
),
)
);
}
But as soon as I try a third one, I got stuck.
@xavier7179 Is there any reason you can't move this code to another epic?
action$.ofType(FETCH_EPIC2).pipe(
take(1),
map(() => fetchEPIC2(original_action.payload))
I know you need original_action.payload
. When your action completes, can you add originalActionPayload: original_action.payload
to the action?
@xavier7179 Is there any reason you can't move this code to another epic?
Sorry, I'm not sure I got the question...
I know you need
original_action.payload
. When your action completes, can you addoriginalActionPayload: original_action.payload
to the action?
I can certainly add it. Is it the issue I'm facing figuring out how to chain several epics in sequence, avoiding parallelism?
I'm not sure if nesting is the right approach. what about putting all the epics at the top level? Start the second epic with the success action of the first. Have a separate epic to further process the errors or combine the logic into a custom operator
But this way I cannot call epics alone... am I right?
`const createStoreWithMiddleware = applyMiddleware('
`promiseMiddleware,`
`ReduxThunk`
`)(createStore);`
`ReactDOM.render(`
`<Provider`
` store={createStoreWithMiddleware(`
` Reducer,`
` window.__REDUX_DEVTOOLS_EXTENSION__ &&`
` window.__REDUX_DEVTOOLS_EXTENSION__()`
` )}`
` >`
` <BrowserRouter>`
` <App />`
` </BrowserRouter>`
`</Provider>,`
@evertbouw @Sawtaytoes I actualy solved 90% of the issue with the following two epics:
export function fetchAllEpics(action$) {
return action$.pipe(
ofType(FETCH_ALL_EPICS),
take(1),
mergeMap((original_action) =>
concat(
of(fetchFirstEpicAction(original_action.payload)),
action$.ofType(EPIC1_FETCHED).pipe(
take(1),
mergeMap(() =>
concat(
of(fetchSecondEpicAction(original_action.payload)),
action$.ofType(EPIC2_FETCHED).pipe(
take(1),
map(() => fetchThirdEpicAction(original_action.payload))
)
),
)
)
)
)
);
}
export function collectEpicResults(action$) {
return action$.pipe(
ofType(FETCH_ALL_EPICS),
mergeMap((action) =>
combineLatest([
action$.ofType(EPCI1_FETCHED),
action$.ofType(EPIC2_FETCHED),
action$.ofType(EPIC3_FETCHED)
]).pipe(
map(([e1, e2, e3]) =>
fetchAllEpicsFullfilled({ e1: e1.payload, e2: e2.payload, e3: e3.payload })
),
catchError(([error_e1, error_e2, error_e3]) => of(fetchAllEpicsFailed([error_e1, error_e2, error_e3])))
)
)
);
}
where the composition can be done by replacing the map
with a mergeMap
+ concat
block. Everything works fine in the serialization, while the second epic is perfect when everything works without errors. When in the chain some epic fails (notice that each EPIC has its own piping of map and catchError) then, the first epic is stuck. I figure out that maybe the issue is not having two epics but putting all in one epic but... I did not work out how, in order to keep the correct path still fully working.
@andrevmatos That might be something in your code. I've run really complex Redux-Observable applications and performance tests. It's more-likely something where your pipelines are spawning more observables and not getting rid of past ones. Or that the pipelines are computationally intensive.
Is it possible to post any of that code?