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
  • 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 Declarative APIs

By wdoug

React Declarative APIs

  • 693