$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  2. Radoslav Stankov
    @rstankov

    rstankov.com

    blog.rstankov.com

    twitter.com/rstankov

    github.com/rstankov

    View Slide

  3. View Slide

  4. View Slide

  5. https://rstankov.com/appearances

    View Slide

  6. Plan for today

    View Slide

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

    View Slide

  8. https://graphql.org/

    View Slide

  9. GraphQL is an open-source data query and manipulation
    language for APIs, and a runtime for fulfilling queries with
    existing data.

    View Slide

  10. View Slide

  11. Query ⬅
    Mutation ➡

    View Slide

  12. Query ⬅

    View Slide

  13. View Slide

  14. query Homepage {
    homefeed {
    edges {
    node {
    id
    title
    items {
    id
    title
    tagline
    url
    thumbnail
    votesCount
    hasViewerVoted
    }
    }
    }
    pageInfo {
    hasNextPage
    endCursor
    }
    }
    }

    View Slide

  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",

    View Slide

  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"
    }
    }
    }
    }

    View Slide

  17. Mutation ➡

    View Slide

  18. View Slide

  19. mutation PostVoteCreate($input: PostVoteCreateInput!) {
    postVoteCreate(input: $input) {
    node {
    id
    votesCount
    hasViewerVoted
    }
    errors {
    field
    messages
    }
    }
    }

    View Slide

  20. {
    "data": {
    "postVoteCreate": {
    "node": {
    "id": "1",
    "votesCount": 1129,
    "hasViewerVoted": true
    },
    "errors": []
    }
    }
    }

    View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. import { gql, useQuery } from '@apollo/client';
    const QUERY = gql`
    query Homepage {
    // ...
    }
    `;
    function Homefeed() {
    const { loading, error, data } = useQuery(QUERY);
    if (loading) {
    return Loading...;
    }
    if (error) {
    return Error...;
    }
    return (

    Homefeed
    {data.homefeed.edges(({ node }) => (

    {node.title}

    View Slide

  25. function Homefeed() {
    const { loading, error, data } = useQuery(QUERY);
    if (loading) {
    return Loading...;
    }
    if (error) {
    return Error...;
    }
    return (

    Homefeed
    {data.homefeed.edges(({ node }) => (

    {node.title}

    {node.items.map((item) => item.title)}


    ))}

    );
    }

    View Slide

  26. View Slide

  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 {post.votesCount};
    }

    View Slide

  28. Generates TS Type definitions
    from GraphQL queries

    View Slide

  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"

    View Slide

  30. View Slide

  31. components/Profile/Avatar/Fragment.ts
    import gql from 'graphql-tag';
    export default gql`
    fragment ProfileAvatarFragment on Profile {
    id
    name
    kind
    imageUrl
    }
    `;

    View Slide

  32. // ====================================================
    // GraphQL fragment: ProfileAvatarFragment
    // ====================================================
    export interface ProfileAvatarFragment {
    __typename: "Profile";
    id: string;
    name: string;
    kind: string;
    imageUrl: string | null;
    }
    graphql/types.ts

    View Slide

  33. import { ProfileAvatarFragment } from '~/graphql/types';

    View Slide

  34. View Slide

  35. Tools on top of Apollo

    View Slide

  36. View Slide

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

    View Slide

  38. ⛷ Pagination

    ☎ Buttons

    % Forms

    View Slide

  39. ⛷ Pagination

    View Slide

  40. View Slide

  41. View Slide

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

    View Slide

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

    View Slide

  44. import { relayStylePagination } from '@apollo/client/utilities';
    export default {
    Query: {
    fields: {
    homefeed: relayStylePagination(['key']),
    // ...
    },
    },
    User: {
    fields: {
    posts: relayStylePagination([]),
    // ...
    },
    },
    // ...
    };

    View Slide

  45. View Slide

  46. View Slide

  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,
    },

    View Slide

  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,
    },

    View Slide

  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,
    },

    View Slide

  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,
    },

    View Slide

  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 };
    }

    View Slide

  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 };
    }

    View Slide

  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 };
    }

    View Slide

  54. export function LoadMoreWithButton(options: ILoadMoreArgs) {
    const { loadMore, isLoading, hasMore } = useLoadMore(options);
    if (isLoading) {
    return Loading...;
    }
    if (hasMore) {
    return Load more;
    }
    return null;
    }

    View Slide

  55. View Slide

  56. View Slide

  57. export default function LoadMoreWithScroll(options: ILoadMoreArgs) {
    const { loadMore, isLoading, hasMore } = useLoadMore(options);
    if (!hasMore) {
    return null;
    }
    return (
    <>
    key={options.connection.pageInfo.endCursor || 'cursor'}
    bottomOffset="-20%"
    onEnter={loadMore}
    fireOnRapidScroll={true}
    />
    {isLoading && }
    >
    );
    }

    View Slide

  58. ☎ Buttons

    View Slide

  59. View Slide

  60. View Slide





  61. View Slide

  62. mutation={MUTATION}
    input={{ postId: post.id }}
    />
    mutation={MUTATION}
    input={{ postId: post.id }}
    optimisticResponse={{
    node: {
    id: post.id,
    votesCount: post.votesCount + 1,
    hasVoted: true },
    }
    }
    />
    mutation={MUTATION}
    input={{ postId: post.id }}
    onMutate={() => alert('Thanks for your vote &')}
    />

    View Slide

  63. export default function Button(props) {
    const onClick = useOnClick(props);
    const Element: any = props.to ? Link : 'button';
    return (
    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}

    );
    }

    View Slide

  64. Button.Primary = ({ title, className, active = false, ...props }) => (
    {...props}
    className={classNames(styles.primary, active && styles.active, className)}>
    {title}

    );
    Button.Secondary = ({ title, className, active = false, ...props }) => (
    {...props}
    className={classNames(styles.secondary, active && styles.active, className}>
    {title}

    );

    Button.Small = ({ title, className, active = false, ...props }) => (
    {...props}
    className={classNames(styles.small, active && styles.active, className}>
    {title}

    );

    View Slide

  65. interface IOnClickOptions {
    onClick?: (e: IEvent) => void | Promise;
    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(options: IOnClickOptions) {
    const ref = React.useRef(null);
    ref.current = {
    ...options,
    isLoading: false,
    isMounted: true,
    } as IRef;
    React.useEffect(() => {

    View Slide

  66. interface IOnClickOptions {
    onClick?: (e: IEvent) => void | Promise;
    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(options: IOnClickOptions) {
    const ref = React.useRef(null);
    ref.current = {
    ...options,
    isLoading: false,
    isMounted: true,
    } as IRef;
    React.useEffect(() => {

    View Slide

  67. interface IOnClickOptions {
    onClick?: (e: IEvent) => void | Promise;
    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(options: IOnClickOptions) {
    const ref = React.useRef(null);
    ref.current = {
    ...options,
    isLoading: false,
    isMounted: true,
    } as IRef;
    React.useEffect(() => {

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. async function runOnClick(options: IRef, 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({
    mutation: options.mutation,
    input: options.input || {},
    update: options.update,
    optimisticResponse: options.optimisticResponse,

    View Slide

  74. async function runOnClick(options: IRef, 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({
    mutation: options.mutation,
    input: options.input || {},
    update: options.update,
    optimisticResponse: options.optimisticResponse,

    View Slide

  75. async function runOnClick(options: IRef, 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({
    mutation: options.mutation,
    input: options.input || {},
    update: options.update,
    optimisticResponse: options.optimisticResponse,

    View Slide

  76. async function runOnClick(options: IRef, 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({
    mutation: options.mutation,
    input: options.input || {},
    update: options.update,
    optimisticResponse: options.optimisticResponse,

    View Slide

  77. async function runOnClick(options: IRef, 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({
    mutation: options.mutation,
    input: options.input || {},
    update: options.update,
    optimisticResponse: options.optimisticResponse,

    View Slide

  78. async function runOnClick(options: IRef, 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({
    mutation: options.mutation,
    input: options.input || {},
    update: options.update,
    optimisticResponse: options.optimisticResponse,

    View Slide

  79. }
    if (options.mutation) {
    const { node, errors } = await executeMutation({
    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;
    }
    }

    View Slide

  80. }
    if (options.mutation) {
    const { node, errors } = await executeMutation({
    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;
    }
    }

    View Slide

  81. }
    if (options.mutation) {
    const { node, errors } = await executeMutation({
    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;
    }
    }

    View Slide

  82. }
    if (options.mutation) {
    const { node, errors } = await executeMutation({
    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;
    }
    }

    View Slide

  83. mutation PostVoteCreate($input: PostVoteCreateInput!) {
    response: postVoteCreate(input: $input) {
    node {
    id
    ...PostVoteButtonFragment
    }
    errors {
    name
    messages
    }
    }
    }

    View Slide

  84. % Forms

    View Slide

  85. View Slide

  86. Input

    View Slide

  87. Input Loading

    View Slide

  88. Input Loading
    Success

    View Slide

  89. Input Loading
    Success
    Errors

    View Slide

  90. Submit Server
    Success
    Errors

    View Slide

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

    View Slide

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

    errors: {
    field1: 'error1',

    field2: 'error2'

    }

    }

    View Slide

  93. mutation UserSettingsUpdate($input: UserSettingsUpdateInput!) {
    response: userSettingsUpdate(input: $input) {
    node {
    id
    ...MySettingsPageViewer
    }
    errors {
    field
    message
    }
    }
    }

    View Slide

  94. mutation={MUTATION}
    initialValues={{
    name: settings.name,
    headline: settings.headline,
    email: settings.email,
    websiteUrl: settings.websiteUrl,
    }}>



















    View Slide

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

    );
    };

    View Slide

  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);

    View Slide

  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);

    View Slide

  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);

    View Slide

  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);

    View Slide

  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);

    View Slide

  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);

    View Slide

  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;
    }

    View Slide

  103. ' utils/graphql

    View Slide

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

    View Slide

  105. export function isPresent(list: IArrayOrConnection | null) {
    return length(list) > 0;
    }
    export function isEmpty(list: IArrayOrConnection | null) {
    return length(list) === 0;
    }
    export function toArray(list: IArrayOrConnection): T[] {
    return Array.isArray(list) ? list : list.edges.map((edge) => edge.node);
    }
    export function map(
    list: IArrayOrConnection,
    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(list: IArrayOrConnection) {
    return Array.isArray(list) ? list[0] : list?.edges[0]?.node;
    }
    export function hasNextPage(connection: IConnection): boolean {
    return connection.pageInfo.hasNextPage;
    }

    View Slide

  106. Recap

    View Slide

  107. ( What is GraphQL
    ) What is Apollo
    * Tools on top of Apollo

    + loadMore

    + useOnClick

    + Form.Mutation

    + utils/graphql

    View Slide

  108. View Slide

  109. View Slide

  110. Thanks ,

    View Slide

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

    View Slide