All Projects → mixcloud → relay-helpers

mixcloud / relay-helpers

Licence: MIT license
Helpers to simplify and enhance Relay (https://facebook.github.io/relay/)

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to relay-helpers

Graphql.js
A Simple and Isomorphic GraphQL Client for JavaScript
Stars: ✭ 2,206 (+11510.53%)
Mutual labels:  relay
lancet
A comprehensive, efficient, and reusable util function library of go.
Stars: ✭ 2,228 (+11626.32%)
Mutual labels:  utils
EuNet
Peer to peer network solution for multiplayer games.
Stars: ✭ 109 (+473.68%)
Mutual labels:  relay
Django Graphql Auth
Django registration and authentication with GraphQL.
Stars: ✭ 200 (+952.63%)
Mutual labels:  relay
Isomorphic Relay
Adds server side rendering support to React Relay
Stars: ✭ 247 (+1200%)
Mutual labels:  relay
fat ecto
Query mechanism for Ecto
Stars: ✭ 20 (+5.26%)
Mutual labels:  utils
Reactnavigationrelaymodern
React Navigation integration with Relay
Stars: ✭ 170 (+794.74%)
Mutual labels:  relay
graphql-compose-relay
No description or website provided.
Stars: ✭ 29 (+52.63%)
Mutual labels:  relay
dt-utils
前端常用工具函数
Stars: ✭ 23 (+21.05%)
Mutual labels:  utils
btc-relay-solidity
Bitcoin Light Client on Ethereum
Stars: ✭ 30 (+57.89%)
Mutual labels:  relay
Relay Compiler Language Typescript
A language plugin for Relay that adds TypeScript support, including emitting type definitions.
Stars: ✭ 201 (+957.89%)
Mutual labels:  relay
Awesome Relay
Awesome resources for Relay
Stars: ✭ 246 (+1194.74%)
Mutual labels:  relay
PowerUp
⚡ Decompilation Tools and High Productivity Utilities ⚡
Stars: ✭ 1,526 (+7931.58%)
Mutual labels:  utils
Relay Workshop
Material for my Relay Workshop
Stars: ✭ 197 (+936.84%)
Mutual labels:  relay
XinFramework
Android 快速开发框架 总结以往开发结合三方项目 不断更新
Stars: ✭ 21 (+10.53%)
Mutual labels:  utils
Ws Tcp Relay
A simple relay between WebSocket clients and TCP servers
Stars: ✭ 186 (+878.95%)
Mutual labels:  relay
BaseToolsLibrary
Android通用适配器和常用的工具类
Stars: ✭ 24 (+26.32%)
Mutual labels:  utils
react-relay-example
Example project how to use React, Relay and TypeScript
Stars: ✭ 27 (+42.11%)
Mutual labels:  relay
universal-react-relay-starter-kit
A starter kit for React in combination with Relay including a GraphQL server, server side rendering, code splitting, i18n, SEO.
Stars: ✭ 14 (-26.32%)
Mutual labels:  relay
fileutils
Golang file system utils such as copy files and directories
Stars: ✭ 19 (+0%)
Mutual labels:  utils

Relay Helpers

Build status

Helpers to simplify and enhance Relay (https://facebook.github.io/relay/) including:

  • Decorators to simplify the Renderer and Mutation APIs
  • TTL on queries
  • Simple way to reset the Relay store
  • Simple server side rendering

and more!

Why?

We're waiting for Relay 2 but in the meantime we wanted to fill some gaps in the features and tidy up the API a bit.

We're hiring!

If you like working with React and GraphQL and you're interested in building the future of radio check out https://www.mixcloud.com/jobs/

Installation

npm install relay-helpers

RelayEnvProvider

RelayEnvProvider is a component that provides the Relay Environment on context. It is used by everything else in relay-helpers.

import Relay from 'react-relay/classic';
import {RelayEnvProvider} from 'relay-helpers';


const env = new Relay.Environment();


class App extends React.Component {
    render() {
        return (
            <RelayEnvProvider initialEnv={env}>
                {/* ... the rest of your app ... */}
            </RelayEnvProvider>
        );
    }
}

Resetting the store

Under some circumstances you will want to reset the entire store (e.g. log in or log out). The context provided by RelayEnvProvider has a reset() method:

import Relay from 'react-relay/classic';
import {RelayEnvProvider, RelayEnvContextType} from 'relay-helpers';


class LogoutButton extends React.Component {
    static contextTypes = {relayEnv: RelayEnvContextType};
    
    onClick = () => {
        // Log out...
        
        // This will create a new relay environment and refresh any renderers on the page that were created using
        // withRelayRenderer or withRelayQuery
        this.context.relayEnv.reset();
    };
    
    render() {
        return <div onClick={this.onClick}>Logout</div>;
    }
}


class App extends React.Component {
    render() {
        // Pass a `createEnv` prop to RelayEnvProvider that will be called on init and when `reset()` is called
        return (
            <RelayEnvProvider createEnv={() => new Relay.Environment()}>
                <LogoutButton />
            </RelayEnvProvider>
        );
    }
}

Higher Order Components

createRelayContainer

A simple wrapper for Relay.createContainer so that it can be used like other higher order components and with tools such as recompose.

e.g.

import {createRelayContainer} from 'relay-helpers';
import {connect} from 'react-redux';
import compose from 'recompose/compose';


const MyContainer = createRelayContainer({fragments: {...}})(MyComponent);


const MyWrappedComponent = compose(
    createRelayContainer({
        initialVariables: {count: 10},
        fragments: {
            user: () => Relay.QL`...`
        }
    }),
    connect(),
    // ... other higher order components
)(MyComponent);

withRelayRenderer

withRelayRenderer can be used to simplify the process of creating a Relay.Renderer. Instead of needing a Renderer and a Route you can just decorate the component. In most circumstances you can probably use withRelayQuery - this is provided as a lower level alternative.

Note: withRelayQuery or withRelayRenderer must be used if you want to make use of context.relayEnv.reset().

import {withRelayRenderer} from 'relay-helpers';


function Username({loading, error, offline, user}) {
    // Note: unlike Relay.Renderer, withRelayRenderer will render your component
    // with `loading`, `error`, and `offline` props.
    // If this is not the behaviour you want a higher order component that only renders if !loading && !error
    if (loading) {
        return <div>Loading...</div>;
    }
    
    // `offline` is true if error is a TypeError with message "Network request failed"
    // the error is still passed in as `error`
    if (offline) {
        return <div>Check your internet connection...</div>;
    }
    
    if (error) {
        return <div>Something went wrong...</div>;
    }
    
    // Data props passed is as usual
    return <div>{user.username}</div>;
}


Username = compose(
    // withRelayRenderer wraps a Relay Container
    withRelayRenderer({
        // like a Relay Route
        queryConfig: {
            name: 'UsernameQuery',
            queries: {
                user: () => Relay.QL`
                    query {
                        user(id: $userId)
                    }
                `
            },
            // params will be taken from props
            // PropTypes are generated to ensure required params aren't missed
            params: {
                userId: {required: true}
            }
        },
        // forceFetch on mount if we haven't seen a success from this query in the past hour (optional)
        ttl: 1000 * 60 * 60 * 1,
        // always forceFetch this component (optional)
        forceFetch: true
    }),
    createRelayContainer({
        fragments: {
            user: () => Relay.QL`
                fragment on User {
                    username
                }
            `
        }
    })
)(Username);
    
    
function App() {
    // params are taken from props
    return <Username userId="user1" />;
}

Changing the query based on props

Instead of providing a queryConfig object you can provide a function:

queryConfig: (props, Container) => {
    return {
        name: 'UsernameQuery',
        queries: {
            user: () => Relay.QL`
                query {
                    user(id: $userId)
                }
            `
        },
        params: {
            userId: props.myUserIdParam
        }
    };
}

This will be called every time props changes.

withRelayQuery

withRelayQuery is a higher level higher order component that enables you to create a component with a query very quickly.

import {withRelayQuery} from 'relay-helpers';

function Username({loading, ...as before

// Note: createContainer is not required
Username = withRelayQuery({
    query: Relay.QL`
        query {
            user(id: $userId) {
                username
            }
        }
    `,
    params: {
        userId: {required: true}
    }
})(Username);

If there is more than one query:

const UserWithTodo = withRelayQuery({
    query: [
        Relay.QL`
            query {
                user(id: $userId) {
                    username
                }
            }
        `,
        Relay.QL`
            query {
                todo(id: $todoId) {
                    name
                }
            }
        `
    ],
    params: {
        userId: {required: true},
        todoId: {required: true}
    }
})(Username);

Options

  • query EITHER a Relay.QL of a full query (not separated into queries and fragments) OR an array of Relay.QL (if you have more than one root)
  • name (optional) name the query - by default the component name will be used
  • params EITHER param definitions OR a function: (props) => ({paramName: 'value', ...})
  • initialVariables as you would provide to Relay.createContainer
  • withHelpers optionally wrap in the withRelayHelpers higher order component
  • forceFetch and ttl (same as withRelayRenderer)

How it works

withRelayQuery splits the Relay.QL into the parts required for the route and fragments. This might not work in all circumstances.

Note about fragment variables

It is possible to have variables that aren't params (in the same way as you can with separate Route and Container) and it is possible to set those variable values from props. For example:

const Wrapper = withRelayQuery({
    query: Relay.QL`
        query {
            user(id: $userId) {
                todos(limit: $limit) {
                    title
                }
            }
        }
    `,
    params: ({userId}) => ({userId, limit: 5}),  // Or params: {userId: {required: true}}
    initialVariables: {limit: null}  // Note: this is still necessary
})(TestComponent);

withRelayHelpers

Provides setVariables and forceFetch props equivalent to props.relay.setVariables and props.relay.forceFetch that return promises instead of taking callbacks.

import {withRelayHelpers} from 'relay-helpers';

function MyComponent({setVariables, relay}) {
    function onClick() {
        setVariables({count: relay.variables.count}).then(() => {
            // Done
        });
    }
    return <div onClick={onClick}>Load more</div>;
}

MyComponent = compose(
    createRelayContainer(...),
    withRelayHelpers()    
)(MyComponent);

Mutations

context.relayEnv provides a mutate function that wraps Relay.GraphQLMutation:

context.relayEnv.mutate({
    query: Relay.QL`
        mutation {
            ...
        }
    `,
    variables: {...},
    files: {...},
    optimisticResponse: {...},
    configs: [...]
}).then(() => {
    // success!
});

withRelayMutations

If you define mutation functions like so:

const myMutation = mutate => (arg1, arg2) => mutate({
    query: Relay.QL`...`
    ... other relayEnv.mutate options
});

You can use withRelayMutations to provide the mutation as a prop to the component:

import {withRelayMutations} from 'relay-helpers';


function MyComponent({myMutation}) {
    function onClick() {
        myMutation('arg1', 'arg2').then(() => {
            // success!
        });
    }
    
    return <div onClick={onClick}>Click Me!</div>;
}

// myMutation from above
MyComponent = withRelayMutations({myMutation})(MyComponent);

Server side (isomorphic) rendering

Server and Client Relay Environments are provided that, combined with RelayEnvProvider and withRelayRenderer/withRelayQuery, can enable isomorphic rendering.

Note: you can provide an `isomorphic: false` option to `withRelayRenderer` and `withRelayQuery` if you do not want them to be included in server side rendering

On the server:

import {ServerEnvironment} from 'relay-helpers';

// Provide a function to get graphql query responses
// getQueryResponse = (query: {query: string, variables: Object}) => Promise<{errors?: Array<Object>, data?: Object}>

const env = new ServerEnvironment(getQueryResponse);

function MyApp() {
    return (
        <RelayEnvProvider initialEnv={env}>
            {/* the rest of the app */}
        </RelayEnvProvider>
    );
}

// This will recursively find queries and run them
env.isomorphicGetData(<MyApp />).then((markup) => {
    // markup = ReactDOMServer.renderToString(<MyApp />);
    
    // query data is ready
    const relayData = env.isomorphicClientData;
    
    // return the markup to the user and include relayData somehow
    // (probably JSON.strinfigy and put in a hidden DOM element)
});

On the client:

import {ClientEnvironment} from 'relay-helpers';


const env = new ClientEnvironment();

// env.injectNetworkLayer ... etc

function MyApp() {
    return (
        <RelayEnvProvider initialEnv={env}>
            {/* the rest of the app */}
        </RelayEnvProvider>
    );
}


const serverData = ...  // Get the data from the server (probably from the hidden DOM element)

env.isomorphicInjectServerData(serverData).then(() => {
    // Data has been injected into Relay store
    ReactDOM.render(<MyApp />, ...);
});

Caching

querySubscriberDecorator

querySubscriberDecorator is a helper that can be used, for example, to create an offline cache for Relay.

Usage:

var env = new Relay.Environment();

// A simple cache example - this could be persisted to localStorage, for example
var cache = {};


// We listen for all queries
env.addNetworkSubscriber(querySubscriberDecorator((queryName, variables, result) => {
    // The helper converts Relay's QueryRequest into something a bit more useful
    
    // Selectively choose what you want to cache
    if (queryName === 'UserProfile') {
        const cacheKey = `${queryName}|${JSON.stringify(variables)}`;
        cache[cacheKey] = result;
    }
}));

The result that querySubscriberDecorator passes in is compatible with the ServerData used for isomorphic rendering. They can be injected into the store in the same way (whether you are using server side rendering or not):

var env = new ClientEnvironment();

// "cache" from the code above - for example loaded from localStorage 
const localResults = Object.keys(cache).map(key => cache[key]);

// Concatenating serverData and localResults - just pass localResults if you're not using isomorphic rendering
env.isomorphicInjectServerData([...serverData, ...localResults]).then(() => {
    // render...
});

Tests

Some helpers are provided for testing.

Relay.Renderer mock

// Either
import {MockRelayRenderer} from 'relay-helpers/lib/tests/mocks';
// or
import {Relay as MockedRelay} from 'relay-helpers/lib/tests/mocks';

Usage (example using jest, but jest isn't required):

import Relay from 'react-relay/classic';

jest.mock('react-relay/classic', () => {
    return require('relay-helpers/lib/tests/mocks').Relay;
});

describe('test', () => {
    it('should render loading state', () => {
        Relay.Renderer.nextRenderResult = {};
        // render a component that has a relay renderer in it
    });
    
    it('should render ready state', () => {
        Relay.Renderer.nextRenderResult = {
            props: {
                user: {...}
            }
        };
        // render a component that has a relay renderer in it
    });
});

createMockRelayEnv

This requires jest but will provide a mocked relayEnv context.

expect(relayEnvContext).toHaveMutated

Also requires jest.

const relayEnv = createMockRelayEnv();
expect(relayEnv).not.toHaveMutated();

const wrapper = shallow(<MyComponent />, {context: {relayEnv}});

// ... do something that causes a mutation such as:
relayEnv.mutate({
    query: {name: 'MyQueryName'},
    variables: {my: 'variable'}
});

expect(relayEnv).toHaveMutated();
expect(relayEnv).toHaveMutated('MyQueryName', {my: 'variable'});
expect(relayEnv).toHaveMutated('MyQueryName', {my: jasmine.any(String)});
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].