├── .gitignore
├── README.md
├── babelRelayPlugin.js
├── data
└── schema.js
├── js
├── app.js
├── components
│ ├── Link.js
│ └── Main.js
└── mutations
│ └── CreateLinkMutation.js
├── package.json
├── public
├── favicon.ico
└── index.html
├── server.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bundle.js*
3 | data/schema.json
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## React GraphQL Relay Example
2 |
3 | A Relay.js example project backed by a GraphQL server on Express.js with data stored in MongoDB
4 |
5 | Contributions are welcome!
6 |
7 | ## Installing
8 |
9 | ```
10 | git clone https://github.com/RGRjs/react-graphql-relay-example.git rgrjs
11 | cd rgrjs
12 | npm install
13 | export MONGO_URL=....
14 | npm start
15 | npm run build
16 | ```
17 |
--------------------------------------------------------------------------------
/babelRelayPlugin.js:
--------------------------------------------------------------------------------
1 | var getBabelRelayPlugin = require('babel-relay-plugin');
2 |
3 | var schemaData = require('./data/schema.json').data;
4 |
5 | module.exports = getBabelRelayPlugin(schemaData);
6 |
--------------------------------------------------------------------------------
/data/schema.js:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLSchema,
3 | GraphQLObjectType,
4 | GraphQLList,
5 | GraphQLInt,
6 | GraphQLString,
7 | GraphQLNonNull,
8 | GraphQLID
9 | } from 'graphql';
10 |
11 | import {
12 | globalIdField,
13 | fromGlobalId,
14 | nodeDefinitions,
15 | connectionDefinitions,
16 | connectionArgs,
17 | connectionFromPromisedArray,
18 | mutationWithClientMutationId
19 | } from "graphql-relay";
20 |
21 | let Schema = (db) => {
22 | class Store {}
23 | let store = new Store();
24 |
25 | let nodeDefs = nodeDefinitions(
26 | (globalId) => {
27 | let {type} = fromGlobalId(globalId);
28 | if (type === 'Store') {
29 | return store
30 | }
31 | return null;
32 | },
33 | (obj) => {
34 | if (obj instanceof Store) {
35 | return storeType;
36 | }
37 | return null;
38 | }
39 | );
40 |
41 | let storeType = new GraphQLObjectType({
42 | name: 'Store',
43 | fields: () => ({
44 | id: globalIdField("Store"),
45 | linkConnection: {
46 | type: linkConnection.connectionType,
47 | args: {
48 | ...connectionArgs,
49 | query: { type: GraphQLString }
50 | },
51 | resolve: (_, args) => {
52 | let findParams = {};
53 | if (args.query) {
54 | findParams.title = new RegExp(args.query, 'i');
55 | }
56 | if (!args.limit || args.limit > 200) {
57 | args.limit = 100
58 | }
59 | return connectionFromPromisedArray(
60 | db.collection("links")
61 | .find(findParams)
62 | .sort({createdAt: -1})
63 | .limit(args.first).toArray(),
64 | args
65 | );
66 | }
67 | }
68 | }),
69 | interfaces: [nodeDefs.nodeInterface]
70 | });
71 |
72 | let linkType = new GraphQLObjectType({
73 | name: 'Link',
74 | fields: () => ({
75 | id: {
76 | type: new GraphQLNonNull(GraphQLID),
77 | resolve: (obj) => obj._id
78 | },
79 | title: { type: GraphQLString },
80 | url: { type: GraphQLString },
81 | createdAt: {
82 | type: GraphQLString,
83 | resolve: (obj) => new Date(obj.createdAt).toISOString()
84 | }
85 | })
86 | });
87 |
88 | let linkConnection = connectionDefinitions({
89 | name: 'Link',
90 | nodeType: linkType
91 | });
92 |
93 | let createLinkMutation = mutationWithClientMutationId({
94 | name: 'CreateLink',
95 |
96 | inputFields: {
97 | title: { type: new GraphQLNonNull(GraphQLString) },
98 | url: { type: new GraphQLNonNull(GraphQLString) },
99 | },
100 |
101 | outputFields: {
102 | linkEdge: {
103 | type: linkConnection.edgeType,
104 | resolve: (obj) => ({ node: obj.ops[0], cursor: obj.insertedId })
105 | },
106 | store: {
107 | type: storeType,
108 | resolve: () => store
109 | }
110 | },
111 |
112 | mutateAndGetPayload: ({title, url}) => {
113 | return db.collection("links").insertOne({
114 | title,
115 | url,
116 | createdAt: Date.now()
117 | });
118 | }
119 | });
120 |
121 | let schema = new GraphQLSchema({
122 | query: new GraphQLObjectType({
123 | name: 'Query',
124 | fields: () => ({
125 | node: nodeDefs.nodeField,
126 | store: {
127 | type: storeType,
128 | resolve: () => store
129 | }
130 | })
131 | }),
132 |
133 | mutation: new GraphQLObjectType({
134 | name: 'Mutation',
135 | fields: () => ({
136 | createLink: createLinkMutation
137 | })
138 | })
139 | });
140 |
141 | return schema
142 | };
143 |
144 | export default Schema;
145 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import Relay from "react-relay";
4 |
5 | import Main from "./components/Main";
6 |
7 | class HomeRoute extends Relay.Route {
8 | static routeName = 'Home';
9 | static queries = {
10 | store: (Component) => Relay.QL`
11 | query MainQuery {
12 | store { ${Component.getFragment('store') } }
13 | }
14 | `
15 | };
16 | }
17 |
18 | ReactDOM.render(
19 | ,
23 | document.getElementById('react')
24 | );
25 |
--------------------------------------------------------------------------------
/js/components/Link.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Relay from "react-relay";
3 | import moment from "moment";
4 |
5 | class Link extends React.Component {
6 | dateStyle = () => ({
7 | color: '#888',
8 | fontSize: '0.7em',
9 | marginRight: '0.5em'
10 | });
11 |
12 | urlStyle = () => ({
13 | color: '#062',
14 | fontSize: '0.85em'
15 | });
16 |
17 | dateLabel = () => {
18 | let {link, relay} = this.props;
19 | if (relay.hasOptimisticUpdate(link)) {
20 | return 'Saving...';
21 | }
22 | return moment(link.createdAt).format('L')
23 | };
24 |
25 | url = () => {
26 | return this.props.link.url.replace(/^https?:\/\/|\/$/ig,'');
27 | };
28 |
29 | render() {
30 | let {link} = this.props;
31 | return (
32 |
33 |
44 |
45 | );
46 | }
47 | }
48 |
49 | Link = Relay.createContainer(Link, {
50 | fragments: {
51 | link: () => Relay.QL`
52 | fragment on Link {
53 | url,
54 | title,
55 | createdAt,
56 | }
57 | `
58 | }
59 | });
60 |
61 | export default Link;
62 |
--------------------------------------------------------------------------------
/js/components/Main.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Relay from "react-relay";
3 | import {debounce} from "lodash";
4 |
5 | import Link from "./Link";
6 | import CreateLinkMutation from "../mutations/CreateLinkMutation";
7 |
8 | class Main extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.setVariables = debounce(this.props.relay.setVariables, 300);
12 | }
13 |
14 | search = (e) => {
15 | this.setVariables({ query: e.target.value });
16 | };
17 |
18 | setLimit = (e) => {
19 | this.setVariables({ limit: Number(e.target.value) });
20 | };
21 |
22 | handleSubmit = (e) => {
23 | e.preventDefault();
24 | let onSuccess = () => {
25 | $('#modal').closeModal();
26 | };
27 | let onFailure = (transaction) => {
28 | var error = transaction.getError() || new Error('Mutation failed.');
29 | console.error(error);
30 | };
31 | Relay.Store.commitUpdate(
32 | new CreateLinkMutation({
33 | title: this.refs.newTitle.value,
34 | url: this.refs.newUrl.value,
35 | store: this.props.store
36 | }),
37 | {onFailure, onSuccess}
38 | );
39 | this.refs.newTitle.value = "";
40 | this.refs.newUrl.value = "";
41 | };
42 |
43 | componentDidMount() {
44 | $('.modal-trigger').leanModal();
45 | }
46 |
47 | render() {
48 | let content = this.props.store.linkConnection.edges.map(edge => {
49 | return ;
50 | });
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
61 |
62 |
65 |
66 |
67 |
72 |
73 |
74 |
79 |
80 |
81 |
82 |
83 |
104 |
105 |
106 | );
107 | }
108 | }
109 |
110 | // Declare the data requirement for this component
111 | Main = Relay.createContainer(Main, {
112 | initialVariables: {
113 | limit: 100,
114 | query: ''
115 | },
116 | fragments: {
117 | store: () => Relay.QL`
118 | fragment on Store {
119 | id,
120 | linkConnection(first: $limit, query: $query) {
121 | edges {
122 | node {
123 | id,
124 | ${Link.getFragment('link')}
125 | }
126 | }
127 | }
128 | }
129 | `
130 | }
131 | });
132 |
133 | export default Main;
134 |
--------------------------------------------------------------------------------
/js/mutations/CreateLinkMutation.js:
--------------------------------------------------------------------------------
1 | import Relay from "react-relay";
2 |
3 | class CreateLinkMutation extends Relay.Mutation {
4 | getMutation() {
5 | return Relay.QL`
6 | mutation { createLink }
7 | `;
8 | }
9 |
10 | getVariables() {
11 | return {
12 | title: this.props.title,
13 | url: this.props.url
14 | }
15 | }
16 |
17 | getFatQuery() {
18 | return Relay.QL`
19 | fragment on CreateLinkPayload {
20 | linkEdge,
21 | store { linkConnection }
22 | }
23 | `;
24 | }
25 |
26 | getConfigs() {
27 | return [{
28 | type: 'RANGE_ADD',
29 | parentName: 'store',
30 | parentID: this.props.store.id,
31 | connectionName: 'linkConnection',
32 | edgeName: 'linkEdge',
33 | rangeBehaviors: {
34 | '': 'prepend',
35 | },
36 | }]
37 | }
38 |
39 | getOptimisticResponse() {
40 | return {
41 | linkEdge: {
42 | node: {
43 | title: this.props.title,
44 | url: this.props.url,
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
51 | export default CreateLinkMutation;
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rgrjs",
3 | "version": "1.0.0",
4 | "description": "A collection of educational resources about React, GraphQL, and Relay",
5 | "scripts": {
6 | "start": "./node_modules/.bin/babel-node --presets react,es2015,stage-0 server.js",
7 | "build": "webpack"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/rgrjs/react-graphql-relay-example.git"
12 | },
13 | "author": "",
14 | "license": "ISC",
15 | "bugs": {
16 | "url": "https://github.com/rgrjs/react-graphql-relay-example/issues"
17 | },
18 | "homepage": "https://github.com/rgrjs/react-graphql-relay-example#readme",
19 | "dependencies": {
20 | "babel": "^6.5.2",
21 | "babel-cli": "^6.5.1",
22 | "babel-core": "^6.5.2",
23 | "babel-loader": "^6.2.3",
24 | "babel-preset-es2015": "^6.5.0",
25 | "babel-preset-react": "^6.5.0",
26 | "babel-preset-stage-0": "^6.5.0",
27 | "babel-relay-plugin": "^0.7.1",
28 | "events": "^1.1.0",
29 | "express": "^4.13.4",
30 | "express-graphql": "^0.4.9",
31 | "flux": "^2.1.1",
32 | "graphql": "^0.4.18",
33 | "graphql-relay": "^0.3.6",
34 | "kerberos": "0.0.18",
35 | "lodash": "^4.5.0",
36 | "moment": "^2.11.2",
37 | "mongodb": "^2.1.7",
38 | "react": "^0.14.7",
39 | "react-dom": "^0.14.7",
40 | "react-relay": "^0.7.1",
41 | "webpack": "^1.12.13"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RGRjs/react-graphql-relay-example/4eff361c58048f023081de4ac60ec3c1544bd2e6/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Resources for React.js, GraphQL, and Relay
11 |
12 |
13 |
14 |
15 | React, GraphQL, and Relay
16 |
17 |
18 | Loading...
19 |
20 |
21 | - This site is built with Relay.js, GraphQL, and React.js.
22 | - Contributions are most welcome. Fork us
23 | - Watch how this site started from scratch in this Pluralsight course
24 |
25 |

26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import express from 'express';
3 | import Schema from './data/schema';
4 | import GraphQLHTTP from 'express-graphql';
5 | import {graphql} from 'graphql';
6 | import {introspectionQuery} from 'graphql/utilities';
7 |
8 | import {MongoClient} from 'mongodb';
9 |
10 | let app = express();
11 | app.use(express.static('public'));
12 |
13 | (async () => {
14 | try {
15 | let db = await MongoClient.connect(process.env.MONGO_URL);
16 | let schema = Schema(db);
17 |
18 | app.use('/graphql', GraphQLHTTP({
19 | schema,
20 | graphiql: true
21 | }));
22 |
23 | let server = app.listen(process.env.PORT || 3000, () => {
24 | console.log(`Listening on port ${server.address().port}`);
25 | });
26 |
27 | // Generate schema.json
28 | let json = await graphql(schema, introspectionQuery);
29 | fs.writeFile('./data/schema.json', JSON.stringify(json, null, 2), err => {
30 | if (err) throw err;
31 |
32 | console.log("JSON schema created");
33 | });
34 | } catch(e) {
35 | console.log(e);
36 | }
37 | })();
38 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: "./js/app.js",
5 | output: {
6 | path: path.resolve(__dirname, "public"),
7 | filename: "bundle.js"
8 | },
9 | module: {
10 | loaders: [
11 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader',
12 | query: {
13 | presets: ['react', 'es2015', 'stage-0'],
14 | plugins: [path.resolve(__dirname, 'babelRelayPlugin')]
15 | }
16 | }
17 | ]
18 | }
19 | };
20 |
--------------------------------------------------------------------------------