Queries are the core data request interface for any Meetup Web Platform app. They are primarily generated by application Routes to indicate what data each one needs from the API when it is active. However, they can be used for any request to the API, and are therefore used for POST, PATCH, PUT and DELETE requests as well.
At a high level, you can think of a Query as a JSON-encoded API request, including all request parameters, the endpoint URI, and any metadata properties like custom headers
A Query is just a plain object with the following shape: A Query is just a plain object with the following shape:
{
ref: string,
endpoint: string,
list?: {
dynamicRef: string,
merge?: {
sort: (Object, Object) => number,
idTest: (Object, Object) => boolean,
}
},
params?: object,
type?: string, // DEPRECATED
meta?: {
flags?: string[],
method?: string
variants: {
[string]: string | number | string[] | number[], // e.g. { experiment1: chapterId }
},
metaRequestHeaders?: string[],
},
}
Example
{
ref: 'foobar',
endpoint: 'foo/bar',
params: {
memberId: 1234,
},
type: 'foo', // generally not needed
meta: {
flags: ['thisflag', 'thatflag'],
method: 'get', // generally not needed
variants: {
'my-member-experiment': '1234', // memberId - MUST BE STRING (e.g. `memberId.toString()`)
'my-group-experiment': '5678', // chapterId
},
metaRequestHeaders: ['unread-messages'],
},
}
A unique string reference to the query. The ref
is used to uniquely identify
the query and uniquely assign the resulting API data to Redux state at
state.api[ref]
.
The endpoint can either be a URL pathname that assumes the REST API domain
api.meetup.com, e.g. members/123456
will call https://api.meetup.com/members/123456,
or a fully-qualified URL that specifies an alternative domain, e.g. https://example.com/list
will be used as-is.
endpoint: 'members/123456'
and endpoint: 'https://api.meetup.com/members/123456'
are functionally equivalent
Note: this URL should not include any parameter placeholders
like /:urlname
- the values should be filled in as needed.
The parameters that should be passed to the API either in the querystring (for GET requests) or the request body (for POST requests).
A one-word description of the 'data type' expected to be returned by the API
for this query. This information is used on the server to further process the
data for certain data types like 'group'
objects, which will receive special
duotone photo URLs in addition to the standard photo URLs provided by the API.
The type
should be the same regardless of whether the returned data is an
array or a singleton.
Type examples
group
member
event
comment
- feature-specific objects like
home
,conversations
An array of feature flag (Runtime Flag) names that should be returned alongside the main request.
The metaRequestHeaders
property is in reference to X-Meta-Request-Headers
in the
meetup api. The response for each
header passed in will be in REF.meta
, converted from snake-case
to camelCase
.
You can force the query to be sent with a particular HTTP method by specifying
it here as a string: get
, post
, delete
, patch
or put
. Note that
you should never try to make a request that contains multiple queries with
different method
s.
Alternatively, you can use method-specific action creators to automatically assingn the method and make the request for individual requests.
You can request variant names for particular experiments with particular
contexts by populating meta.variants
with an object containing keys that are
experiment names and values that are string IDs (member ID or chapter ID
depending on the experiment).
A query response is also a plain object
{
ref: string,
value: any,
type?: string,
meta?: {
flags?: string[],
variants?: {
[string]: { // experiment
[string]: string // context: variant
},
},
},
}
If a query contains a meta.variants
request, the query response might not
contain a corresponding variants
response if the variants service fails -
you must test for the existence of meta.variants
before reading from it.
In general, queries start as the payload of an API_REQ
action, which will
generate responses that are applied to Redux state.
You should always use the action creators in apiActionCreators
to dispatch
API requests. Each action creator takes a single query or an array of queries
as its first argument, and an optional meta
argument
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
const getQuery = {
endpoint: 'ny-tech/members',
ref: 'newMember',
};
const getAction = api.get(getQuery);
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
const postQuery = {
endpoint: 'ny-tech/members',
ref: 'newMember',
params: { name, bio },
};
const postAction = api.post(postQuery);
const patchAction = api.patch(postQuery);
const putAction = api.put(postQuery);
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
const deleteQuery = {
endpoint: 'ny-tech/members/123456',
ref: 'deletedMember',
params: { id },
};
const deleteAction = api.del(deleteQuery); // note `api.del` not `api.delete` because `delete` is a keywork
Use Redux's store.dispatch
or bindActionCreators
to dispatch the query
actions.
An API request action object always has a type of API_REQ
, and it will always
have a meta
property that contains a request
property corresponding to the
current status of the request. The Promise will resolve on a successful API
request, and reject
on a 400/500 error from the fetch call.
This property can be accessed by the dispatch
caller by consuming the return
value of the dispatch()
call, which is the dispatched action itself. The
caller can then attach response handlers in action.meta.request.then()
Promise
callbacks.
Example using mapDispatchToProps
// Example.jsx
import { SubmissionError } from 'redux-form';
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
function mapDispatchToProps(dispatch) {
return bindActionCreators({ post: api.post }, dispatch);
}
class Example extends React.Component {
onSubmit() {
const formQuery = { ... };
const apiRequest = this.props.post(formQuery); // this triggers the POST, returns dispatched action object
const { request } = apiRequest.meta;
request.then(response => {
const formResponse = response;
// validate the form, throw `SubmissionError` as needed
})
}
}
When query actions are dispatched, the sync middleware will generate a
corresponding fetch
to the API proxy endpoint of the app server, e.g.
GET /mu_api?queries=[query, ...]
API response array returned as JSON from app server
The app server API proxy endpoint will respond with an array of Query responses.
[
{
ref: string,
value: {},
error?: {},
type?: string,
meta?: {}, // data returned from API separate from `value`
},
// ...
]
The fetchQueries function will then filter these Query Response objects into
separate successes
and errors
arrays, where each array element is an object
containing the original query
object and its corresponding response
:
{
successes: [{ query, response }, ...],
errors: [{ query, response }, ...],
}
The sync middleware will read these two arrays and generate a separate
API_RESP_SUCCESS
or API_RESP_ERROR
action for each response object.
If the fetch
fails entirely, no responses will be delivered to the
application. Instead, an API_RESP_FAIL
action will be dispatched with an
Error
payload.
After all Query Responses have been dispatched, the sync middleware will
dispatch a final API_RESP_COMPLETE
action.
When API_REQ
is first dispatched, the platform api
reducer will add each
Query's ref
to an inFlight
array. This property can be inspected to
determine whether a particular ref is 'in-flight', e.g.
mapStateToProps(state) {
return {
isLoading: state.api.inFlight.includes(myRef),
};
}
When the API responds, the platform reducer will read the API_RESP_SUCCESS
and API_RESP_ERROR
values into state.api[ref]
for each response and clear
the corresponding ref
from state.api.inFlight
.
Redux state after being processed by API_RESP_SUCCESS
action
{
[ref]: {
ref,
type?,
value: {},
meta?: {}
},
// ...
}
Redux state after being processed by API_RESP_ERROR
action
{
[ref]: {
ref,
type?,
error: error,
meta?: {}
},
// ...
}
If API_RESP_FAIL
is dispatched, state.api
will not receive new data,
but will instead populate state.fail
with an Error
object describing the
the failed call.
Redux state after being processed by API_RESP_FAIL
action
{
fail: Error,
// ... all other data is stale but technically still valid
}
// see description for docs about object argument
({ params, isExact, url, path, location }, state) => Query;
One of the primary uses for queries is to load route-specific data from the API. The application will automatically generate these queries by calling particular 'query creator' functions that are assigned to application routes, producing a fully-qualified 'query' object from the routing state input.
Route query creator functions have two requirements:
- They are assigned as props of React Router
route
s .Thequery
prop can be either a single query creator function or an array of functions - They are pure functions that take two arguments:
- object constructed from the
match
object from React Router, which includesparams
extracted from the URL) with an additionallocation
property corresponding to the current React Routerlocation
. - current Redux
state
, including feature flag values
- object constructed from the
More info in the mwp-router docs.
export const GROUP_REF = 'group';
function groupQuery({ params, location }) {
const { urlname } = params;
return {
ref: GROUP_REF,
type: 'group',
endpoint: `/${urlname}`,
params: {
fields: ['event_sample'],
},
};
}
// applied to a route:
const groupRoute = {
path: '/:urlname',
query: groupQuery, // or [groupQuery, ...<other queries>]
component: GroupContainer,
};
On page load, the application will automatically collect any query objects
generated by the query functions you assign to your routes. However, if you need
to make another GET request, you can manually dispatch an API request using the
get
action creator from
apiActionCreators
.
// Example.jsx
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
function mapDispatchToProps(dispatch) {
return bindActionCreators({ get: api.get }, dispatch);
}
class Example extends React.Component {
componentDidMount() {
const lazyQuery = {
endpoint: `${this.props.match.params.urlname}/more/stuff`,
ref: 'moreStuff',
params: { foo: 'bar' },
};
this.props.get(lazyQuery);
}
}
// Example.jsx
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
const NEW_STUFF_REF = 'newStuff';
function mapStateToProps(state) {
// when the POST returns, the response will be accessible in Redux state,
// populated by an `API_RESP_SUCCESS` or `API_RESP_ERROR` action
return {
NEW_STUFF_REF: state.api[NEW_STUFF_REF],
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ post: api.post }, dispatch);
}
class Example extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps[NEW_STUFF_REF] !== this.props[NEW_STUFF_REF]) {
// the POST returned _something_ - maybe an error
// you probably want to call `this.setState` or something here
}
}
onSubmit(e) {
e.preventDefault(); // prevent full-page submit
const postQuery = {
endpoint: `${this.props.match.params.urlname}/new/stuff`,
ref: NEW_STUFF_REF,
params: this.state.formValues, // this would be set by controlled inputs in the form
};
this.props.post(postQuery);
}
render() {
return <form onSubmit={this.onSubmit}>...</form>;
}
}
API POST/PATCH/PUT endpoints that support file uploads have one additional
constraint because the file data cannot be easily JSON-serialized like params
in other query objects.
The standard way of encoding form data that includes file uploads is to assemble
the entire form contents into a FormData
instance, which
allows the file contents to be passed around the application as a Blob
that
can be encoded for transmission.
To form a Query with form data, simply pass a FormData
instance as the
params
property
// Example.jsx
import * as api from 'meetup-web-platform/lib/actions/apiActionCreators';
const NEW_FILE_STUFF = 'newFileStuff';
function mapStateToProps(state) {
// when the POST returns, the response will be accessible in Redux state,
// populated by an `API_RESP_SUCCESS` or `API_RESP_ERROR` action
return {
NEW_FILE_STUFF: state.api[NEW_FILE_STUFF],
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ post: api.post }, dispatch);
}
class Example extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps[NEW_FILE_STUFF] !== this.props[NEW_FILE_STUFF]) {
// the POST returned _something_ - maybe an error
// you probably want to call `this.setState` or something here
}
}
onSubmit(e) {
e.preventDefault(); // prevent full-page submit
const postQuery = {
endpoint: `${this.props.match.params.urlname}/new/stuff`,
ref: NEW_FILE_STUFF,
params: new FormData(this.form), // one stop form encoding - forces 'multipart/form-data' content type
};
this.props.post(postQuery);
}
render() {
return (
<form onSubmit={this.onSubmit} ref={el => (this.form = el)}>
...
</form>
);
}
}
In practice, a PATCH request is just a POST by another name - use the post
action creator from apiActionCreators
.
The difference between PUT and POST is that PUT is idempotent: calling it once or several times successively has the same effect.
In practice, a DELETE request is just a GET by another name - use the del
action creator from apiActionCreators
.
The response will generally be a '204 - No Content', so you'll have to read the 'updated' value in Redux state a little more carefully.
The variants service provides its own public API endpoint returning JSON. See the variants service README and the OpenAPI spec.
To use it in production, set the endpoint
to
https://variant.data.meetuphq.io/variant/v2/${experimentId}/${entityId}
where expirementId
is the name of your experiment in the variants service,
and entityId
is a member ID, chapter ID, or group urlname
, depending
on the type of experiment.
In dev, use the variantNoEnrollment
version of the endpoint (documented in the OpenAPI spec)
in order to prevent the experiment data from showing up in site analytics (Looker), i.e.
https://variant.data.meetuphq.io/variantNoEnrollment/v2/${experimentId}/${entityId}
import { getProperty } from '@meetup/api-state-selectors';
const MY_VARIANT_REF = 'my-cool-experiment-variant';
const variantEndpoint =
process.env.NODE_ENV === 'production' ? 'variant' : 'variantNoEnrollment';
const myVariantQuery = {
endpoint: `https://variant.data.meetuphq.io/${variantEndpoint}/v2/my-cool-experiment/${member.id}`,
ref: MY_VARIANT_REF,
};
const myVariantSelector = state => {
const response = state.api[MY_VARIANT_REF] || {};
// get the assigned variant as a string, default to ''
return getProperty(response, 'variant', '');
};