Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building tools on top of Apollo

Building tools on top of Apollo

Apollo is great, but it is a bit like IKEA furniture - you have to assemble it yourself.

Showcasing internal tooling we build at Product Hunt to make our life easier when dealing with GraphQL and Apollo.

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

July 20, 2022
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. Building tools on top of Radoslav Stankov

  2. Radoslav Stankov @rstankov 
 rstankov.com blog.rstankov.com
 twitter.com/rstankov
 github.com/rstankov

  3. None
  4. None
  5. https://rstankov.com/appearances

  6. Plan for today

  7. 1.What is GraphQL 2.What is Apollo 3.Tools on top of

    Apollo Plan for today
  8. https://graphql.org/

  9. GraphQL is an open-source data query and manipulation language for

    APIs, and a runtime for fulfilling queries with existing data.
  10. None
  11. Query ⬅ Mutation ➡

  12. Query ⬅

  13. None
  14. query Homepage { homefeed { edges { node { id

    title items { id title tagline url thumbnail votesCount hasViewerVoted } } } pageInfo { hasNextPage endCursor } } }
  15. { "data": { "homefeed": { "edges": [ { "node": {

    "id": "day1", "title": "Day 1", "items": { "id": 1, "title": "Post 1", "tagline": "Tagline", "url": "https://example.com/1", "thumbnail": "https://example.com/1.png", "votesCount": 123, "hasViewerVoted": false } } }, { "node": { "id": "day2", "title": "Day 2",
  16. }, { "node": { "id": "day2", "title": "Day 2", "items":

    { "id": 1, "title": "Post 2", "tagline": "Tagline", "url": "https://example.com/2", "thumbnail": "https://example.com/2.png", "votesCount": 23, "hasViewerVoted": true } } } ], "pageInfo": { "hasNextPage": true, "endCursor": "day3" } } } }
  17. Mutation ➡

  18. None
  19. mutation PostVoteCreate($input: PostVoteCreateInput!) { postVoteCreate(input: $input) { node { id

    votesCount hasViewerVoted } errors { field messages } } }
  20. { "data": { "postVoteCreate": { "node": { "id": "1", "votesCount":

    1129, "hasViewerVoted": true }, "errors": [] } } }
  21. None
  22. None
  23. None
  24. import { gql, useQuery } from '@apollo/client'; const QUERY =

    gql` query Homepage { // ... } `; function Homefeed() { const { loading, error, data } = useQuery(QUERY); if (loading) { return <p>Loading...</p>; } if (error) { return <p>Error...</p>; } return ( <div> <h1>Homefeed</h1> {data.homefeed.edges(({ node }) => ( <section key={node.id}> <h2>{node.title}</h2>
  25. function Homefeed() { const { loading, error, data } =

    useQuery(QUERY); if (loading) { return <p>Loading...</p>; } if (error) { return <p>Error...</p>; } return ( <div> <h1>Homefeed</h1> {data.homefeed.edges(({ node }) => ( <section key={node.id}> <h2>{node.title}</h2> <ul> <li>{node.items.map((item) => item.title)}</li> </ul> </section> ))} </div> ); }
  26. None
  27. import { gql, useMutation } from '@apollo/client'; const CREATE_MUTATION =

    gql` mutation PostVoteCreate($input: PostVoteCreateInput!){ /* ... */ }` const UPDATE_MUTATION = gql` mutation PostVoteUpdate($input: PostVoteUpdateInput! { /* ... */ }` function VoteButton({ post }) { const [vote] = useMutation(CREATE_MUTATION); const [unvote] = useMutation(UPDATE_MUTATION); const onClick = () => { if (post.hasViewerVoted) { unvote({ variables: { postId: post.id } }); } else { vote({ variables: { postId: post.id } }); } }; return <button onClick={onClick}>{post.votesCount}</button>; }
  28. Generates TS Type definitions from GraphQL queries

  29. apollo client:codegen \ --localSchemaFile="graphql/schema.json" \ --addTypename \ --tagName=gql \ --target=typescript

    \ --includes="{components,screens,utils,hooks,layouts}/**/*.{tsx,ts}" \ --outputFlat="graphql/types.ts"
  30. None
  31. components/Profile/Avatar/Fragment.ts import gql from 'graphql-tag'; export default gql` fragment ProfileAvatarFragment

    on Profile { id name kind imageUrl } `;
  32. // ==================================================== // GraphQL fragment: ProfileAvatarFragment // ==================================================== export interface

    ProfileAvatarFragment { __typename: "Profile"; id: string; name: string; kind: string; imageUrl: string | null; } graphql/types.ts
  33. import { ProfileAvatarFragment } from '~/graphql/types';

  34. None
  35. Tools on top of Apollo

  36. None
  37. What are common things to have in an app?

  38. ⛷ Pagination
 ☎ Buttons
 % Forms

  39. ⛷ Pagination

  40. None
  41. None
  42. fetchMore({ variables: { cursor: endCursor, }, });

  43. Type Policy https://www.apollographql.com/docs/react/caching/ cache-configuration/#typepolicy-fields

  44. import { relayStylePagination } from '@apollo/client/utilities'; export default { Query:

    { fields: { homefeed: relayStylePagination(['key']), // ... }, }, User: { fields: { posts: relayStylePagination([]), // ... }, }, // ... };
  45. None
  46. None
  47. function useLoadMore({ connection, fetchMore, cursorName }) { const [isLoading, setIsLoading]

    = useState(false); const ref = useRef({}); ref.current.isLoading = isLoading; ref.current.setIsLoading = setIsLoading; ref.current.fetchMore = fetchMore; React.useEffect(() => { ref.current.isMounted = true; return () => { ref.current.isMounted = false; }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, },
  48. function useLoadMore({ connection, fetchMore, cursorName }) { const [isLoading, setIsLoading]

    = useState(false); const ref = useRef({}); ref.current.isLoading = isLoading; ref.current.setIsLoading = setIsLoading; ref.current.fetchMore = fetchMore; React.useEffect(() => { ref.current.isMounted = true; return () => { ref.current.isMounted = false; }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, },
  49. function useLoadMore({ connection, fetchMore, cursorName }) { const [isLoading, setIsLoading]

    = useState(false); const ref = useRef({}); ref.current.isLoading = isLoading; ref.current.setIsLoading = setIsLoading; ref.current.fetchMore = fetchMore; React.useEffect(() => { ref.current.isMounted = true; return () => { ref.current.isMounted = false; }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, },
  50. function useLoadMore({ connection, fetchMore, cursorName }) { const [isLoading, setIsLoading]

    = useState(false); const ref = useRef({}); ref.current.isLoading = isLoading; ref.current.setIsLoading = setIsLoading; ref.current.fetchMore = fetchMore; React.useEffect(() => { ref.current.isMounted = true; return () => { ref.current.isMounted = false; }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, },
  51. ref.current.isMounted = true; return () => { ref.current.isMounted = false;

    }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, }, }); if (ref.current.isMounted) { ref.current.setIsLoading(false); } }, [connection, ref]); return { loadMore, isLoading, hasMore: connection.pageInfo.hasNextPage }; }
  52. ref.current.isMounted = true; return () => { ref.current.isMounted = false;

    }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, }, }); if (ref.current.isMounted) { ref.current.setIsLoading(false); } }, [connection, ref]); return { loadMore, isLoading, hasMore: connection.pageInfo.hasNextPage }; }
  53. ref.current.isMounted = true; return () => { ref.current.isMounted = false;

    }; }, [ref]); const loadMore = useCallback(async () => { if (ref.current.isLoading || !connection.pageInfo.hasNextPage) { return; } ref.current.setIsLoading(true); await ref.current.fetchMore({ variables: { [cursorName || 'cursor']: connection.pageInfo.endCursor, }, }); if (ref.current.isMounted) { ref.current.setIsLoading(false); } }, [connection, ref]); return { loadMore, isLoading, hasMore: connection.pageInfo.hasNextPage }; }
  54. export function LoadMoreWithButton(options: ILoadMoreArgs) { const { loadMore, isLoading, hasMore

    } = useLoadMore(options); if (isLoading) { return <Button disabled={true}>Loading...</Button>; } if (hasMore) { return <Button onClick={loadMore}>Load more</Button>; } return null; }
  55. None
  56. None
  57. export default function LoadMoreWithScroll(options: ILoadMoreArgs) { const { loadMore, isLoading,

    hasMore } = useLoadMore(options); if (!hasMore) { return null; } return ( <> <Waypoint key={options.connection.pageInfo.endCursor || 'cursor'} bottomOffset="-20%" onEnter={loadMore} fireOnRapidScroll={true} /> {isLoading && <Loading />} </> ); }
  58. ☎ Buttons

  59. None
  60. None
  61. <Button.Primary to={paths.homepage()} /> <Button.Primary onClick={onClickReturnsPromise} /> <Button.Primary confirm="Are you sure?"

    />
 <Button.Primary requireLogin={true} />
  62. <Button.Primary mutation={MUTATION} input={{ postId: post.id }} /> <Button.Primary mutation={MUTATION} input={{

    postId: post.id }} optimisticResponse={{ node: { id: post.id, votesCount: post.votesCount + 1, hasVoted: true }, } } /> <Button.Primary mutation={MUTATION} input={{ postId: post.id }} onMutate={() => alert('Thanks for your vote &')} />
  63. export default function Button(props) { const onClick = useOnClick(props); const

    Element: any = props.to ? Link : 'button'; return ( <Element disabled={props.disabled} to={props.to} type={props.to ? undefined : props.type} target={props.target} data-test={props.dataTest} className={classNames(styles.reset, props.className)} onClick={props.onClick} rel={props.rel}> {children} </Element> ); }
  64. Button.Primary = ({ title, className, active = false, ...props })

    => ( <Button {...props} className={classNames(styles.primary, active && styles.active, className)}> <div>{title}</div> </Button> ); Button.Secondary = ({ title, className, active = false, ...props }) => ( <Button {...props} className={classNames(styles.secondary, active && styles.active, className}> {title} </Button> ); 
 Button.Small = ({ title, className, active = false, ...props }) => ( <Button {...props} className={classNames(styles.small, active && styles.active, className}> {title} </Button> );
  65. interface IOnClickOptions<IResponse> { onClick?: (e: IEvent) => void | Promise<IResponse>;

    requireLogin?: boolean | string; disabled?: boolean; confirm?: string; mutation?: DocumentNode; input?: DefaultObject; onMutate?: (node: IResponse) => void; onMutateError?: (errors: DefaultObject) => void; optimisticResponse?: any; update?: (cache: any, result: any) => void; updateQueries?: any; refetchQueries?: { query: any; variables?: any }[]; } export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => {
  66. interface IOnClickOptions<IResponse> { onClick?: (e: IEvent) => void | Promise<IResponse>;

    requireLogin?: boolean | string; disabled?: boolean; confirm?: string; mutation?: DocumentNode; input?: DefaultObject; onMutate?: (node: IResponse) => void; onMutateError?: (errors: DefaultObject) => void; optimisticResponse?: any; update?: (cache: any, result: any) => void; updateQueries?: any; refetchQueries?: { query: any; variables?: any }[]; } export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => {
  67. interface IOnClickOptions<IResponse> { onClick?: (e: IEvent) => void | Promise<IResponse>;

    requireLogin?: boolean | string; disabled?: boolean; confirm?: string; mutation?: DocumentNode; input?: DefaultObject; onMutate?: (node: IResponse) => void; onMutateError?: (errors: DefaultObject) => void; optimisticResponse?: any; update?: (cache: any, result: any) => void; updateQueries?: any; refetchQueries?: { query: any; variables?: any }[]; } export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => {
  68. export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref

    = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => { ref.current!.isMounted = true; return () => { ref.current!.isMounted = false; }; }, [ref]); return React.useCallback((e: IEvent) => runOnClick<T>(ref.current!, e), [ ref, ]); } async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.confirm && !(await window.confirm(options.confirm))) { return;
  69. export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref

    = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => { ref.current!.isMounted = true; return () => { ref.current!.isMounted = false; }; }, [ref]); return React.useCallback((e: IEvent) => runOnClick<T>(ref.current!, e), [ ref, ]); } async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.confirm && !(await window.confirm(options.confirm))) { return;
  70. export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref

    = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => { ref.current!.isMounted = true; return () => { ref.current!.isMounted = false; }; }, [ref]); return React.useCallback((e: IEvent) => runOnClick<T>(ref.current!, e), [ ref, ]); } async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.confirm && !(await window.confirm(options.confirm))) { return;
  71. export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref

    = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => { ref.current!.isMounted = true; return () => { ref.current!.isMounted = false; }; }, [ref]); return React.useCallback((e: IEvent) => runOnClick<T>(ref.current!, e), [ ref, ]); } async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.confirm && !(await window.confirm(options.confirm))) { return;
  72. export default function useOnClick<T = any>(options: IOnClickOptions<T>) { const ref

    = React.useRef(null); ref.current = { ...options, isLoading: false, isMounted: true, } as IRef<T>; React.useEffect(() => { ref.current!.isMounted = true; return () => { ref.current!.isMounted = false; }; }, [ref]); return React.useCallback((e: IEvent) => runOnClick<T>(ref.current!, e), [ ref, ]); } async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled || options.isLoading) { return;
  73. async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled ||

    options.isLoading) { return; } if (options.requireLogin && !isLoggedIn()) { return openLoginFullscreen(options.requireLogin); } if (options.confirm && !(await window.confirm(options.confirm))) { return; } options.isLoading = true; if (options.onClick) { await options.onClick(e); } if (options.mutation) { const { node, errors } = await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse,
  74. async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled ||

    options.isLoading) { return; } if (options.requireLogin && !isLoggedIn()) { return openLoginFullscreen(options.requireLogin); } if (options.confirm && !(await window.confirm(options.confirm))) { return; } options.isLoading = true; if (options.onClick) { await options.onClick(e); } if (options.mutation) { const { node, errors } = await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse,
  75. async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled ||

    options.isLoading) { return; } if (options.requireLogin && !isLoggedIn()) { return openLoginFullscreen(options.requireLogin); } if (options.confirm && !(await window.confirm(options.confirm))) { return; } options.isLoading = true; if (options.onClick) { await options.onClick(e); } if (options.mutation) { const { node, errors } = await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse,
  76. async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled ||

    options.isLoading) { return; } if (options.requireLogin && !isLoggedIn()) { return openLoginFullscreen(options.requireLogin); } if (options.confirm && !(await window.confirm(options.confirm))) { return; } options.isLoading = true; if (options.onClick) { await options.onClick(e); } if (options.mutation) { const { node, errors } = await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse,
  77. async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled ||

    options.isLoading) { return; } if (options.requireLogin && !isLoggedIn()) { return openLoginFullscreen(options.requireLogin); } if (options.confirm && !(await window.confirm(options.confirm))) { return; } options.isLoading = true; if (options.onClick) { await options.onClick(e); } if (options.mutation) { const { node, errors } = await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse,
  78. async function runOnClick<T>(options: IRef<T>, e: IEvent) { if (options.disabled ||

    options.isLoading) { return; } if (options.requireLogin && !isLoggedIn()) { return openLoginFullscreen(options.requireLogin); } if (options.confirm && !(await window.confirm(options.confirm))) { return; } options.isLoading = true; if (options.onClick) { await options.onClick(e); } if (options.mutation) { const { node, errors } = await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse,
  79. } if (options.mutation) { const { node, errors } =

    await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse, updateQueries: options.updateQueries, refetchQueries: options.refetchQueries, }); if (node) { options.onMutate?.(node); } if (errors) { options.onMutateError?.(errors); } } if (options.isMounted) { options.isLoading = false; } }
  80. } if (options.mutation) { const { node, errors } =

    await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse, updateQueries: options.updateQueries, refetchQueries: options.refetchQueries, }); if (node) { options.onMutate?.(node); } if (errors) { options.onMutateError?.(errors); } } if (options.isMounted) { options.isLoading = false; } }
  81. } if (options.mutation) { const { node, errors } =

    await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse, updateQueries: options.updateQueries, refetchQueries: options.refetchQueries, }); if (node) { options.onMutate?.(node); } if (errors) { options.onMutateError?.(errors); } } if (options.isMounted) { options.isLoading = false; } }
  82. } if (options.mutation) { const { node, errors } =

    await executeMutation<T>({ mutation: options.mutation, input: options.input || {}, update: options.update, optimisticResponse: options.optimisticResponse, updateQueries: options.updateQueries, refetchQueries: options.refetchQueries, }); if (node) { options.onMutate?.(node); } if (errors) { options.onMutateError?.(errors); } } if (options.isMounted) { options.isLoading = false; } }
  83. mutation PostVoteCreate($input: PostVoteCreateInput!) { response: postVoteCreate(input: $input) { node {

    id ...PostVoteButtonFragment } errors { name messages } } }
  84. % Forms

  85. None
  86. Input

  87. Input Loading

  88. Input Loading Success

  89. Input Loading Success Errors

  90. Submit Server Success Errors

  91. mutation Server { node: 'obj' } { errors: {…} }

  92. mutation Server { node: 'obj' } { 
 errors: {

    field1: 'error1',
 field2: 'error2'
 }
 }
  93. mutation UserSettingsUpdate($input: UserSettingsUpdateInput!) { response: userSettingsUpdate(input: $input) { node {

    id ...MySettingsPageViewer } errors { field message } } }
  94. <Form.Mutation mutation={MUTATION} initialValues={{ name: settings.name, headline: settings.headline, email: settings.email, websiteUrl:

    settings.websiteUrl, }}> <Flex.Column gap={6}> <Form.Label label="Name"> <Form.Input name="name" maxLength={40} /> </Form.Label> <Form.Label label="Headline"> <Form.Input name="headline" maxLength={40} /> </Form.Label> <Form.Label label="Email"> <EmailInput name="email" /> </Form.Label> <Form.Label label="Website"> <PrefixInput name="websiteUrl" prefix="https://" /> </Form.Label> </Flex.Column> <Flex.ResponsiveRow gap={5}> <Form.SubmitButton title="Save Changes" /> <Form.StatusMessage /> </Flex.ResponsiveRow> </Form.Mutation>
  95. Form.Mutation = ({ children, className, id, initialValues, mutation, mutationInput, onSubmit,

    onError, updateCache, validate, }) => { const submit = useSubmit({ mutation, mutationInput, updateCache, onSubmit, onError, }); return ( <Form className={className} id={id} initialValues={initialValues} submit={submit} validate={validate}> {children} </Form> ); };
  96. function useSubmit({ mutation, mutationInput, updateCache, onSubmit, onError, }) { const

    ref = React.useRef({}); ref.current.mutation = mutation; ref.current.mutationInput = mutationInput; ref.current.updateCache = updateCache; ref.current.onSubmit = onSubmit; ref.current.onError = onError; return React.useCallback( async (values: any, ..._rest: any) => { const { node, errors } = await executeMutation({ mutation: ref.current.mutation, input: normalizeInput(buildInput(values, ref.current.mutationInput)), update: ref.current.updateCache, }); if (errors) { ref.current.onError?.(errors);
  97. function useSubmit({ mutation, mutationInput, updateCache, onSubmit, onError, }) { const

    ref = React.useRef({}); ref.current.mutation = mutation; ref.current.mutationInput = mutationInput; ref.current.updateCache = updateCache; ref.current.onSubmit = onSubmit; ref.current.onError = onError; return React.useCallback( async (values: any, ..._rest: any) => { const { node, errors } = await executeMutation({ mutation: ref.current.mutation, input: normalizeInput(buildInput(values, ref.current.mutationInput)), update: ref.current.updateCache, }); if (errors) { ref.current.onError?.(errors);
  98. function useSubmit({ mutation, mutationInput, updateCache, onSubmit, onError, }) { const

    ref = React.useRef({}); ref.current.mutation = mutation; ref.current.mutationInput = mutationInput; ref.current.updateCache = updateCache; ref.current.onSubmit = onSubmit; ref.current.onError = onError; return React.useCallback( async (values: any, ..._rest: any) => { const { node, errors } = await executeMutation({ mutation: ref.current.mutation, input: normalizeInput(buildInput(values, ref.current.mutationInput)), update: ref.current.updateCache, }); if (errors) { ref.current.onError?.(errors);
  99. return React.useCallback( async (values: any, ..._rest: any) => { const

    { node, errors } = await executeMutation({ mutation: ref.current.mutation, input: normalizeInput(buildInput(values, ref.current.mutationInput)), update: ref.current.updateCache, }); if (errors) { ref.current.onError?.(errors); return errors; } ref.current.onSubmit?.(node); }, [ref], ); } function buildInput( input: any, mutationInput: undefined | null | any | ((data: any) => any), ) { if (typeof mutationInput === 'function') { return mutationInput(input);
  100. return React.useCallback( async (values: any, ..._rest: any) => { const

    { node, errors } = await executeMutation({ mutation: ref.current.mutation, input: normalizeInput(buildInput(values, ref.current.mutationInput)), update: ref.current.updateCache, }); if (errors) { ref.current.onError?.(errors); return errors; } ref.current.onSubmit?.(node); }, [ref], ); } function buildInput( input: any, mutationInput: undefined | null | any | ((data: any) => any), ) { if (typeof mutationInput === 'function') { return mutationInput(input);
  101. return React.useCallback( async (values: any, ..._rest: any) => { const

    { node, errors } = await executeMutation({ mutation: ref.current.mutation, input: normalizeInput(buildInput(values, ref.current.mutationInput)), update: ref.current.updateCache, }); if (errors) { ref.current.onError?.(errors); return errors; } ref.current.onSubmit?.(node); }, [ref], ); } function buildInput( input: any, mutationInput: undefined | null | any | ((data: any) => any), ) { if (typeof mutationInput === 'function') { return mutationInput(input);
  102. function buildInput( input: any, mutationInput: undefined | null | any

    | ((data: any) => any), ) { if (typeof mutationInput === 'function') { return mutationInput(input); } if (mutationInput) { return { ...mutationInput, ...input }; } return input; } function normalizeInput(input: any) { const object = {}; for (const key in input) { if (input.hasOwnProperty(key)) { object[key] = typeof input[key] === 'undefined' ? null : input[key]; } } return object; }
  103. ' utils/graphql

  104. interface IEdge<TNode> { node: TNode; } export interface IConnectionPartial<TNode> {

    edges: IEdge<TNode>[]; } export interface IConnection<TNode> { edges: IEdge<TNode>[]; pageInfo: { startCursor?: string | null; endCursor: string | null; hasNextPage: boolean; }; } export type IArrayOrConnection<T> = T[] | IConnectionPartial<T>; export function length(list: IArrayOrConnection<any> | null) { if (!list) { return 0; } return Array.isArray(list) ? list.length : list.edges!.length; } export function isPresent(list: IArrayOrConnection<any> | null) { return length(list) > 0;
  105. export function isPresent(list: IArrayOrConnection<any> | null) { return length(list) >

    0; } export function isEmpty(list: IArrayOrConnection<any> | null) { return length(list) === 0; } export function toArray<T>(list: IArrayOrConnection<T>): T[] { return Array.isArray(list) ? list : list.edges.map((edge) => edge.node); } export function map<T, R>( list: IArrayOrConnection<T>, fn: (node: T, i: number) => R, ) { return Array.isArray(list) ? list.map(fn) : list.edges!.map(({ node }: any, i) => fn(node, i)); } export function first<T>(list: IArrayOrConnection<T>) { return Array.isArray(list) ? list[0] : list?.edges[0]?.node; } export function hasNextPage<TNode>(connection: IConnection<TNode>): boolean { return connection.pageInfo.hasNextPage; }
  106. Recap

  107. ( What is GraphQL ) What is Apollo * Tools

    on top of Apollo
 + loadMore
 + useOnClick
 + Form.Mutation
 + utils/graphql
  108. None
  109. None
  110. Thanks ,

  111. Thanks , https://rstankov.com/appearances