Skip to content

Commit f465022

Browse files
authored
Add mergeProps argument (#20)
* Add minor optimization by checking `exists()` before calling `val()` * Add `mergeProps` argument to `connect()` * Change argument order to ownProps, subscriptionProps and actionProps * Fix "Unknown prop `firebaseApp` on <div> tag" warning * Pass firebaseProps to mergeProps instead of actionProps and subscriptionProps * Clarify "actions" term in README * Revert adding exists() to snapshot.val() check * Only merge actions from firebaseProps before merging * Improve tests
1 parent 5f9e48b commit f465022

File tree

4 files changed

+83
-24
lines changed

4 files changed

+83
-24
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default connect((props, ref) => ({
3838

3939
## Usage
4040

41-
### `connect([mapFirebaseToProps])`
41+
### `connect([mapFirebaseToProps], [mergeProps])`
4242

4343
Connects a React component to a Firebase App reference.
4444

@@ -48,10 +48,15 @@ It does not modify the component class passed to it. Instead, it *returns* a new
4848

4949
* [`mapFirebaseToProps(props, ref, firebaseApp): subscriptions`] \(*Object or Function*): Its result, or the argument itself must be a plain object. Each value must either be a path to a location in your database, a query object or a function. If you omit it, the default implementation just passes `firebaseApp` as a prop to your component.
5050

51+
52+
* [`mergeProps(ownProps, firebaseProps): props`] \(*Function*): If specified, it is passed the parent `props` and current subscription state merged with the result of `mapFirebaseToProps()`. The plain object you return from it will be passed as props to the wrapped component. If you omit it, `Object.assign({}, ownProps, firebaseProps)` is used by default.
53+
5154
#### Returns
5255

5356
A React component class that passes subscriptions and actions as props to your component according to the specified options.
5457

58+
> Note: "actions" are any function values returned by `mapFirebaseToProps()` which are typically used to modify data in Firebase.
59+
5560
##### Static Properties
5661

5762
* `WrappedComponent` *(Component)*: The original component class passed to `connect()`.

src/connect.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ import 'firebase/database'
66
import { firebaseAppShape } from './PropTypes'
77
import { applyMethods, getDisplayName } from './utils'
88

9-
const mergeProps = (actionProps, subscriptionProps, ownProps) => ({
9+
const defaultMergeProps = (ownProps, firebaseProps) => ({
1010
...ownProps,
11-
...actionProps,
12-
...subscriptionProps,
11+
...firebaseProps,
1312
})
1413

1514
const mapSubscriptionsToQueries = subscriptions => (
@@ -21,7 +20,7 @@ const mapSubscriptionsToQueries = subscriptions => (
2120

2221
const defaultMapFirebaseToProps = (props, ref, firebaseApp) => ({ firebaseApp })
2322

24-
export default (mapFirebaseToProps = defaultMapFirebaseToProps) => {
23+
export default (mapFirebaseToProps = defaultMapFirebaseToProps, mergeProps = defaultMergeProps) => {
2524
const mapFirebase = (
2625
isFunction(mapFirebaseToProps) ? mapFirebaseToProps : () => mapFirebaseToProps
2726
)
@@ -132,7 +131,7 @@ export default (mapFirebaseToProps = defaultMapFirebaseToProps) => {
132131
const firebaseProps = mapFirebase(this.props, this.ref, this.firebaseApp)
133132
const actionProps = pickBy(firebaseProps, isFunction)
134133
const subscriptionProps = this.state.subscriptionsState
135-
const props = mergeProps(actionProps, subscriptionProps, this.props)
134+
const props = mergeProps(this.props, { ...actionProps, ...subscriptionProps })
136135

137136
return createElement(WrappedComponent, props)
138137
}

src/tests/connect-test.js

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import { findRenderedComponentWithType, renderIntoDocument } from 'react-addons-
99
import connect from '../connect'
1010
import { createMockApp, createMockSnapshot } from './helpers'
1111

12-
const renderStub = (mapFirebaseToProps, firebaseApp, props) => {
12+
const renderStub = ({ mapFirebaseToProps, mergeProps, firebaseApp }, props) => {
1313
class Passthrough extends Component { // eslint-disable-line react/prefer-stateless-function
1414
render() {
1515
return <div />
1616
}
1717
}
1818

19-
const WrappedComponent = connect(mapFirebaseToProps)(Passthrough)
19+
const WrappedComponent = connect(mapFirebaseToProps, mergeProps)(Passthrough)
2020
const container = renderIntoDocument(<WrappedComponent {...props} firebaseApp={firebaseApp} />)
2121
const stub = findRenderedComponentWithType(container, Passthrough)
2222

@@ -33,7 +33,7 @@ test('Should throw if no initialized Firebase app instance was found', assert =>
3333
// Default app instance
3434
assert.doesNotThrow(() => {
3535
const defaultApp = firebase.initializeApp({})
36-
const WrappedComponent = connect()('div')
36+
const WrappedComponent = connect()(() => <div />)
3737
const container = renderIntoDocument(<WrappedComponent />)
3838
const stub = findRenderedComponentWithType(container, WrappedComponent)
3939

@@ -66,16 +66,59 @@ test('Should subscribe to a single path', assert => {
6666
},
6767
on: (event, callback) => {
6868
assert.equal(event, 'value')
69-
callback(createMockSnapshot('foo value'))
69+
callback(createMockSnapshot({ bar: 'bar' }))
7070
},
7171
}
7272

7373
const mapFirebaseToProps = () => ({ foo: 'foo' })
7474
const firebaseApp = createMockApp(mockDatabase)
75-
const { state, props } = renderStub(mapFirebaseToProps, firebaseApp)
75+
const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp })
7676

77-
assert.deepEqual(state, { foo: 'foo value' })
78-
assert.equal(props.foo, 'foo value')
77+
assert.deepEqual(state, { foo: { bar: 'bar' } })
78+
assert.deepEqual(props.foo, { bar: 'bar' })
79+
assert.end()
80+
})
81+
82+
test('Should return null if a subscribed path does not exist', assert => {
83+
const mockDatabase = {
84+
ref: path => {
85+
assert.equal(path, 'foo')
86+
87+
return mockDatabase
88+
},
89+
on: (event, callback) => {
90+
assert.equal(event, 'value')
91+
callback(createMockSnapshot(null))
92+
},
93+
}
94+
95+
const mapFirebaseToProps = () => ({ foo: 'foo' })
96+
const firebaseApp = createMockApp(mockDatabase)
97+
const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp })
98+
99+
assert.deepEqual(state, { foo: null })
100+
assert.equal(props.foo, null)
101+
assert.end()
102+
})
103+
104+
test('Should not pass unresolved subscriptions from result of mapFirebaseToProps', assert => {
105+
const mockDatabase = {
106+
ref: path => {
107+
assert.equal(path, 'foo')
108+
109+
return mockDatabase
110+
},
111+
on: event => {
112+
assert.equal(event, 'value')
113+
},
114+
}
115+
116+
const mapFirebaseToProps = () => ({ foo: 'foo' })
117+
const firebaseApp = createMockApp(mockDatabase)
118+
const first = renderStub({ mapFirebaseToProps, firebaseApp })
119+
120+
assert.equal(first.state, null)
121+
assert.equal(first.props.foo, undefined)
79122
assert.end()
80123
})
81124

@@ -112,7 +155,7 @@ test('Should subscribe to a query', assert => {
112155
})
113156

114157
const firebaseApp = createMockApp(mockDatabase)
115-
const { state, props } = renderStub(mapFirebaseToProps, firebaseApp)
158+
const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp })
116159

117160
assert.deepEqual(state, { bar: 'bar value' })
118161
assert.equal(props.bar, 'bar value')
@@ -126,7 +169,7 @@ test('Should not subscribe to functions', assert => {
126169
})
127170

128171
const firebaseApp = createMockApp()
129-
const { state, props } = renderStub(mapFirebaseToProps, firebaseApp)
172+
const { state, props } = renderStub({ mapFirebaseToProps, firebaseApp })
130173

131174
assert.deepEqual(state, { foo: 'foo value' })
132175
assert.equal(props.foo, 'foo value')
@@ -152,7 +195,7 @@ test('Should unsubscribe when component unmounts', assert => {
152195

153196
const mapFirebaseToProps = () => ({ baz: 'baz' })
154197
const firebaseApp = createMockApp(mockDatabase)
155-
const { container } = renderStub(mapFirebaseToProps, firebaseApp)
198+
const { container } = renderStub({ mapFirebaseToProps, firebaseApp })
156199

157200
assert.notEqual(container.listeners.baz, undefined)
158201
unmountComponentAtNode(findDOMNode(container).parentNode)
@@ -171,31 +214,43 @@ test('Should pass props, ref and firebaseApp to mapFirebaseToProps', assert => {
171214
}
172215

173216
const firebaseApp = createMockApp()
174-
const { props } = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo prop' })
217+
const { props } = renderStub({ mapFirebaseToProps, firebaseApp }, { foo: 'foo prop' })
175218

176219
assert.equal(props.foo, 'foo value')
177220
assert.end()
178221
})
179222

180223
test('Should update subscriptions when props change', assert => {
181224
const mapFirebaseToProps = props => ({ foo: props.foo, bar: props.bar })
182-
183225
const firebaseApp = createMockApp()
184-
const initial = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo' })
226+
const stubOptions = { mapFirebaseToProps, firebaseApp }
227+
228+
const initial = renderStub(stubOptions, { foo: 'foo' })
185229
assert.equal(initial.props.foo, 'foo value')
186230
assert.equal(initial.props.bar, undefined)
187231

188-
const added = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo', bar: 'bar' })
232+
const added = renderStub(stubOptions, { foo: 'foo', bar: 'bar' })
189233
assert.equal(added.props.foo, 'foo value')
190234
assert.equal(added.props.bar, 'bar value')
191235

192-
const changed = renderStub(mapFirebaseToProps, firebaseApp, { foo: 'foo', bar: 'baz' })
236+
const changed = renderStub(stubOptions, { foo: 'foo', bar: 'baz' })
193237
assert.equal(changed.props.foo, 'foo value')
194238
assert.equal(changed.props.bar, 'baz value')
195239

196-
const removed = renderStub(mapFirebaseToProps, firebaseApp, { bar: 'baz' })
240+
const removed = renderStub(stubOptions, { bar: 'baz' })
197241
assert.equal(removed.props.foo, undefined)
198242
assert.equal(removed.props.bar, 'baz value')
199243

200244
assert.end()
201245
})
246+
247+
test('Should use custom mergeProps function if provided', assert => {
248+
const mapFirebaseToProps = props => ({ foo: props.foo })
249+
const mergeProps = () => ({ bar: 'bar merge props' })
250+
251+
const firebaseApp = createMockApp()
252+
const { props } = renderStub({ mapFirebaseToProps, mergeProps, firebaseApp }, { foo: 'foo prop' })
253+
254+
assert.deepEqual(props, { bar: 'bar merge props' })
255+
assert.end()
256+
})

src/tests/helpers.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
export const createMockSnapshot = (val, ...otherProps) => ({
1+
export const createMockSnapshot = (value, ...otherProps) => ({
22
...otherProps,
3-
val: () => val,
3+
val: () => value,
44
})
55

66
const defaultDatabaseProps = {

0 commit comments

Comments
 (0)