Slide 1

Slide 1 text

build a GraphQL client for the browser why the hell did I

Slide 2

Slide 2 text

build a GraphQL client for the browser why the hell did I

Slide 3

Slide 3 text

lead engineering manager chief shitpost of fi cer @ swan Matthias Le Brun @bloodyowl

Slide 4

Slide 4 text

our product is a GraphQL API* * and some frontend interfaces I promise my job is real

Slide 5

Slide 5 text

→ dashboard → auth → banking* → onboarding* * open-source our apps

Slide 6

Slide 6 text

consume GraphQL APIs our apps

Slide 7

Slide 7 text

need a GraphQL client our apps

Slide 8

Slide 8 text

but existing clients … → designed for a single schema → caching requires introspection → caching issues anyway → impact on bundle size → hard to extend/contribute

Slide 9

Slide 9 text

«how hard can it be to build a GQL client from scratch?» — me, clueless about how hard it can be to build a GQL client from scratch

Slide 10

Slide 10 text

«how hard can it be to build a GQL client from scratch?» — me, clueless about how hard it can be to build a GQL client from scratch

Slide 11

Slide 11 text

1. the API 2. the engine 3. the extras

Slide 12

Slide 12 text

current state of the ecosystem API const UserProfile = () => { const { loading, data, error } = useQuery(query, {}); // ... };

Slide 13

Slide 13 text

error handling in GraphQL ambiguities { "status": null } + Create your fi rst status Error loading status

Slide 14

Slide 14 text

error handling in GraphQL ambiguities if you want to guarantee that the UI is consistent with the data, query success is all or nothing

Slide 15

Slide 15 text

API structure if (error) { return ; } return ;

Slide 16

Slide 16 text

API @swan-io/boxed type Option = Some | None; type Result = Ok | Error; type AsyncData = | NotAsked | Loading | Done; open-source!

Slide 17

Slide 17 text

usage API const MyComponent = () => { const [data] = useQuery(query, {}); return match(data) .with(AsyncData.P.NotAsked, AsyncData.P.Loading, () => ) .with(AsyncData.P.Done(Result.P.Error(P.select())), (error) => ( )) .with(AsyncData.P.Done(Result.P.Ok(P.select())), (data) => { // show your data }) .exhaustive(); };

Slide 18

Slide 18 text

usage API const UserPage = ({ userId }: Props) => { const [updateUsername, usernameUpdate] = useMutation(updateUsernameMutation); const [username, setUsername] = useState(""); const onSubmit = (event) => { event.preventDefault(); updateUsername({ userId, username }); }; const isLoading = usernameUpdate.isLoading(); // ... };

Slide 19

Slide 19 text

advantages API → no possible mixing of state → exhaustivity checks → no impossible states

Slide 20

Slide 20 text

1. the API 2. the engine 3. the extras

Slide 21

Slide 21 text

engine what is a GraphQL client? fetch relay

Slide 22

Slide 22 text

engine 1. no cache 2. operation cache 3. normalized cache levels of caching

Slide 23

Slide 23 text

engine → levels of caching → no cache API render query result

Slide 24

Slide 24 text

engine → levels of caching → operation cache API render query result cache if query not in cache

Slide 25

Slide 25 text

engine → levels of caching → operation cache API render query result cache → returns cache if available → calls API systemically to refresh stale while revalidate

Slide 26

Slide 26 text

engine → levels of caching → operation cache API render mutation result mutation invalidation

Slide 27

Slide 27 text

engine → levels of caching → operation cache API render mutation result mutation invalidation API render query result cache prime manual refresh

Slide 28

Slide 28 text

engine → levels of caching → normalized cache result User<1> Post<4> Comment<10>

Slide 29

Slide 29 text

engine → levels of caching → normalized cache result User<1> Post<4> Comment<10> mutation prime

Slide 30

Slide 30 text

engine → levels of caching → normalized cache mutation invalidation API render mutation result cache prime

Slide 31

Slide 31 text

engine how can we normalize cache with minimal information? normalized cache

Slide 32

Slide 32 text

engine normalized cache how can we normalize cache with minimal information? types

Slide 33

Slide 33 text

engine normalized cache add the __typename everywhere in queries

Slide 34

Slide 34 text

query typename export const addTypenames = (documentNode: DocumentNode): DocumentNode => { return visit(documentNode, { [Kind.SELECTION_SET]: (selectionSet): SelectionSetNode => { if ( selectionSet.selections.find( (selection) => selection.kind === Kind.FIELD && selection.name.value === "__typename", ) ) { return selectionSet; } else { return { ... selectionSet, selections: [TYPENAME_NODE, ... selectionSet.selections], }; } }, }); };

Slide 35

Slide 35 text

engine what's a cache entry? → root types (Query, Mutation) → IDed objects

Slide 36

Slide 36 text

engine caveat the client expects you query IDs systematically eslint plugin or codegen transforms can do that for you

Slide 37

Slide 37 text

engine what's a cache entry? anything else needs to be cached in its closest cached ancestor entry

Slide 38

Slide 38 text

engine cache structure Map { Symbol(Query) => { Symbol(user({"id":1})) => Symbol(User<1>), } Symbol(User<1>) => { Symbol(id) => 1, Symbol(username) => "bloodyowl" } }

Slide 39

Slide 39 text

engine cache structure Map { Symbol(Query) > Symbol(user({"id":1})) > } Symbol(User<1>) > Symbol(id) > Symbol(username) > } } Symbol(Query) Symbol(User<1>) → entries

Slide 40

Slide 40 text

engine cache structure Map { Symbol(Query) > Symbol(user({"id":1})) > } Symbol(User<1>) > Symbol(id) > Symbol(username) > } } Symbol(user({"id":1})) Symbol(id) Symbol(username) → fi elds with their arguments

Slide 41

Slide 41 text

engine cache structure Map { Symbol(Query) > Symbol(user({"id":1})) > } Symbol(User<1>) > Symbol(id) > Symbol(username) > } } Symbol(User<1>), → pointers to cache entries

Slide 42

Slide 42 text

engine cache: respond to query the engine runs the query against the cache

Slide 43

Slide 43 text

engine cache: respond to query how do you know if the cache has enough data to return a result while the API is loading?

Slide 44

Slide 44 text

engine cache structure Symbol(User<1>) > Symbol(__requestedKeys) > Symbol(id) > Symbol(username) > } Symbol(__requestedKeys) => Set { Symbol(id), Symbol(username) } → track fi elds that have been requested cache can respond if all requested fi elds are cached

Slide 45

Slide 45 text

engine ambiguities interfaces aka how object-oriented programming still manages to make things awful

Slide 46

Slide 46 text

engine → ambiguities → interfaces ... on Transaction { __typename id amount { value currency } }

Slide 47

Slide 47 text

engine → ambiguities → interfaces . __typename id amount { value currency } } { "__typename": "CardTransaction", "id": "efea560c-1a26-4d31-9555-ac5714 "amount": { "value": "10", "currency": "EUR" } } Transaction "CardTransaction" → can't assume fragment name as cache key we need to provide the cache the interface → object map

Slide 48

Slide 48 text

1. the API 2. the engine 3. the extras

Slide 49

Slide 49 text

extras relay pagination in fi nite scroll is contextual to the consumer

Slide 50

Slide 50 text

extras relay pagination const [data, { isLoading, setVariables }] = useQuery( AllFilmsQuery, { first: 3 }, ); if a variable changes, data goes back to loading

Slide 51

Slide 51 text

extras relay pagination const [data, { isLoading, setVariables }] = useQuery( AllFilmsQuery, { first: 3 }, ); if a variable changes, data remains Done, isLoading gets true

Slide 52

Slide 52 text

extras relay pagination const connection = useForwardPagination(connection); → keeps track of the queries it saw → aggregates the connection

Slide 53

Slide 53 text

engine cache structure Map { Symbol(Query) => { Symbol(user({"after":null})) => { ... }, Symbol(user({"after":"1az2zae ... "})) => { ... }, } }

Slide 54

Slide 54 text

extras connection updates useMutation(BlockUser, { connectionUpdates: [ ({ data, append }) => Option.fromNullable(data.blockUser).map(({ user }) => append(blockedUsers, [user]), ), ({ data, prepend }) => Option.fromNullable(data.blockUser).map(({ user }) => prepend(lastBlockedUsers, [user]), ), ], });

Slide 55

Slide 55 text

extras optional optimization const [data] = useQuery( MyQuery, {}, { optimize: true }, ); → creates an optimized query, only requests missing fi elds → doesn't query if everything in cache

Slide 56

Slide 56 text

→ @swan-io/graphql-client → 12kb gzipped → used in production for 6 months wrapping up

Slide 57

Slide 57 text

conclusion

Slide 58

Slide 58 text

conclusion I didn't revolutionize anything there

Slide 59

Slide 59 text

conclusion but I learnt things by trying

Slide 60

Slide 60 text

conclusion but I learnt things by trying

Slide 61

Slide 61 text

conclusion I didn't revolutionize anything there

Slide 62

Slide 62 text

conclusion but I can make it evolve

Slide 63

Slide 63 text

conclusion I didn't revolutionize anything there

Slide 64

Slide 64 text

conclusion but it works

Slide 65

Slide 65 text

conclusion there's no such thing as an absolute best practice

Slide 66

Slide 66 text

conclusion

Slide 67

Slide 67 text

conclusion

Slide 68

Slide 68 text

conclusion

Slide 69

Slide 69 text

conclusion build things

Slide 70

Slide 70 text

thank you! Matthias Le Brun @bloodyowl