React Declarative APIs
Willy Douglas
CA Technologies
Code for Denver

Looking for Sponsors
Why
- One of the most common things we do with React
- Not clear for new people
- Some common issues
- hacked together
- Lots of boilerplate
A Simple (Naive) Option
Component State
Component State
class TodoList extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
fetch('api/todos')
.then(getJsonAndHandleErrorsEtc)
.then(todos => {
this.setState({ todos });
});
}
render() {
// use this.state.todos
}
}What about mutating data?
The Problem
-
Cache invalidation
-
When to invalidate?
-
Out of sync data
-
-
Optimistic updates?
-
Each case handled separately
Redux
Redux is great for handling mutating data
Common patterns and tools for different needs
What about getting the data in the first place?
Idiomatic Redux Fetch Example
Wire up some middleware for async actions (only needed once)
Idiomatic Redux Fetch Example
export const LOAD_EVENTS_FAILURE = 'LOAD_EVENTS_FAILURE';
export const LOAD_EVENTS_SUCCESS = 'LOAD_EVENTS_SUCCESS';
export const LOAD_EVENTS_REQUEST = 'LOAD_EVENTS_REQUEST';In a constants file:
Idiomatic Redux Fetch Example
import {
LOAD_EVENTS_FAILURE,
LOAD_EVENTS_SUCCESS,
LOAD_EVENTS_REQUEST
} from '../constants/reduxConstants';
import { createAction, createErrorAction } from '../utils/actionHelpers';
import { get } from '../utils/apiHelpers';
const loadEventsStarted = createAction(LOAD_EVENTS_REQUEST);
const loadEventsSucceeded = createAction(LOAD_EVENTS_SUCCESS);
const loadEventsFailed = createErrorAction(LOAD_EVENTS_FAILURE);
export function loadEvents() {
return (dispatch, getState) => {
dispatch(loadEventsStarted);
get('/api/events')
.then(events => {
dispatch(loadEventsSucceeded(events))
})
.catch(error => {
dispatch(loadEventsFailed(error);
});
};
}In an actions file:
Idiomatic Redux Fetch Example
import {
LOAD_EVENTS_REQUEST,
LOAD_EVENTS_SUCCESS
} from '../constants/reduxConstants';
const initialState = { isFetching: false };
const eventManager = (state = initialState, action) => {
switch (action.type) {
case LOAD_EVENTS_REQUEST:
return {
...state,
isFetching: true
};
case LOAD_EVENTS_SUCCESS:
return {
...state,
events: action.payload,
isFetching: false
};
default:
return state;
}
};
export default eventManager;
export const getEvents = (state) => state.eventsReducerKey.events;
export const eventsAreFetching = (state) => state.eventsReducerKey.isFetching;In a reducer file:
Idiomatic Redux Fetch Example
import { connect } from 'react-redux';
import { loadEvents } from '../actions';
import Events from '../components/Events';
import { getEvents, eventsAreFetching } from '../reducers/EventsReducer';
const mapStateToProps = (state) => {
return {
loading: eventsAreFetching(state),
events: getEvents(state)
};
};
const mapDispatchToProps = {
loadEvents
};
export default connect(mapStateToProps, mapDispatchToProps)(Events);In a container file:
Idiomatic Redux Fetch Example
class Events extends React.Component {
...
componentWillMount() {
this.props.loadEvents();
}
...
// use this.props.events
}In the actual component:
Then you have to actually kick off the fetch
All Together
// In an Actions file:
import {
LOAD_EVENTS_FAILURE,
LOAD_EVENTS_SUCCESS,
LOAD_EVENTS_REQUEST
} from '../constants/reduxConstants';
import { createAction, createErrorAction } from '../utils/actionHelpers';
import { get } from '../utils/apiHelpers';
const loadEventsStarted = createAction(LOAD_EVENTS_REQUEST);
const loadEventsSucceeded = createAction(LOAD_EVENTS_SUCCESS);
const loadEventsFailed = createErrorAction(LOAD_EVENTS_FAILURE);
export function loadEvents() {
return (dispatch, getState) => {
dispatch(loadEventsStarted);
get('/api/events')
.then(events => {
dispatch(loadEventsSucceeded(events))
})
.catch(error => {
dispatch(loadEventsFailed(error);
});
};
}
// In a Reducer file:
import {
LOAD_EVENTS_REQUEST,
LOAD_EVENTS_SUCCESS
} from '../constants/reduxConstants';
const initialState = { isFetching: false };
const eventManager = (state = initialState, action) => {
switch (action.type) {
case LOAD_EVENTS_REQUEST:
return {
...state,
isFetching: true
};
case LOAD_EVENTS_SUCCESS:
return {
...state,
events: action.payload,
isFetching: false
};
default:
return state;
}
};
export default eventManager;
export const getEvents = (state) => state.eventsReducerKey.events;
export const eventsAreFetching = (state) => state.eventsReducerKey.isFetching;// In a Constants file:
export const LOAD_EVENTS_FAILURE = 'LOAD_EVENTS_FAILURE';
export const LOAD_EVENTS_SUCCESS = 'LOAD_EVENTS_SUCCESS';
export const LOAD_EVENTS_REQUEST = 'LOAD_EVENTS_REQUEST';
// In a Container file:
import { connect } from 'react-redux';
import { loadEvents } from '../actions';
import Events from '../components/Events';
import { getEvents, eventsAreFetching } from '../reducers/EventsReducer';
const mapStateToProps = (state) => {
return {
loading: eventsAreFetching(state),
events: getEvents(state)
};
};
const mapDispatchToProps = {
loadEvents
};
export default connect(mapStateToProps, mapDispatchToProps)(Events);
// In an actual Component:
class Events extends React.Component {
...
componentWillMount() {
this.props.loadEvents();
}
...
// use this.props.events
}That feels like a lot
If you do this for every API call:
- Lots of Boilerplate
- Each case is its own snowflake
- Hard to optimize
- Potentially duplicating data
A new way
-
Relay (Facebook)
- Falcor (Netflix)
Tea = Relay.createContainer(Tea, {
fragments: {
tea: () => Relay.QL`
fragment on Tea {
name,
steepingTime,
}
`,
},
});Relay Example
Benefits
- Declarative
- Low boilerplate/overhead from client
- Batched calls
- Decoupling client and server
- Data needs described adjacent to data use
Relay
-
GraphQL
Falcor
- JSON Graph
Both require a new API
What if you don’t want to / can’t update your API?
- Could we still have a similar interface?
- Could we get some of the benefits of this approach?
- Could we create something that would be an easy transition to one of these technologies?
Let's go back to Component State briefly
Higher Order Component?
class TodoList extends React.Component {
render() {
...
}
}
fetchData('/api/todos')(TodoList);There's a library for that
React Refetch
https://github.com/heroku/react-refetch
React Refetch
import { connect } from 'react-refetch'
class Profile extends Component {
render() {
...
}
}
export default connect(props => ({
userFetch: `/users/${props.userId}`,
likesFetch: `/users/${props.userId}/likes`
}))(Profile)If all you need is to display read-only data
This can be a great option
That Higher Order Component can do more internally
Goal
gimmeData('/api/todos')(TodoList);With shared data cache and handling
How
Higher Order Component
Needs to do 2 things
- Kick off fetching data and storing in cache
- React Side Effect
- Dispatch Redux fetch action
- React Side Effect
- Getting data from cache and passing to component
- Redux connect
Cache - Simple
{
[url]: {
status: FETCHING | CURRENT | FAILED | STALE,
data: urlData
}
}When data is mutated
- Can pessimistically set all data to stale
- Refetch everything
Cache - Normalized
{
urlInfo: {
[url]: {
status: FETCHING | CURRENT | FAILED | STALE,
model: modelType
ids: [modelIds]
},
},
normalizedCache: {
[modelType]: {
[id]: model
}
}
}Normalized Data
customer: {
id: 1,
name: 'Frodo',
followers: [{
id: 2
name: 'Sam'
}]
}customer: {
1: {
id: 1,
name: 'Frodo',
followers: [2]
},
2: {
id: 2,
name: 'Sam'
}
}Would become something like:
Normalized Data Is Great
Benefits:
- No duplicated data
- No inconsistent data
- Ideal for
- handling real-time data
- optimistic updates
Some things the HOC could do
| Issue | Potential Solution |
|---|---|
| fetch status (e.g. loading) | Store statuses |
| Duplicate data | Normalize and dedupe data |
| Handling complex / unknown data | Fall back to denormalized storage or handle independently |
| Numerous API calls | Batch calls |
| Components use computed data | Provide consumers with a way to compute data |
| Mutations / Updates | |
|---|---|
| Stale data | Hook into mutations / realtime |
| Immediate feedback | Optimistic updates |
| Out of sync with server | Queue Requests / CRDTs |
Wait, is this just a new framework?
Should we reevaluate some of those existing frameworks?
GraphQL API
type Todo {
content: String!
}
type Customer {
id: ID!,
name: String,
todos: [Todo]!
}
type Query {
getCustomer(id: ID!): Customer
}class Customer {
constructor(c) {
this.id = c.id;
this.name = c.name;
}
todos() {
return db.getTodos();
}
}
const rootValue = {
getCustomer(args) {
return db.getCustomer(args.id)
.then(c => new Customer(c));
}
};Schema
Resolvers
server.use('/graphql', expressGraphql({
buildSchema(schemaString),
rootValue,
graphiql: true,
}));Provide to Server
GraphQL Query
query {
customer: getCustomer(id: 'id1') {
name
todos {
content
}
}
}Query
{
data {
customer: {
name: 'Frodo',
todos: [{
content: 'Destroy Ring'
}]
}
}
}Returns
GraphQL Client
const MyComponentWithData = graphql(MyQuery)(MyComponent);Relay is being rewritten
Apollo Client
Summary
Ultimately, it is easy to switch between any of these Higher Order Component variations
You don't need a different API to have simple, declarative, co-located queries
Just displaying read-only data?
Use React Re-Fetch
You can use a higher order component to handle requests
If you want some of the other benefits of something like GraphQL, check out Apollo Client (or wait for Relay 2)
Resources
Libraries:
- React Refetch
- normalizr (normalizing data)
- React Side Effect
- Relay (wait for Relay 2)
- Falcor (not much investment)
- Apollo Client
Repo for this talk:
Learning:
Talks of note:
React Declarative APIs
By wdoug
React Declarative APIs
- 693