Skip to content

Commit b97d64a

Browse files
committed
initial
0 parents  commit b97d64a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+8130
-0
lines changed

Diff for: .babelrc

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"plugins": [
3+
"@babel/syntax-object-rest-spread",
4+
"@babel/plugin-proposal-pipeline-operator",
5+
[
6+
"module-resolver",
7+
{
8+
"extensions": [".js", ".jsx", ".ts", ".tsx"],
9+
"root": ["."]
10+
}
11+
]
12+
],
13+
"presets": [
14+
"@babel/typescript",
15+
"@babel/react",
16+
"@babel/stage-3",
17+
[
18+
"@babel/env",
19+
{
20+
"targets": {
21+
"browsers": "last 2 Chrome versions"
22+
}
23+
}
24+
]
25+
]
26+
}

Diff for: .eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lib

Diff for: .eslintrc

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"extends": "formidable/configurations/es6-react",
3+
"parser": "typescript-eslint-parser",
4+
"globals": { "__dirname": true },
5+
"rules": {
6+
"quotes": [2, "single", { "allowTemplateLiterals": true }],
7+
"comma-dangle": [
8+
"error",
9+
{
10+
"arrays": "always-multiline",
11+
"objects": "always-multiline",
12+
"imports": "always-multiline",
13+
"exports": "always-multiline",
14+
"functions": "ignore"
15+
}
16+
],
17+
"no-magic-numbers": "off",
18+
"func-style": "off",
19+
"arrow-parens": "off",
20+
"no-use-before-define": "off",
21+
"react/jsx-filename-extension": "off",
22+
"react/require-extension": "off",
23+
"react/no-multi-comp": "warn",
24+
"react/prop-types": "warn",
25+
"react/sort-comp": "warn",
26+
"react/sort-prop-types": "warn"
27+
}
28+
}

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

Diff for: .vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "./node_modules/typescript/lib"
3+
}

Diff for: README.md

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# urql
2+
3+
Universal React Query Library
4+
5+
![Urkel](https://media.giphy.com/media/Dc60NIUs12QvK/giphy.gif)
6+
7+
## What is `urql`
8+
9+
`urql` is a GraphQL client, exposed as a set of ReactJS components.
10+
11+
## Why this exists
12+
13+
In my experience, existing solutions have been a bit heavy on the API side of things, and I see people getting discouraged or turned away from the magic that is GraphQL. This library aims to make GraphQL on the client side as simple as possible.
14+
15+
## How its different
16+
17+
### React
18+
19+
`urql` is specifically for React. There have been no efforts made to abstract the core in order to work with other libaries. Usage with React was a priority from the get go, and it has been architected as such.
20+
21+
### Render Props
22+
23+
`urql` exposes its API via render props. Recent discussion has shown render props to be an extraordinarily flexible and appropriate API decision for libraries targeting React.
24+
25+
### Caching
26+
27+
`urql` takes a unique approach to caching. Many existing solutions normalize your data and parse your queries to try to invalidate cached data. I am not smart enough to implement this solution, and further, normalizing everything, on big datasets, can potentially lead to performance/memory issues.
28+
29+
`urql` takes a different approach. It takes your query signature and creates a hash, which it uses to cache the results of your query. It also adds `__typename` fields to both queries and mutations, and by default, will invalidate a cached query if it contains a type changed by a mutation. Further, handing control back to the users, it exposes a `shouldInvalidate` prop, which is a function that can be used to determine whether the cache is invalid based upon typenames, mutation response and your current data.
30+
31+
## Install
32+
33+
`npm install urql --save`
34+
35+
## Getting Started
36+
37+
The core of `urql` is three exports, `Provider`, `Connect` and `Client`. To get started, you simply create a `Client` instance, pass it to a `Provider` and then wrap any components you want to make queries or fire mutation from with a `Connect` component.
38+
39+
Lets look at a root level component and how you can get it set up:
40+
41+
```jsx
42+
import React from 'react';
43+
import ReactDOM from 'react-dom';
44+
45+
import { Provider, Client } from '../src/index';
46+
import Home from './home';
47+
48+
const client = new Client({
49+
url: 'http://localhost:3001/graphql',
50+
});
51+
52+
export const App = () => (
53+
<Provider client={client}>
54+
<Home />
55+
</Provider>
56+
);
57+
58+
ReactDOM.render(<App />, document.getElementById('root'));
59+
```
60+
61+
As you can see above, all that's required to get started is the `url` field on `Client` which tells us where your GraphQL API lives. After the client is created, and passed to the `Provider` that wraps your app, now you can wrap any component down in the tree with a `Connect` to start issuing queries.
62+
63+
Queries and mutations both have creation functions, which you can import. An `urql` `Connect` component can take multiple queries, and multiple mutations. The `render` prop exposes the internal logic to any component you`d like to provide it to.
64+
65+
Lets start by defining a query and a mutation:
66+
67+
```javascript
68+
const TodoQuery = `
69+
query {
70+
todos {
71+
id
72+
text
73+
}
74+
}
75+
`;
76+
```
77+
78+
## HOLD UP FAM THIS IS IMPORTANT
79+
80+
It is absolutely necessary if you want this library to work properly, to create a valid mutation response. If you change a todo, return it. If you delete a todo, return it. If you add a todo, return it. If you don't return the thing that changed and file an issue, I'm going to screenshot this paragraph, paste it into the issue, and then drop my finger from a 3ft height onto the close button while making plane crash sounds.
81+
82+
```javascript
83+
const AddTodo = `
84+
mutation($text: String!) {
85+
addTodo(text: $text) {
86+
id
87+
text
88+
}
89+
}
90+
`;
91+
```
92+
93+
Now we can use the `mutation` and `query` functions to format them in the way `urql` expects.
94+
95+
```javascript
96+
const Home = () => (
97+
<Connect
98+
query={query(TodoQuery)}
99+
mutation={{
100+
addTodo: mutation(AddTodo),
101+
}}
102+
render={({ loaded, fetching, refetch, data, error, addTodo }) => {
103+
//...Your Component
104+
}}
105+
/>
106+
);
107+
```
108+
109+
The render prop sends a couple of fields back by default:
110+
111+
* `loaded` - This is like `loading` but its false by default, and becomes true after the first time your query loads. This makes initial loading states easy and reduces flicker on subsequent fetch/refetches.
112+
* `fetching` - This is what you might commonly think of as `loading`. Any time a query or mutation is taking place, this puppy equals true, resolving to false when complete.
113+
* `refetch` - This is a method that you can use to manually refetch your query, skipping and repopulating the cache.
114+
* `data` - This is where your data lives. Once the query returns, This would look like `{ todos: [...] }`.
115+
* `error` - If there is an error returned when making the query, instead of data, you get this and you can handle it or show a `refetch` button or cry or whatever you wanna do.
116+
117+
Also, any mutations, because they are named, are also passed into this render prop.
118+
119+
As you can see above, the `query` accepts either a single query, or an array of queries. The `mutation` prop accepts an object, with the mutation names as keys.
120+
121+
So why do we use these `query` and `mutation` functions before passing them? Variables, thats why. If you wanted to pass a query with variables, you would construct it like so:
122+
123+
```javascript
124+
query(TodoQuery, { myVariable: 5 });
125+
```
126+
127+
Similarly, you can pass variables to your mutation. Mutation, however is a bit different, in the sense that it returns a function that you can call with a variable set:
128+
129+
```javascript
130+
mutation(AddTodo); // No initial variables
131+
132+
// After you pass 'addTodo' from the render prop to a component:
133+
134+
addTodo({ text: `I'm a variable!` });
135+
```
136+
137+
## Cache control
138+
139+
Normally in `urql`, the cache is aggressively invalidated based upon `__typename`, but if you want finer grained control over your cache, you can use the `shouldInvalidate` prop. It is a function, that returns boolean, much like `shouldComponentUpdate`, which you can use to determine whether your data needs a refresh from the server. It gets called after every mutation:
140+
141+
```javascript
142+
const MyComponent = () => (
143+
<Connect
144+
query={query(MyQuery)}
145+
shouldInvalidate={(changedTypenames, typenames, mutationResponse, data) => {
146+
return data.todos.some(d => d.id === mutationResponse.id);
147+
}}
148+
render={({ loaded, fetching, refetch, data, error, addTodo }) => {
149+
//...Your Component
150+
}}
151+
/>
152+
);
153+
```
154+
155+
The signature of `shouldComponentUpdate` is basically:
156+
157+
* `changedTypenames` - The typenames returned from the mutation. ex: `['Todo']`
158+
* `typenames` - The typenames that are included in your `Connect` component. ex: `['Todo', 'User', 'Author']`
159+
* `response` - The actual data returned from the mutation. ex: `{ id: 123 }`
160+
* `data` - The data that is local to your `Connect` component as a result of a query. ex: `{ todos: [] }`
161+
162+
Using all or some of these arguments can give you the power to pretty accurately describe whether your connection has now been invalidated.
163+
164+
## API
165+
166+
## TODO
167+
168+
* [ ] Server Side Rendering
169+
* [ ] Client HoC
170+
* [ ] Client Side GraphQL
171+
* [ ] Tests
172+
* [ ] Fix Lint
173+
174+
## Prior Art
175+
176+
### Apollo

Diff for: custom-typings/create-react-context.d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
declare module 'create-react-context' {
2+
import * as React from 'react';
3+
4+
export default function createReactContext<T>(defaultValue: T): Context<T>;
5+
6+
export type Context<T> = {
7+
Provider: React.ComponentClass<ProviderProps<T>>;
8+
Consumer: React.ComponentClass<ConsumerProps<T>>;
9+
};
10+
11+
export type ProviderProps<T> = {
12+
value: T;
13+
children: React.ReactNode;
14+
};
15+
16+
export type ConsumerProps<T> = {
17+
children: (value: T) => React.ReactNode;
18+
};
19+
}

Diff for: custom-typings/graphql-tag.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
declare module 'graphql-tag' {
2+
import { DocumentNode } from 'graphql';
3+
export default function gql(
4+
literals: TemplateStringsArray,
5+
...placeholders: Array<string>
6+
): DocumentNode;
7+
}

Diff for: demo/home.tsx

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { Connect, query, mutation } from '../src/index';
3+
import TodoList from './todo-list';
4+
import TodoForm from './todo-form';
5+
import Loading from './loading';
6+
7+
const Home = () => (
8+
<Connect
9+
query={query(TodoQuery)}
10+
mutation={{
11+
addTodo: mutation(AddTodo),
12+
removeTodo: mutation(RemoveTodo),
13+
}}
14+
render={({ loaded, data, addTodo, removeTodo }) => {
15+
return (
16+
<div>
17+
{!loaded ? (
18+
<Loading />
19+
) : (
20+
<TodoList todos={data.todos} removeTodo={removeTodo} />
21+
)}
22+
<TodoForm addTodo={addTodo} />
23+
</div>
24+
);
25+
}}
26+
/>
27+
);
28+
29+
const AddTodo = `
30+
mutation($text: String!) {
31+
addTodo(text: $text) {
32+
id
33+
text
34+
}
35+
}
36+
`;
37+
38+
const RemoveTodo = `
39+
mutation($id: ID!) {
40+
removeTodo(id: $id) {
41+
id
42+
}
43+
}
44+
`;
45+
46+
const TodoQuery = `
47+
query {
48+
todos {
49+
id
50+
text
51+
}
52+
user {
53+
name
54+
}
55+
}
56+
`;
57+
58+
export default Home;

Diff for: demo/index.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
4+
import { Provider, Client } from '../src/index';
5+
import Home from './home';
6+
7+
const client = new Client({
8+
url: 'http://localhost:3001/graphql',
9+
});
10+
11+
export const App = () => (
12+
<Provider client={client}>
13+
<Home />
14+
</Provider>
15+
);
16+
17+
ReactDOM.render(<App />, document.getElementById('root'));

Diff for: demo/loading.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react';
2+
3+
const Loading = () => <p>Loading...</p>;
4+
5+
export default Loading;

Diff for: demo/todo-form.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
3+
type TodoFormProps = {
4+
addTodo: (text: object) => void;
5+
};
6+
7+
class TodoForm extends React.Component<TodoFormProps> {
8+
input: HTMLInputElement;
9+
addTodo = () => {
10+
this.props.addTodo({ text: this.input.value });
11+
this.input.value = '';
12+
};
13+
render() {
14+
return (
15+
<div>
16+
<input
17+
type="text"
18+
ref={i => {
19+
this.input = i;
20+
}}
21+
/>
22+
<button type="button" onClick={this.addTodo}>
23+
Add Todo
24+
</button>
25+
</div>
26+
);
27+
}
28+
}
29+
30+
export default TodoForm;

0 commit comments

Comments
 (0)