Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for injecting a custom argument into epics #26

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,48 @@ const someOtherEpic = pipe(
)
```

## Dependencies

When creating an `EpicMiddleware` object using `createEpicMiddleware` or
`createStateStreamEnhancer`, you can pass an optional `dependencies` argument which
will be passed to each of your epics as the third argument. This can then be used
when testing your epics to avoid a more complex mocking approach.

__Example__

```js
// redux/configureStore.js
...
const fetchJSON = url => fetch(url).then(r => r.json());
const epicMiddleware = createEpicMiddleware(rootEpic, { fetchJSON })
...

// epics/user.js
import { FETCH_USER } from '../constants/ActionTypes'
import { storeUser } from '../actions'
import { select } from 'redux-most'

const fetchUserEpic = (action$, store, { fetchJSON }) =>
action$.thru(select(FETCH_USER))
.chain(({ userId }) => fromPromise(fetchJSON(`/users/${userId}`)))
.map(storeUser);

// tests/user.js
it('fetches a user then emits a STORE_USER action', () => {
const mockFetchJSON = jest
.fn()
.mockReturnValueOnce(Promise.resolve({ username: 'foo' }));
const actionsIn = of(fetchUser({ userId: 5 }));
return fetchUserEpic(actionsIn, mockStore, { fetchJSON: mockFetchJSON })
.reduce(flip(append), [])
.then(actionsOut => {
expect(actionsOut).toHaveLength(1);
expect(actionsOut[0].type).toEqual(STORE_USER);
});
});

```

## API Reference

- [createEpicMiddleware](https://github.com/joshburgess/redux-most#createepicmiddleware-rootepic)
Expand All @@ -190,14 +232,15 @@ const someOtherEpic = pipe(

---

### `createEpicMiddleware (rootEpic)`
### `createEpicMiddleware (rootEpic, dependencies)`

`createEpicMiddleware` is used to create an instance of the actual `redux-most` middleware.
You provide a single root `Epic`.
You provide a single root `Epic` and optional `dependencies`.

__Arguments__

1. `rootEpic` _(`Epic`)_: The root Epic.
2. `dependencies` _(`any`)_: Optional dependencies for your epics.

__Returns__

Expand Down Expand Up @@ -236,6 +279,7 @@ other middleware.
__Arguments__

1. `rootEpic` _(`Epic`)_: The root Epic.
2. `dependencies` _(`any`)_: Optional dependencies for your epics.

__Returns__

Expand Down
32 changes: 18 additions & 14 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,47 @@ import { Stream } from 'most';
A = Action
T = ActionType (a string or symbol)
S = State
D = Dependency
*****************************************/

// for the original, redux-observable style API
export declare interface Epic<A extends Action, S> {
export declare interface Epic<A extends Action, S, D> {
(
actionStream: Stream<A>,
middlewareApi: MiddlewareAPI<S>
middlewareApi: MiddlewareAPI<S>,
dependencies: D
): Stream<A>;
}

// for the newer, declarative only API, which takes in a state stream
// to sample via the withState utility instead of exposing dispatch/getState
export declare interface Epic<A extends Action, S> {
export declare interface Epic<A extends Action, S, D> {
(
actionStream: Stream<A>,
stateStream: Stream<S>
stateStream: Stream<S>,
dependencies: D
): Stream<A>;
}

export interface EpicMiddleware<A extends Action, S> extends Middleware {
export interface EpicMiddleware<A extends Action, S, D> extends Middleware {
replaceEpic (
nextEpic: Epic<A, S>
nextEpic: Epic<A, S, D>
): void;
}

export declare function createEpicMiddleware<A extends Action, S> (
rootEpic: Epic<A, S>
): EpicMiddleware<A, S>;
export declare function createEpicMiddleware<A extends Action, S, D> (
rootEpic: Epic<A, S, D>,
dependencies: D
): EpicMiddleware<A, S, D>;


export declare function createStateStreamEnhancer<A extends Action, S> (
epicMiddleware: EpicMiddleware<A, S>
export declare function createStateStreamEnhancer<A extends Action, S, D> (
epicMiddleware: EpicMiddleware<A, S, D>
): StoreEnhancer<S>;

export declare function combineEpics<A extends Action, S> (
epicsArray: Epic<A, S>[]
): Epic<A, S>;
export declare function combineEpics<A extends Action, S, D> (
epicsArray: Epic<A, S, D>[],
): Epic<A, S, D>;

export declare type ActionType = string | symbol

Expand Down
4 changes: 2 additions & 2 deletions src/combineEpics.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mergeArray } from 'most'
import { findIndex, map } from '@most/prelude'

export const combineEpics = epicsArray => (actions, store) => {
export const combineEpics = epicsArray => (actions, store, dependencies) => {
if (!epicsArray || !Array.isArray(epicsArray)) {
throw new TypeError('You must provide an array of Epics to combineEpics.')
}
Expand All @@ -15,7 +15,7 @@ export const combineEpics = epicsArray => (actions, store) => {
throw new TypeError('The array passed to combineEpics must contain only Epics (functions).')
}

const out = epic(actions, store)
const out = epic(actions, store, dependencies)

if (!out || !out.source) {
const epicIdentifier = epic.name
Expand Down
6 changes: 3 additions & 3 deletions src/createEpicMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { async } from 'most-subject'
import { epicBegin, epicEnd } from './actions'
import { STATE_STREAM_SYMBOL } from './constants'

export const createEpicMiddleware = epic => {
export const createEpicMiddleware = (epic, dependencies) => {
if (typeof epic !== 'function') {
throw new TypeError('You must provide an Epic (a function) to createEpicMiddleware.')
}
Expand Down Expand Up @@ -33,9 +33,9 @@ export const createEpicMiddleware = epic => {

return isUsingStateStreamEnhancer
// new style API (declarative only, no dispatch/getState)
? nextEpic(actionsIn$, state$)
? nextEpic(actionsIn$, state$, dependencies)
// redux-observable style Epic API
: nextEpic(actionsIn$, middlewareApi)
: nextEpic(actionsIn$, middlewareApi, dependencies)
}

const actionsOut$ = switchLatest(map(callNextEpic, epic$))
Expand Down
9 changes: 5 additions & 4 deletions tests/combineEpics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ test('combineEpics should combine an array of epics', t => {
const DELEGATED_1 = 'DELEGATED_1'
const DELEGATED_2 = 'DELEGATED_2'
const MOCKED_STORE = { I: 'am', a: 'store' }
const DEPENDENCIES = 'deps'

const epic1 = (actions$, store) => map(
action => ({ type: DELEGATED_1, action, store }),
select(ACTION_1, actions$)
)

const epic2 = (actions$, store) => map(
action => ({ type: DELEGATED_2, action, store }),
const epic2 = (actions$, store, deps) => map(
action => ({ type: DELEGATED_2, action, store, deps }),
select(ACTION_2, actions$)
)

Expand All @@ -27,7 +28,7 @@ test('combineEpics should combine an array of epics', t => {

const store = MOCKED_STORE
const actions$ = sync()
const result$ = epic(actions$, store)
const result$ = epic(actions$, store, DEPENDENCIES)
const emittedActions = []

observe(emittedAction => emittedActions.push(emittedAction), result$)
Expand All @@ -37,7 +38,7 @@ test('combineEpics should combine an array of epics', t => {

const MOCKED_EMITTED_ACTIONS = [
{ type: DELEGATED_1, action: { type: ACTION_1 }, store },
{ type: DELEGATED_2, action: { type: ACTION_2 }, store },
{ type: DELEGATED_2, action: { type: ACTION_2 }, store, deps: DEPENDENCIES },
]

t.deepEqual(MOCKED_EMITTED_ACTIONS, emittedActions)
Expand Down