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. GraphQL is an open-source data query and manipulation language for

    APIs, and a runtime for fulfilling queries with existing data.
  2. query Homepage { homefeed { edges { node { id

    title items { id title tagline url thumbnail votesCount hasViewerVoted } } } pageInfo { hasNextPage endCursor } } }
  3. { "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",
  4. }, { "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" } } } }
  5. { "data": { "postVoteCreate": { "node": { "id": "1", "votesCount":

    1129, "hasViewerVoted": true }, "errors": [] } } }
  6. 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>
  7. 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> ); }
  8. 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>; }
  9. apollo client:codegen \ --localSchemaFile="graphql/schema.json" \ --addTypename \ --tagName=gql \ --target=typescript

    \ --includes="{components,screens,utils,hooks,layouts}/**/*.{tsx,ts}" \ --outputFlat="graphql/types.ts"
  10. // ==================================================== // GraphQL fragment: ProfileAvatarFragment // ==================================================== export interface

    ProfileAvatarFragment { __typename: "Profile"; id: string; name: string; kind: string; imageUrl: string | null; } graphql/types.ts
  11. import { relayStylePagination } from '@apollo/client/utilities'; export default { Query:

    { fields: { homefeed: relayStylePagination(['key']), // ... }, }, User: { fields: { posts: relayStylePagination([]), // ... }, }, // ... };
  12. 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, },
  13. 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, },
  14. 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, },
  15. 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, },
  16. 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 }; }
  17. 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 }; }
  18. 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 }; }
  19. 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; }
  20. 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 />} </> ); }
  21. <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 &')} />
  22. 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> ); }
  23. 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> );
  24. 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(() => {
  25. 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(() => {
  26. 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(() => {
  27. 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;
  28. 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;
  29. 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;
  30. 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;
  31. 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;
  32. 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,
  33. 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,
  34. 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,
  35. 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,
  36. 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,
  37. 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,
  38. } 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; } }
  39. } 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; } }
  40. } 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; } }
  41. } 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; } }
  42. mutation Server { node: 'obj' } { 
 errors: {

    field1: 'error1',
 field2: 'error2'
 }
 }
  43. <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>
  44. 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> ); };
  45. 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);
  46. 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);
  47. 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);
  48. 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);
  49. 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);
  50. 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);
  51. 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; }
  52. 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;
  53. 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; }
  54. ( What is GraphQL ) What is Apollo * Tools

    on top of Apollo
 + loadMore
 + useOnClick
 + Form.Mutation
 + utils/graphql