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.

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