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 "reduxRootSelector" parameter to the ApolloClient contructor #631

Closed
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ delianides <[email protected]>
greenkeeperio-bot <[email protected]>
hammadj <[email protected]>
matt debergalis <[email protected]>
Vladimir Guguiev <[email protected]>
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Expect active development and potentially significant breaking changes in the `0

- Options set in middleware can override the fetch query in the network layer. [Issue #627](https://github.com/apollostack/apollo-client/issues/627) and [PR #628](https://github.com/apollostack/apollo-client/pull/628).
- Make `returnPartialData` work better with fragments. [PR #580](https://github.com/apollostack/apollo-client/pull/580)
- Add "reduxRootSelector" parameter to the ApolloClient constructor and deprecate "reduxRootKey". [PR #631](https://github.com/apollostack/apollo-client/pull/631)

### v0.4.14

Expand Down
13 changes: 7 additions & 6 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {

import {
ApolloQueryResult,
ApolloStateSelector,
} from './index';

import {
Expand Down Expand Up @@ -113,7 +114,7 @@ export class QueryManager {
public store: ApolloStore;

private networkInterface: NetworkInterface;
private reduxRootKey: string;
private reduxRootSelector: ApolloStateSelector;
private queryTransformer: QueryTransformer;
private queryListeners: { [queryId: string]: QueryListener };

Expand Down Expand Up @@ -150,14 +151,14 @@ export class QueryManager {
constructor({
networkInterface,
store,
reduxRootKey,
reduxRootSelector,
queryTransformer,
shouldBatch = false,
batchInterval = 10,
}: {
networkInterface: NetworkInterface,
store: ApolloStore,
reduxRootKey: string,
reduxRootSelector: ApolloStateSelector,
queryTransformer?: QueryTransformer,
shouldBatch?: Boolean,
batchInterval?: number,
Expand All @@ -166,7 +167,7 @@ export class QueryManager {
// is that the network interface?
this.networkInterface = networkInterface;
this.store = store;
this.reduxRootKey = reduxRootKey;
this.reduxRootSelector = reduxRootSelector;
this.queryTransformer = queryTransformer;
this.pollingTimers = {};
this.batchInterval = batchInterval;
Expand Down Expand Up @@ -429,7 +430,7 @@ export class QueryManager {
};

public getApolloState(): Store {
return this.store.getState()[this.reduxRootKey];
return this.reduxRootSelector(this.store.getState());
}

public getDataWithOptimisticResults(): NormalizedCache {
Expand Down Expand Up @@ -748,7 +749,7 @@ export class QueryManager {
} {
const { missingSelectionSets, result } = diffSelectionSetAgainstStore({
selectionSet: queryDef.selectionSet,
store: this.store.getState()[this.reduxRootKey].data,
store: this.reduxRootSelector(this.store.getState()).data,
throwOnMissingField: false,
rootId,
variables,
Expand Down
86 changes: 72 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ApolloStore,
createApolloReducer,
ApolloReducerConfig,
Store,
} from './store';

import {
Expand Down Expand Up @@ -81,6 +82,7 @@ import {
import isUndefined = require('lodash.isundefined');
import assign = require('lodash.assign');
import flatten = require('lodash.flatten');
import isString = require('lodash.isstring');

// We expose the print method from GraphQL so that people that implement
// custom network interfaces can turn query ASTs into query strings as needed.
Expand All @@ -105,6 +107,16 @@ export type ApolloQueryResult = {
// Those are thrown via the standard promise/observer catch mechanism.
}

/**
* This type defines a "selector" function that receives state from the Redux store
* and returns the part of it that is managed by ApolloClient
* @param state State of a Redux store
* @returns {Store} Part of state managed by ApolloClient
*/
export type ApolloStateSelector = (state: any) => Store;

const DEFAULT_REDUX_ROOT_KEY = 'apollo';

// A map going from the name of a fragment to that fragment's definition.
// The point is to keep track of fragments that exist and print a warning if we encounter two
// fragments that have the same name, i.e. the values *should* be of arrays of length 1.
Expand Down Expand Up @@ -163,6 +175,10 @@ export function clearFragmentDefinitions() {
fragmentDefinitionsMap = {};
}

function defaultReduxRootSelector(state: any) {
return state[DEFAULT_REDUX_ROOT_KEY];
}

/**
* This is the primary Apollo Client class. It is used to send GraphQL documents (i.e. queries
* and mutations) to a GraphQL spec-compliant server over a {@link NetworkInterface} instance,
Expand All @@ -172,7 +188,7 @@ export function clearFragmentDefinitions() {
export default class ApolloClient {
public networkInterface: NetworkInterface;
public store: ApolloStore;
public reduxRootKey: string;
public reduxRootSelector: ApolloStateSelector | null;
public initialState: any;
public queryManager: QueryManager;
public reducerConfig: ApolloReducerConfig;
Expand All @@ -189,8 +205,13 @@ export default class ApolloClient {
* @param networkInterface The {@link NetworkInterface} over which GraphQL documents will be sent
* to a GraphQL spec-compliant server.
*
* @param reduxRootKey The root key within the Redux store in which data fetched from the server
* will be stored.
* @deprecated please use "reduxRootSelector" instead
* @param reduxRootKey The root key within the Redux store in which data fetched from the server.
* will be stored. This option should only be used if the store is created outside of the client.
*
* @param reduxRootSelector Either a "selector" function that receives state from the Redux store
* and returns the part of it that is managed by ApolloClient or a key that points to that state.
* This option should only be used if the store is created outside of the client.
*
* @param initialState The initial state assigned to the store.
*
Expand Down Expand Up @@ -225,6 +246,7 @@ export default class ApolloClient {
constructor({
networkInterface,
reduxRootKey,
reduxRootSelector,
initialState,
dataIdFromObject,
queryTransformer,
Expand All @@ -236,6 +258,7 @@ export default class ApolloClient {
}: {
networkInterface?: NetworkInterface,
reduxRootKey?: string,
reduxRootSelector?: string | ApolloStateSelector,
initialState?: any,
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
Expand All @@ -245,7 +268,28 @@ export default class ApolloClient {
mutationBehaviorReducers?: MutationBehaviorReducerMap,
batchInterval?: number,
} = {}) {
this.reduxRootKey = reduxRootKey ? reduxRootKey : 'apollo';
if (reduxRootKey && reduxRootSelector) {
throw new Error('Both "reduxRootKey" and "reduxRootSelector" are configured, but only one of two is allowed.');
}

if (reduxRootKey) {
console.warn(
'"reduxRootKey" option is deprecated and might be removed in the upcoming versions, ' +
'please use the "reduxRootSelector" instead.'
);
}

if (!reduxRootSelector && reduxRootKey) {
this.reduxRootSelector = (state: any) => state[reduxRootKey];
} else if (isString(reduxRootSelector)) {
this.reduxRootSelector = (state: any) => state[reduxRootSelector as string];
} else if (typeof reduxRootSelector === 'function') {
this.reduxRootSelector = reduxRootSelector;
} else {
// we need to know that reduxRootSelector wasn't provided by the user
this.reduxRootSelector = null;
}

this.initialState = initialState ? initialState : {};
this.networkInterface = networkInterface ? networkInterface :
createNetworkInterface('/graphql');
Expand Down Expand Up @@ -339,25 +383,25 @@ export default class ApolloClient {
*
* It takes options as an object with the following keys and values:
*
* @param mutation A GraphQL document, often created with `gql` from the `graphql-tag` package,
* @param options.mutation A GraphQL document, often created with `gql` from the `graphql-tag` package,
* that contains a single mutation inside of it.
*
* @param variables An object that maps from the name of a variable as used in the mutation
* @param options.variables An object that maps from the name of a variable as used in the mutation
* GraphQL document to that variable's value.
*
* @param fragments A list of fragments as returned by {@link createFragment}. These fragments
* @param options.fragments A list of fragments as returned by {@link createFragment}. These fragments
* can be referenced from within the GraphQL mutation document.
*
* @param optimisticResponse An object that represents the result of this mutation that will be
* @param options.optimisticResponse An object that represents the result of this mutation that will be
* optimistically stored before the server has actually returned a result. This is most often
* used for optimistic UI, where we want to be able to see the result of a mutation immediately,
* and update the UI later if any errors appear.
*
* @param updateQueries A {@link MutationQueryReducersMap}, which is map from query names to
* @param options.updateQueries A {@link MutationQueryReducersMap}, which is map from query names to
* mutation query reducers. Briefly, this map defines how to incorporate the results of the
* mutation into the results of queries that are currently being watched by your application.
*
* @param refetchQueries A list of query names which will be refetched once this mutation has
* @param options.refetchQueries A list of query names which will be refetched once this mutation has
* returned. This is often used if you have a set of queries which may be affected by a mutation
* and will have to update. Rather than writing a mutation query reducer (i.e. `updateQueries`)
* for this, you can simply refetch the queries that will be affected and achieve a consistent
Expand Down Expand Up @@ -409,9 +453,18 @@ export default class ApolloClient {
return;
}

if (this.reduxRootSelector) {
throw new Error(
'Cannot initialize the store because "reduxRootSelector" or "reduxRootKey" is provided. ' +
'They should only be used when the store is created outside of the client. ' +
'This may lead to unexpected results when querying the store internally. ' +
`Please remove that option from ApolloClient constructor.`
);
}

// If we don't have a store already, initialize a default one
this.setStore(createApolloStore({
reduxRootKey: this.reduxRootKey,
reduxRootKey: DEFAULT_REDUX_ROOT_KEY,
initialState: this.initialState,
config: this.reducerConfig,
}));
Expand All @@ -422,16 +475,21 @@ export default class ApolloClient {
};

private setStore(store: ApolloStore) {
const reduxRootSelector = (this.reduxRootSelector) ? this.reduxRootSelector : defaultReduxRootSelector;

// ensure existing store has apolloReducer
if (isUndefined(store.getState()[this.reduxRootKey])) {
throw new Error(`Existing store does not use apolloReducer for ${this.reduxRootKey}`);
if (isUndefined(reduxRootSelector(store.getState()))) {
throw new Error(
'Existing store does not use apolloReducer. Please make sure the store ' +
'is properly configured and "reduxRootSelector" is correctly specified.'
);
}

this.store = store;

this.queryManager = new QueryManager({
networkInterface: this.networkInterface,
reduxRootKey: this.reduxRootKey,
reduxRootSelector: reduxRootSelector,
store,
queryTransformer: this.queryTransformer,
shouldBatch: this.shouldBatch,
Expand Down
19 changes: 12 additions & 7 deletions test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from 'graphql';

import ApolloClient, {
ApolloStateSelector,
ApolloQueryResult,
} from '../src/index';

Expand Down Expand Up @@ -78,26 +79,29 @@ describe('QueryManager', () => {
return undefined;
};

const defaultReduxRootSelector = (state: any) => state.apollo;

// Helper method that serves as the constructor method for
// QueryManager but has defaults that make sense for these
// tests.
const createQueryManager = ({
networkInterface,
store,
reduxRootKey,
reduxRootSelector,
queryTransformer,
shouldBatch,
}: {
networkInterface?: NetworkInterface,
store?: ApolloStore,
reduxRootKey?: string,
reduxRootSelector?: ApolloStateSelector,
queryTransformer?: QueryTransformer,
shouldBatch?: boolean,
}) => {

return new QueryManager({
networkInterface: networkInterface || mockNetworkInterface(),
store: store || createApolloStore(),
reduxRootKey: reduxRootKey || 'apollo',
reduxRootSelector: reduxRootSelector || defaultReduxRootSelector,
queryTransformer,
shouldBatch,
});
Expand All @@ -109,7 +113,7 @@ describe('QueryManager', () => {
return new QueryManager({
networkInterface: mockNetworkInterface(...mockedResponses),
store: createApolloStore(),
reduxRootKey: 'apollo',
reduxRootSelector: defaultReduxRootSelector,
});
};

Expand Down Expand Up @@ -1096,6 +1100,7 @@ describe('QueryManager', () => {
};

const reduxRootKey = 'test';
const reduxRootSelector = (state: any) => state[reduxRootKey];
const store = createApolloStore({
reduxRootKey,
config: { dataIdFromObject: getIdField },
Expand All @@ -1108,7 +1113,7 @@ describe('QueryManager', () => {
}
),
store,
reduxRootKey,
reduxRootSelector,
});

return queryManager.mutate({
Expand All @@ -1117,7 +1122,7 @@ describe('QueryManager', () => {
assert.deepEqual(result.data, data);

// Make sure we updated the store with the new data
assert.deepEqual(store.getState()[reduxRootKey].data['5'], { id: '5', isPrivate: true });
assert.deepEqual(reduxRootSelector(store.getState()).data['5'], { id: '5', isPrivate: true });
});
});

Expand Down Expand Up @@ -3113,7 +3118,7 @@ function testDiffing(
store: createApolloStore({
config: { dataIdFromObject: getIdField },
}),
reduxRootKey: 'apollo',
reduxRootSelector: (state) => state.apollo,
});

const steps = queryArray.map(({ query, fullResponse, variables }) => {
Expand Down
Loading