Willy Douglas
Looking for Sponsors
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
}
}Cache invalidation
When to invalidate?
Out of sync data
Optimistic updates?
Each case handled separately
Common patterns and tools for different needs
Wire up some middleware for async actions (only needed once)
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:
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:
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:
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:
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
}If you do this for every API call:
Relay (Facebook)
Tea = Relay.createContainer(Tea, {
fragments: {
tea: () => Relay.QL`
fragment on Tea {
name,
steepingTime,
}
`,
},
});Relay Example
GraphQL
class TodoList extends React.Component {
render() {
...
}
}
fetchData('/api/todos')(TodoList);https://github.com/heroku/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)gimmeData('/api/todos')(TodoList);With shared data cache and handling
Needs to do 2 things
{
[url]: {
status: FETCHING | CURRENT | FAILED | STALE,
data: urlData
}
}When data is mutated
{
urlInfo: {
[url]: {
status: FETCHING | CURRENT | FAILED | STALE,
model: modelType
ids: [modelIds]
},
},
normalizedCache: {
[modelType]: {
[id]: model
}
}
}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:
Benefits:
| 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 |
Should we reevaluate some of those existing frameworks?
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
query {
customer: getCustomer(id: 'id1') {
name
todos {
content
}
}
}Query
{
data {
customer: {
name: 'Frodo',
todos: [{
content: 'Destroy Ring'
}]
}
}
}Returns
const MyComponentWithData = graphql(MyQuery)(MyComponent);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)
Libraries:
Repo for this talk:
Learning:
Talks of note: