Skip to content

Commit

Permalink
v1.0.0-alpha (#142)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
prescottprue authored Oct 3, 2018
1 parent d946b59 commit 7e3269e
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 354 deletions.
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ deploy:
on:
node: '10'
branch: 'next'
- provider: npm
skip_cleanup: true
email: $NPM_EMAIL
api_key: $NPM_TOKEN
tag: alpha
on:
node: '10'
branch: 'alpha'
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ It is common to make react components "functional" meaning that the component is

```js
import { connect } from 'react-redux'
import { firestoreOrderedSelector } from 'redux-firestore'
import {
compose,
withHandlers,
Expand All @@ -104,22 +105,30 @@ const withStore = compose(
getContext({ store: PropTypes.object }),
)

function todosQuery() {
return {
collection: 'todos'
}
}

const orderedTodosSelector = firestoreOrderedSelector(todosQuery())

const enhance = compose(
withStore,
withHandlers({
loadData: props => () => props.store.firestore.get('todos'),
loadData: props => () => props.store.firestore.get(todosQuery()),
onDoneClick: props => (key, done = false) =>
props.store.firestore.update(`todos/${key}`, { done }),
onNewSubmit: props => newTodo =>
props.store.firestore.add('todos', { ...newTodo, owner: 'Anonymous' }),
props.store.firestore.add(todosQuery(), { ...newTodo, owner: 'Anonymous' }),
}),
lifecycle({
componentWillMount(props) {
props.loadData()
}
}),
connect(({ firebase }) => ({ // state.firebase
todos: firebase.ordered.todos,
connect((state) => ({
todos: orderedTodosSelector(state),
}))
)

Expand Down Expand Up @@ -161,9 +170,10 @@ class Todos extends Component {
)
}
}
const orderedTodosSelector = firestoreOrderedSelector(todosQuery())

export default connect((state) => ({
todos: state.firestore.ordered.todos
todos: orderedTodosSelector(state)
}))(Todos)
```
### API
Expand Down
35 changes: 30 additions & 5 deletions examples/complete/src/routes/Home/components/Home/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Theme from 'theme'
import { connect } from 'react-redux'
import { compose, lifecycle, withHandlers } from 'recompose'
import { withFirestore } from 'react-redux-firebase'
import { firestoreOrderedSelector } from 'redux-firestore'
import { withStore } from 'utils/components'
import classes from './Home.scss'

Expand All @@ -15,7 +16,7 @@ const Home = ({ todos }) => (
<h2>Home Route</h2>
</div>
<div>
{todos.map((todo, i) => (
{todos && todos.map && todos.map((todo, i) => (
<div key={`${todo.id}-${i}`}>{JSON.stringify(todo)}</div>
))}
</div>
Expand All @@ -26,6 +27,30 @@ Home.propTypes = {
todos: PropTypes.array
}

// Function which returns todos query config
function getTodosQuery() {
return {
collection: 'todos',
limit: 10
}
}

// Function which returns todos query config
function getTodoEventsQuery(props) {
if (!props.todoId) {
console.error('todoId is required to create todo events query, check component props')
return
}
return {
collection: 'todos',
doc: props.todoId,
limit: 10,
subcollections: [{ collection: 'events' }]
}
}

const selector = firestoreOrderedSelector(getTodosQuery())

const enhance = compose(
withStore,
withFirestore,
Expand All @@ -39,12 +64,12 @@ const enhance = compose(
}),
lifecycle({
componentWillMount() {
this.props.loadCollection('todos')
this.props.loadCollection(getTodosQuery())
}
}),
connect(({ firestore, firebase }) => ({
todos: firestore.ordered.todos || [],
uid: firebase.auth.uid
connect((state, props) => ({
todos: selector(state),
uid: state.firebase.auth.uid
}))
)

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-firestore",
"version": "0.6.0-alpha.2",
"version": "1.0.0-alpha",
"description": "Redux bindings for Firestore.",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { firestoreActions } from './actions';
import createFirestoreInstance from './createFirestoreInstance';
import constants, { actionTypes } from './constants';
import middleware, { CALL_FIRESTORE } from './middleware';
import { getQueryName } from './utils/query';
import { firestoreOrderedSelector, firestoreDataSelector } from './selectors';

// converted with transform-inline-environment-variables
export const version = process.env.npm_package_version;
Expand All @@ -15,6 +17,9 @@ export {
enhancer as reduxFirestore,
createFirestoreInstance,
firestoreActions as actions,
getQueryName,
firestoreOrderedSelector,
firestoreDataSelector,
getFirestore,
constants,
actionTypes,
Expand Down
56 changes: 32 additions & 24 deletions src/reducers/dataReducer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { get } from 'lodash';
import { get, last, dropRight } from 'lodash';
import { setWith, assign } from 'lodash/fp';
import { actionTypes } from '../constants';
import { pathFromMeta, preserveValuesFromState } from '../utils/reducers';
import { getQueryName } from '../utils/query';
import { preserveValuesFromState, pathToArr } from '../utils/reducers';

const {
CLEAR_DATA,
Expand Down Expand Up @@ -37,42 +38,41 @@ export default function dataReducer(state = {}, action) {
if (!payload || payload.data === undefined) {
return state;
}
// Get doc from subcollections if they exist
const getDocName = data =>
data.subcollections
? getDocName(data.subcollections.slice(-1)[0]) // doc from last item of subcollections array
: data.doc; // doc from top level meta
const docName = getDocName(meta);
// Data to set to state is doc if doc name exists within meta
const data = docName ? get(payload.data, docName) : payload.data;
const queryName = getQueryName(meta, { onlySubcollections: true });
// Get previous data at path to check for existence
const previousData = get(state, meta.storeAs || pathFromMeta(meta));
const previousData = get(state, meta.storeAs || queryName);
if (meta.subcollections) {
const setPath =
queryName.split('/').length % 2
? getQueryName(meta)
: dropRight(pathToArr(queryName)).join('/');
// Set data to state immutabily (lodash/fp's setWith creates copy)
return setWith(Object, setPath, payload.data, state);
}
// Set data (without merging) if no previous data exists or if there are subcollections
if (!previousData || meta.subcollections) {
// Set data to state immutabily (lodash/fp's setWith creates copy)
return setWith(Object, meta.storeAs || pathFromMeta(meta), data, state);
return setWith(Object, meta.storeAs || queryName, payload.data, state);
}
// Otherwise merge with existing data
const mergedData = assign(previousData, data);
const mergedData = assign(previousData, payload.data);
// Set data to state (with merge) immutabily (lodash/fp's setWith creates copy)
return setWith(
Object,
meta.storeAs || pathFromMeta(meta),
mergedData,
state,
);
return setWith(Object, meta.storeAs || queryName, mergedData, state);
case DOCUMENT_MODIFIED:
case DOCUMENT_ADDED:
return setWith(
Object,
pathFromMeta(action.meta),
getQueryName(action.meta, { onlySubcollections: true }),
action.payload.data,
state,
);
case DOCUMENT_REMOVED:
case DELETE_SUCCESS:
const removePath = pathFromMeta(action.meta);
const cleanedState = setWith(Object, removePath, null, state);
const removePath = getQueryName(action.meta, {
onlySubcollections: true,
});
const id = last(pathToArr(getQueryName(action.meta)));
const cleanedState = setWith(Object, `${removePath}.${id}`, null, state);
if (action.preserve && action.preserve.data) {
return preserveValuesFromState(
state,
Expand All @@ -89,11 +89,19 @@ export default function dataReducer(state = {}, action) {
return {};
case LISTENER_ERROR:
// Set data to state immutabily (lodash/fp's setWith creates copy)
const nextState = setWith(Object, pathFromMeta(action.meta), null, state);
const nextState = setWith(
Object,
getQueryName(action.meta, { onlySubcollections: true }),
null,
state,
);
if (action.preserve && action.preserve.data) {
return preserveValuesFromState(state, action.preserve.data, nextState);
}
const existingState = get(state, pathFromMeta(action.meta));
const existingState = get(
state,
getQueryName(action.meta, { onlySubcollections: true }),
);
// If path contains data already, leave it as it is (other listeners
// could have placed it there)
if (existingState) {
Expand Down
Loading

0 comments on commit 7e3269e

Please sign in to comment.