-
Notifications
You must be signed in to change notification settings - Fork 355
/
useLocal.tsx
137 lines (128 loc) · 3.75 KB
/
useLocal.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { useCallback, useEffect, useRef, useState } from "react";
import {
commitLocalUpdate,
CSelector,
GraphQLTaggedNode,
RecordProxy,
} from "relay-runtime";
import { OmitFragments } from "coral-framework/testHelpers/removeFragmentRefs";
import { DeepPartial } from "coral-framework/types";
import { useCoralContext } from "../bootstrap";
import { LOCAL_ID, LOCAL_TYPE } from "./localState";
/**
* AdvancedUpdater gives you full access to the Relay Record Proxy to update Local State.
*/
type AdvancedUpdater = (record: RecordProxy) => void;
/**
* LocalUpdater that allows the caller to update local state either
* using a simple object or the AdvancedUpdater.
*/
type LocalUpdater<T> = DeepPartial<T> | AdvancedUpdater;
function isAdvancedUpdater(t: LocalUpdater<any>): t is AdvancedUpdater {
return typeof t === "function";
}
/**
* applySimplified takes selections defined in a fragment, an object
* containing data changes and smartly applies it to the record proxy.
*
* @param record Record Proxy poing to Local Record
* @param selections Selections of the fragment
* @param data Data you want to set
*/
function applySimplified(
record: RecordProxy,
selections: ReadonlyArray<any>,
data: any
) {
const keys = Object.keys(data);
keys.forEach(k => {
const field = selections.find(s => s.alias === k || s.name === k);
if (!field) {
throw new Error(`Field '${k}' not found in selection`);
}
switch (field.kind) {
case "ScalarField":
record.setValue(data[k], field.name);
return;
case "LinkedField":
applySimplified(
record.getOrCreateLinkedRecord(field.name, field.concreteType),
field.selections,
data[k]
);
return;
default:
throw new Error(
`Simplified apply does not support field of kind ${field.kind}`
);
}
});
}
/**
* useLocal is a React Hook that allows you to subscribe to the Client Local State
* inside of the Relay Cache.
*
* Example:
* ```
* const [local, setLocal] = useLocal<Local>(graphql`
* fragment ProfileLocal on Local {
* profileTab
* }
* `);
* ```
*
* @param fragmentSpec graphql fragment
*/
function useLocal<T>(
fragmentSpec: GraphQLTaggedNode
): [OmitFragments<T>, (update: LocalUpdater<OmitFragments<T>>) => void] {
const fragment =
typeof fragmentSpec === "function"
? fragmentSpec().default
: (fragmentSpec as any).data().default;
if (fragment.kind !== "Fragment") {
throw new Error("Expected fragment");
}
if (fragment.type !== LOCAL_TYPE) {
throw new Error(`Type must be "Local" in "Fragment ${fragment.name}"`);
}
const selector: CSelector<any> = {
dataID: LOCAL_ID,
node: { selections: fragment.selections },
variables: {},
};
const { relayEnvironment } = useCoralContext();
const [local, setLocal] = useState<T>(
() => relayEnvironment.lookup(selector).data as T
);
const localUpdate = useCallback(
(update: LocalUpdater<T>) => {
commitLocalUpdate(relayEnvironment, store => {
const record = store.get(LOCAL_ID)!;
if (isAdvancedUpdater(update)) {
update(record);
} else {
applySimplified(record, fragment.selections[0].selections, update);
}
});
return;
},
[relayEnvironment, fragment]
);
const firstRun = useRef(true);
useEffect(() => {
const snapshot = relayEnvironment.lookup(selector);
const subscription = relayEnvironment.subscribe(snapshot, update =>
setLocal(update.data as T)
);
if (!firstRun) {
setLocal(snapshot.data as T);
}
firstRun.current = false;
return () => {
subscription.dispose();
};
}, [relayEnvironment, fragment]);
return [local, localUpdate];
}
export default useLocal;