/adrenaline

Simple Relay alternative

Primary LanguageJavaScriptMIT LicenseMIT

Adrenaline

build status npm version npm downloads

This library provides a subset of Relay's behaviour with a cleaner API.

Why?

Relay is a great framework with exciting ideas behind it. The downside is that in order to get all the cool features, you need to deal with a complex API. Relay provides you a lot of tricky optimizations which probably are more suitable for huge projects. In small, medium and even large ones you would prefer to have better developer experience while working with a simple, minimalistic set of APIs.

Adrenaline intends to provide you Relay-like ability to describe your components with declarative data requirements, while keeping the API as simple as possible. You are free to use it with different libraries like Redux, React Router, etc.

When not to use it?

  • You have a huge project and highly need tricky optimisations to reduce client-server traffic.
  • When you don't understand why you should prefer Adrenaline to Relay.

Installation

npm install --save adrenaline

Adrenaline requires React 15.0 or later.

Adrenaline uses fetch under the hood so you need to install the polyfill by yourself.

npm install --save whatwg-fetch

and then import it at the very top of your entry JavaScript file:

import 'whatwg-fetch';
import React from 'react';
import ReactDOM from 'react-dom';
import { Adrenaline } from 'adrenaline';

import App from './components/App';

ReactDOM.render(
  <Adrenaline>
    <App />
  </Adrenaline>,
  document.getElementById('root')
)

API

Adrenaline follows the idea of Presentational and Container Components

<Adrenaline endpoint />

Root of your application should be wrapped with Adrenaline component. This component is a provider component which injects some helpful stuff into your React application.

prop name type default/required purpose
endpoint string "/graphql" URI of your GraphQL endpoint

container({ variables, query })(Component)

In Adrenaline, you create container components mostly for your route handlers. The purpose of containers is to collect data requirements from presentation components in a single GraphQL query. They also behave like view controllers and are able to speak to the outside world using mutations.

key type default/required purpose
variables (props: Props) => Object () => ({}) describe query variables as a pure function of props
query string required your GraphQL query for this container
import React, { Component, PropTypes } from 'react';
import { container } from 'adrenaline';
import TodoList from './TodoList';

class UserItem extends Component {
  static propTypes = {
    viewer: PropTypes.object.isRequired,
  }
  /* ... */
}

export default container({
  variables: (props) => ({
    id: props.userId,
  }),
  query: `
    query ($id: ID!) {
      viewer(id: $id) {
        id,
        name,
        ${TodoList.getFragment('todos')}
      }
    }
  `,
})(UserItem);

Containers may also pass 2 additional properties to your component: mutate and isFetching.

  • mutate({ mutation: String, variables: Object = {}, invalidate: boolean = true }): Promise: You need to use this function in order to perform mutations. invalidate argument means you need to resolve data declarations after mutation.
  • isFetching: boolean: This property helps you understand if your component is in the middle of resolving data.

presenter({ fragments })(Component)

As in the presentational components idea, all your dumb components may be declared as simple React components. But if you want to declare your data requirements in a way similar to Relay, you can use the presenter() higher-order component.

import React, { Component } from 'react';
import { presenter } from 'adrenaline';

class TodoList extends Component {
  /* ... */
}

export default presenter({
  fragments: {
    todos: `
      fragment on User {
        todos {
          id,
          text
        }
      }
    `,
  },
})(TodoList);

Using ES7 decorators

Adrenaline works as higher-order components, so you can decorate your container components using ES7 decorators

import { container } from 'adrenaline'

@container({
  variables: (props) => ({
    id: props.userId,
  }),
  query: `
    query ($id: ID!) {
      viewer(id: $id) {
        id,
        name,
        ${TodoList.getFragment('todos')}
      }
    }
  `
})
export default class extends Component {
  static propTypes = {
    viewer: PropTypes.object.isRequired,
  }
  /* ... */
}

Mutations

You can declare your mutations as simple as:

const createTodo = `
  mutation ($text: String, $owner: ID) {
    createTodo(text: $text, owner: $owner) {
      id,
      text,
      owner {
        id
      }
    }
  }
`;

Then you can use this mutation with your component:

import React, { Component, PropTypes } from 'react';
import { createSmartComponent } from 'adrenaline';

class UserItem extends Component {
  static propTypes = {
    viewer: PropTypes.object.isRequired,
  }

  onSomeButtonClick() {
    this.props.mutate({
      mutation: createTodo,
      variables: {
        text: hello,
        owner: this.props.viewer.id
      },
    });
  }

  render() {
    /* render some stuff */
  }
}

Testing

There is a common problem I've discovered so far while developing applications. When you change the GraphQL schema, you'd like to know which particular subtrees in your applications need to be fixed. And you probably do not want to check this by running your application and going through it by hand.

For this case, Adrenaline provides you helper utilities for integration testing (currently for expect only). You can use toBeValidAgainst for checking your components' data requirements against your schema with GraphQL validation mechanism.

import expect from 'expect';
import TestUtils from 'adrenaline/lib/test';

import schema from 'path/to/schema';
// TodoApp is a container component
import TodoApp from 'path/to/TodoApp';

expect.extend(TestUtils.expect);

describe('Queries regression', () => {
  it('for TodoApp', () => {
    expect(TodoApp).toBeValidAgainst(schema);
  });
});

Image