Skip to content

Commit 7e3269e

Browse files
authored
v1.0.0-alpha (#142)
* feat(reducers): `ordered` and `data` reducer using new v1 state pattern outlined in [the v1.0.0 roadmap](https://github.com/prescottprue/redux-firestore/wiki/v1.0.0-Roadmap) (full query path in ordered, sub-collections separate from doc in data) * feat(core): `firestoreDataSelector ` and`firestoreOrderedSelector` utilities for selecting values from state * fix(reducers): `LISTENER_RESPONSE` action not correctly updating state for sub-collections - #103 * fix(reducers): `ordered` state not updated with added item in array - #116 * fix(reducers): updates to arrays inside documents don't work as expected - #140
1 parent d946b59 commit 7e3269e

File tree

14 files changed

+310
-354
lines changed

14 files changed

+310
-354
lines changed

.travis.yml

+8
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ deploy:
4747
on:
4848
node: '10'
4949
branch: 'next'
50+
- provider: npm
51+
skip_cleanup: true
52+
email: $NPM_EMAIL
53+
api_key: $NPM_TOKEN
54+
tag: alpha
55+
on:
56+
node: '10'
57+
branch: 'alpha'

README.md

+15-5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ It is common to make react components "functional" meaning that the component is
9191

9292
```js
9393
import { connect } from 'react-redux'
94+
import { firestoreOrderedSelector } from 'redux-firestore'
9495
import {
9596
compose,
9697
withHandlers,
@@ -104,22 +105,30 @@ const withStore = compose(
104105
getContext({ store: PropTypes.object }),
105106
)
106107

108+
function todosQuery() {
109+
return {
110+
collection: 'todos'
111+
}
112+
}
113+
114+
const orderedTodosSelector = firestoreOrderedSelector(todosQuery())
115+
107116
const enhance = compose(
108117
withStore,
109118
withHandlers({
110-
loadData: props => () => props.store.firestore.get('todos'),
119+
loadData: props => () => props.store.firestore.get(todosQuery()),
111120
onDoneClick: props => (key, done = false) =>
112121
props.store.firestore.update(`todos/${key}`, { done }),
113122
onNewSubmit: props => newTodo =>
114-
props.store.firestore.add('todos', { ...newTodo, owner: 'Anonymous' }),
123+
props.store.firestore.add(todosQuery(), { ...newTodo, owner: 'Anonymous' }),
115124
}),
116125
lifecycle({
117126
componentWillMount(props) {
118127
props.loadData()
119128
}
120129
}),
121-
connect(({ firebase }) => ({ // state.firebase
122-
todos: firebase.ordered.todos,
130+
connect((state) => ({
131+
todos: orderedTodosSelector(state),
123132
}))
124133
)
125134

@@ -161,9 +170,10 @@ class Todos extends Component {
161170
)
162171
}
163172
}
173+
const orderedTodosSelector = firestoreOrderedSelector(todosQuery())
164174

165175
export default connect((state) => ({
166-
todos: state.firestore.ordered.todos
176+
todos: orderedTodosSelector(state)
167177
}))(Todos)
168178
```
169179
### API

examples/complete/src/routes/Home/components/Home/Home.js

+30-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Theme from 'theme'
44
import { connect } from 'react-redux'
55
import { compose, lifecycle, withHandlers } from 'recompose'
66
import { withFirestore } from 'react-redux-firebase'
7+
import { firestoreOrderedSelector } from 'redux-firestore'
78
import { withStore } from 'utils/components'
89
import classes from './Home.scss'
910

@@ -15,7 +16,7 @@ const Home = ({ todos }) => (
1516
<h2>Home Route</h2>
1617
</div>
1718
<div>
18-
{todos.map((todo, i) => (
19+
{todos && todos.map && todos.map((todo, i) => (
1920
<div key={`${todo.id}-${i}`}>{JSON.stringify(todo)}</div>
2021
))}
2122
</div>
@@ -26,6 +27,30 @@ Home.propTypes = {
2627
todos: PropTypes.array
2728
}
2829

30+
// Function which returns todos query config
31+
function getTodosQuery() {
32+
return {
33+
collection: 'todos',
34+
limit: 10
35+
}
36+
}
37+
38+
// Function which returns todos query config
39+
function getTodoEventsQuery(props) {
40+
if (!props.todoId) {
41+
console.error('todoId is required to create todo events query, check component props')
42+
return
43+
}
44+
return {
45+
collection: 'todos',
46+
doc: props.todoId,
47+
limit: 10,
48+
subcollections: [{ collection: 'events' }]
49+
}
50+
}
51+
52+
const selector = firestoreOrderedSelector(getTodosQuery())
53+
2954
const enhance = compose(
3055
withStore,
3156
withFirestore,
@@ -39,12 +64,12 @@ const enhance = compose(
3964
}),
4065
lifecycle({
4166
componentWillMount() {
42-
this.props.loadCollection('todos')
67+
this.props.loadCollection(getTodosQuery())
4368
}
4469
}),
45-
connect(({ firestore, firebase }) => ({
46-
todos: firestore.ordered.todos || [],
47-
uid: firebase.auth.uid
70+
connect((state, props) => ({
71+
todos: selector(state),
72+
uid: state.firebase.auth.uid
4873
}))
4974
)
5075

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-firestore",
3-
"version": "0.6.0-alpha.2",
3+
"version": "1.0.0-alpha",
44
"description": "Redux bindings for Firestore.",
55
"main": "lib/index.js",
66
"module": "es/index.js",

src/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { firestoreActions } from './actions';
44
import createFirestoreInstance from './createFirestoreInstance';
55
import constants, { actionTypes } from './constants';
66
import middleware, { CALL_FIRESTORE } from './middleware';
7+
import { getQueryName } from './utils/query';
8+
import { firestoreOrderedSelector, firestoreDataSelector } from './selectors';
79

810
// converted with transform-inline-environment-variables
911
export const version = process.env.npm_package_version;
@@ -15,6 +17,9 @@ export {
1517
enhancer as reduxFirestore,
1618
createFirestoreInstance,
1719
firestoreActions as actions,
20+
getQueryName,
21+
firestoreOrderedSelector,
22+
firestoreDataSelector,
1823
getFirestore,
1924
constants,
2025
actionTypes,

src/reducers/dataReducer.js

+32-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { get } from 'lodash';
1+
import { get, last, dropRight } from 'lodash';
22
import { setWith, assign } from 'lodash/fp';
33
import { actionTypes } from '../constants';
4-
import { pathFromMeta, preserveValuesFromState } from '../utils/reducers';
4+
import { getQueryName } from '../utils/query';
5+
import { preserveValuesFromState, pathToArr } from '../utils/reducers';
56

67
const {
78
CLEAR_DATA,
@@ -37,42 +38,41 @@ export default function dataReducer(state = {}, action) {
3738
if (!payload || payload.data === undefined) {
3839
return state;
3940
}
40-
// Get doc from subcollections if they exist
41-
const getDocName = data =>
42-
data.subcollections
43-
? getDocName(data.subcollections.slice(-1)[0]) // doc from last item of subcollections array
44-
: data.doc; // doc from top level meta
45-
const docName = getDocName(meta);
46-
// Data to set to state is doc if doc name exists within meta
47-
const data = docName ? get(payload.data, docName) : payload.data;
41+
const queryName = getQueryName(meta, { onlySubcollections: true });
4842
// Get previous data at path to check for existence
49-
const previousData = get(state, meta.storeAs || pathFromMeta(meta));
43+
const previousData = get(state, meta.storeAs || queryName);
44+
if (meta.subcollections) {
45+
const setPath =
46+
queryName.split('/').length % 2
47+
? getQueryName(meta)
48+
: dropRight(pathToArr(queryName)).join('/');
49+
// Set data to state immutabily (lodash/fp's setWith creates copy)
50+
return setWith(Object, setPath, payload.data, state);
51+
}
5052
// Set data (without merging) if no previous data exists or if there are subcollections
5153
if (!previousData || meta.subcollections) {
5254
// Set data to state immutabily (lodash/fp's setWith creates copy)
53-
return setWith(Object, meta.storeAs || pathFromMeta(meta), data, state);
55+
return setWith(Object, meta.storeAs || queryName, payload.data, state);
5456
}
5557
// Otherwise merge with existing data
56-
const mergedData = assign(previousData, data);
58+
const mergedData = assign(previousData, payload.data);
5759
// Set data to state (with merge) immutabily (lodash/fp's setWith creates copy)
58-
return setWith(
59-
Object,
60-
meta.storeAs || pathFromMeta(meta),
61-
mergedData,
62-
state,
63-
);
60+
return setWith(Object, meta.storeAs || queryName, mergedData, state);
6461
case DOCUMENT_MODIFIED:
6562
case DOCUMENT_ADDED:
6663
return setWith(
6764
Object,
68-
pathFromMeta(action.meta),
65+
getQueryName(action.meta, { onlySubcollections: true }),
6966
action.payload.data,
7067
state,
7168
);
7269
case DOCUMENT_REMOVED:
7370
case DELETE_SUCCESS:
74-
const removePath = pathFromMeta(action.meta);
75-
const cleanedState = setWith(Object, removePath, null, state);
71+
const removePath = getQueryName(action.meta, {
72+
onlySubcollections: true,
73+
});
74+
const id = last(pathToArr(getQueryName(action.meta)));
75+
const cleanedState = setWith(Object, `${removePath}.${id}`, null, state);
7676
if (action.preserve && action.preserve.data) {
7777
return preserveValuesFromState(
7878
state,
@@ -89,11 +89,19 @@ export default function dataReducer(state = {}, action) {
8989
return {};
9090
case LISTENER_ERROR:
9191
// Set data to state immutabily (lodash/fp's setWith creates copy)
92-
const nextState = setWith(Object, pathFromMeta(action.meta), null, state);
92+
const nextState = setWith(
93+
Object,
94+
getQueryName(action.meta, { onlySubcollections: true }),
95+
null,
96+
state,
97+
);
9398
if (action.preserve && action.preserve.data) {
9499
return preserveValuesFromState(state, action.preserve.data, nextState);
95100
}
96-
const existingState = get(state, pathFromMeta(action.meta));
101+
const existingState = get(
102+
state,
103+
getQueryName(action.meta, { onlySubcollections: true }),
104+
);
97105
// If path contains data already, leave it as it is (other listeners
98106
// could have placed it there)
99107
if (existingState) {

0 commit comments

Comments
 (0)