DEV Community

Cover image for URQL, o básico
Vinicius Savegnago
Vinicius Savegnago

Posted on

URQL, o básico

Recentemente fiz um post sobre TypeGraphQL e como utilizar o framework para incríveis benefícios em sua API GraphQL com Typescript. Para complementar, dessa vez vou mostrar um pouco sobre o URQL, um GraphQL Client. Com ele vamos consumir uma API de receitas - Que no caso é a mesma API que fiz em meu post anterior.

Para isso vamos utilizar React.js para a construção de nosso CRUD 😊

Antes de tudo, devo-lhe uma breve apresentação sobre este Client.

O URQL é um client GraphQL com o foco em usabilidade e adaptabilidade, com um setup rápido, e de fácil utilização, sendo capaz de suportar infraestruturas bem avançadas em GraphQL.

urql Documentation

BORA CODAR!

Primeiro, vamos criar um novo projeto.

Criamos uma pasta para o projeto. (O nome é você quem decide)

mkdir urql-basics
cd urql-basics
Enter fullscreen mode Exit fullscreen mode

Vamos inicializar o projeto com um template do React.js com Typescript. Pode utilizar o npx ou o yarn. Vou utilizar o yarn.

yarn create react-app . --template typescript
Enter fullscreen mode Exit fullscreen mode

Com o projeto inicializado, vamos instalar o URQL.

yarn add urql graphql
Enter fullscreen mode Exit fullscreen mode

Agora que tudo está instalado podemos remover alguns arquivos que não iremos utilizar.

Só vamos precisar dos sequintes:

/public
    index.html
/src
    App.tsx
    index.tsx
    index.css
  react-app-env.d.ts
Enter fullscreen mode Exit fullscreen mode
yarn start
Enter fullscreen mode Exit fullscreen mode

O app deve estar rodando na porta 3000 👍🏼

Nos exemplos, vou estar utilizando styled-components para ajudar com a estilização do app. Se preferir de outra maneira, não tem problema.

btw CSS in JS = 💘

yarn add styled-components @typed/styled-components -D
Enter fullscreen mode Exit fullscreen mode

Com o styled-components, podemos criar de fato um componente React, com toda sua estilização acoplada. Apartir de "Literais de Modelos Marcados" construimos todo o estilo do componente. Essa marcação é simplesmente CSS/Sass.

Veja mais aqui:

styled-components: Basics

Primeiro de tudo, vamos configurar o URQL e criar o nosso provider.

Em uma pasta ./api, criei um arquivo chamado urql.ts.

Neste arquiivo vamos exportar um Client

import { createClient } from 'urql';

export const urqlClient = createClient({
  url: 'http://localhost:4000/',
});
Enter fullscreen mode Exit fullscreen mode

Para tudo funcionar, passamos um objeto com algumas configurações para uma função que retorna um Client.

Em nosso caso vamos passar apenas o mínimo, que seria a url da nossa API GraphQL

Agora, para começar, vamos criar um Provider para a nossa aplicação fazer o uso do Client.

Como esse Provider utiliza a API de Context, vamos envolver nossa aplicação com ele.

Em nosso app.tsx

import { Provider } from 'urql';
import { urqlClient } from './api/urql';

const App: FunctionComponent = () => {

  return (
      <Provider value={urqlClient}>
        <Wrapper>
                    //...App
        </Wrapper>
      </Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

No meu App, acabei criando um component Wrapper, para centralizar o conteúdo no meio da tela

Todos os meus componentes iram ficar em uma pasta ./components, E cada um deles em uma pasta com seus próprios estilos.

Para esse post não ficar muito grande, irei passar vagamente pela estilização, dando um foco maior no URQL. Mas não se preocupe, vou disponibilizar tudo em um repositório no Github 😎

Agora que já temos nosso Client configurado, vamos criar nossa primeira Query, que ira buscar receitas em minha API.

Dentro de ./src vou criar uma pasta ./graphql. Dentro dela podemos colocar as nossas Mutations e Queries

.src/graphql/queries/recipesQuery.ts

export const recipesQuery = `
    query {
        recipes {
            id
            name
            description
            ingredients
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

Simplesmente a minha query é uma String, com a sintaxe do GraphQL.

Para executarmos a nossa query, vamos criar um componente que irá listar todas as nossas receitas.

./components/RecipeList.component.tsx

import React, { FunctionComponent } from 'react';
import RecipeCard from '../recipeCard/RecipeCard.component';

import RecipesWrapper from './styles';

import { useQuery } from 'urql';
import { recipesQuery } from '../../graphql/queries/recipesQuery';

interface RecipesListProps {}

const RecipesList: FunctionComponent<RecipesListProps> = () => {
  const [recipesResult, reexecuteQuery] = useQuery({
    query: recipesQuery,
  });

  const { data, fetching, error } = recipesResult;

  if (fetching) return <p>Carregando...</p>;

  if (error) return <p>Algo deu errado... {error.message}</p>;

  return (
    <RecipesWrapper>
      {data.recipes.map((recipe: any) => (
        <RecipeCard
          id={recipe.id}
          key={recipe.id}
          name={recipe.name}
          description={recipe.description}
          ingredients={[...recipe.ingredients]}
        />
      ))}
    </RecipesWrapper>
  );
};

export default RecipesList;
Enter fullscreen mode Exit fullscreen mode

Utilizando o hook useQuery disponibilizado pelo próprio URQL, enviamos a nossa query, que trará uma tupla, contendo um objeto com o resultado da query e uma função de reexecução.

Esse objeto vai conter:

  • data ⇒ Os dados obtidos da API
  • fetching ⇒ Uma indicação de que os dados estão sendo carregados.
  • error ⇒ Erros de conexão ou mesmo GraphQLErrors

Logo, utilizando o data, vamos exibir na tela todas as receitas que existirem.

Para isso criei um RecipeCard component, que é preenchido com as informações das receitas.

./components/RecipeCard.component.tsx

import React, { FunctionComponent, useContext } from 'react';

interface RecipeCardProps {
  id?: string;
  name: string;
  description: string;
  ingredients: Array<string>;
}

const RecipeCard: FunctionComponent<RecipeCardProps> = ({
  id,
  name,
  description,
  ingredients,
}) => {

  return (
    <Card>
      <TextWrapper>
        <TextLabel>Receita</TextLabel>
        <Title>{name}</Title>
      </TextWrapper>

      <TextWrapper>
        <TextLabel>Descrição</TextLabel>
        <Description>{description}</Description>
      </TextWrapper>

      <TextWrapper>
        <TextLabel>Ingredientes</TextLabel>

        {ingredients.map((ingredient, index) => (
          <Ingredient key={index}>{ingredient}</Ingredient>
        ))}
      </TextWrapper>

      <TextWrapper>
        <TextLabel>Opções</TextLabel>
        <ActionsWrapper>
          <UpdateButton>Atualizar</UpdateButton>
          <DeleteButton>Deletar</DeleteButton>
        </ActionsWrapper>
      </TextWrapper>
    </Card>
  );
};

export default RecipeCard;
Enter fullscreen mode Exit fullscreen mode

Lista de Receitas

Incrível! 🚀

Agora vamos adicionar a Mutation para criarmos uma nova receita.

Vamos criar a createRecipeMutation.ts

./graphql/mutations/createRecipeMutation.ts

export const createRecipeMutation = `
    mutation(
        $name: String!,
        $description: String!,
        $ingredients: [String!]!
    ) {
        createRecipe(data: {
            name: $name,
            description: $description,
            ingredients: $ingredients
        }) {
            recipe {
                id
            }
            error {
                message
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

No caso da API de receitas, precisamos enviar o nome, a descrição e uma lista com os ingredientes, especificando cada um no inicio de nossa mutation.

Com nossa createRecipeMutation pronta, vamos criar um formulário para fazer o registro de uma receita. Para isso vou utilizar o Formik, que é uma biblioteca para gerenciar formulários.

Se você não conhece, sugiro que dê uma olhada:

Formik

Para deixar o app mais limpo e simples, vou utilizar um unico formulário, tanto para o Update, quanto para o Create.

Para abrir o formulário de Create, criei um botão e adicionei ele em app.tsx

<Provider value={urqlClient}>
        <Wrapper>
          <Title>myRecipes</Title>

          <RecipesList />

          <Recipeform />

          <CreateRecipeButton />
        </Wrapper>
</Provider>
Enter fullscreen mode Exit fullscreen mode

Para compartilhar qual formulário está aberto e qual está fechado, utilizei o Context API para compartilhar dois atributos que indicam quais dos formulários vão abrir. Sendo o Create ou o Update.

Dentro de ./context, criei o contexto do app.

./context/context.ts

import { createContext } from 'react';

interface AppContextType {
  isCreateRecipeFormOpen: boolean;
  isUpdateRecipeFormOpen: boolean;
}

export const initialAppContext: AppContextType = {
  isCreateRecipeFormOpen: false,
  isUpdateRecipeFormOpen: false,
};

export const AppContext = createContext<
  [AppContextType, React.Dispatch<React.SetStateAction<AppContextType>>]
>([initialAppContext, () => {}]);
Enter fullscreen mode Exit fullscreen mode

Para checar o estado dos formulários, criei um componente que irá renderizar apenas o formulário que foi requisitado.

./components/RecipeForm.component.tsx

import React, { FunctionComponent, useContext } from 'react';

import { AppContext } from '../../context/context';

import Form from '../form/Form.component';

const Recipeform: FunctionComponent = () => {
  const [appContext] = useContext(AppContext);

  if (appContext.isCreateRecipeFormOpen) {
    return <Form btnName="Criar" formType="create" title="Criar receita" />;
  }

  if (appContext.isUpdateRecipeFormOpen) {
    return (
      <Form btnName="Atualizar" formType="update" title="Atualizar receita" />
    );
  }

  return null;
};

export default Recipeform;
Enter fullscreen mode Exit fullscreen mode

E nosso formulário fica assim:

./components/Form.component.tsx

import React, { FunctionComponent, useContext } from 'react';

import { FormikValues, useFormik } from 'formik';

import { FormField, Title, InputsWrapper, Input, FinishButton } from './styles';

interface FormProps {
  title: string;
  btnName: string;
  formType: 'update' | 'create';
}

const Form: FunctionComponent<FormProps> = ({ formType, title, btnName }) => {

  const formik = useFormik({
    initialValues: {
      name: '',
      description: '',
      ingredients: '',
    },
    onSubmit: (formikValues) => handleForm(formikValues),
  });

  const update = async (formikValues: FormikValues) => {
    // TODO Update Recipe Mutation
  };

  const create = async (formikValues: FormikValues) => {
    // TODO Create Recipe Mutation

  };

  const handleForm = (formikValues: any) => {
    // TODO handle update or create
  };

  const handleIngredientsField = (ingredients: string) => {
    let ingredientsArray = ingredients.split(',');
    return ingredientsArray;
  };

  return (
    <FormField onSubmit={formik.handleSubmit}>
      <Title>{title}</Title>

      <InputsWrapper>
        <Input
          name="name"
          id="name"
          type="text"
          placeholder="Nome da sua receita"
          onChange={formik.handleChange}
          value={formik.values.name}
        />

        <Input
          name="description"
          id="description"
          type="text"
          placeholder="Descrição da sua receita"
          onChange={formik.handleChange}
          value={formik.values.description}
        />

        <Input
          name="ingredients"
          id="ingredients"
          type="text"
          placeholder="Ingredientes (separados por virgula)"
          onChange={formik.handleChange}
          value={formik.values.ingredients}
        />

        <FinishButton type="submit">{btnName}</FinishButton>
      </InputsWrapper>
    </FormField>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

Agora vamos adicionar nossa createRecipeMutation:

./components/Form.tsx

import { useMutation } from 'urql';
import { createRecipeMutation } from '../../graphql/mutations/createRecipeMutation';

interface FormProps {
  title: string;
  btnName: string;
  formType: 'update' | 'create';
}

const Form: FunctionComponent<FormProps> = ({ formType, title, btnName }) => {
  const [createRecipeResult, createRecipe] = useMutation(createRecipeMutation);
  const [appContext, setAppContext] = useContext(AppContext);

  const formik = useFormik({
    initialValues: {
      name: '',
      description: '',
      ingredients: '',
    },
    onSubmit: (formikValues) => handleForm(formikValues),
  });

  const update = async (formikValues: FormikValues) => {
    // TODO Update Recipe Mutation
  };

  const create = async (formikValues: FormikValues) => {
    // Create Recipe Mutation
    await createRecipe({
      ...formikValues,
      ingredients: handleIngredientsField(formikValues.ingredients),
    });
  };

  const handleForm = (formikValues: any) => {
    setAppContext({
      ...appContext,
      isUpdateRecipeFormOpen: false,
      isCreateRecipeFormOpen: false,
    });

    create(formikValues);
  };

  const handleIngredientsField = (ingredients: string) => {
    let ingredientsArray = ingredients.split(',');
    return ingredientsArray;
  };

return (
    //...
    )
};

export default Form;

Enter fullscreen mode Exit fullscreen mode

Utilizando o hook de useMutation, vamos ter um objeto com o resultado e uma função para executar a Mutation.

Vamos testar!

Alt Text

Alt Text

Show! 🔥

Agora para a nossa Mutation de Update, vamos fazer algo muito parecido.

Porém, desta vez, vamos precisar enviar o ID da receita que queremos fazer a atualização.

./updateRecipeMutation.ts

export const updateRecipeMutation = `
    mutation(
        $id: String!,
        $name: String!,
        $description: String!,
        $ingredients: [String!]!
    ) {
        updateRecipe(
            id: $id,
            data: {
                name: $name,
                description: $description,
                ingredients: $ingredients
        }) {
            recipe {
                id
            }
            error {
                message
            }
            success
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

Então em nosso RecipeCard, vamos utilizar o botão de update, para iniciar o processo de atualização.

No App, também utilizei o Context API para compartilhar o ID da receita que vai ser atualizada. E Neste caso, como sabemos, vamos abrir o formulário de Update.

AppContext.ts

import { createContext } from 'react';
import Recipe from '../interfaces/Recipe';

interface AppContextType {
  recipes: Array<Recipe>;
  isCreateRecipeFormOpen: boolean;
  isUpdateRecipeFormOpen: boolean;
  recipeIdToUpdate: string;
}

export const initialAppContext: AppContextType = {
  recipes: [],
  isCreateRecipeFormOpen: false,
  isUpdateRecipeFormOpen: false,
  recipeIdToUpdate: '',
};

export const AppContext = createContext<
  [AppContextType, React.Dispatch<React.SetStateAction<AppContextType>>]
>([initialAppContext, () => {}]);
Enter fullscreen mode Exit fullscreen mode

./RecipeCard.component.tsx

const openUpdateForm = () => {
    setAppContext({
      ...appContext,
      isCreateRecipeFormOpen: false,
      isUpdateRecipeFormOpen: true,
      recipeIdToUpdate: id ? id : '',
    });
  };

<ActionsWrapper>
          <UpdateButton onClick={openUpdateForm}>Atualizar</UpdateButton>
          <DeleteButton>Deletar</DeleteButton>
</ActionsWrapper
Enter fullscreen mode Exit fullscreen mode

E nosso em nosso Form:

./components/Form.component.tsx

import { useMutation } from 'urql';
import { updateRecipeMutation } from '../../graphql/mutations/updateRecipeMutation';

interface FormProps {
  title: string;
  btnName: string;
  formType: 'update' | 'create';
}

const Form: FunctionComponent<FormProps> = ({ formType, title, btnName }) => {
  const [createRecipeResult, createRecipe] = useMutation(createRecipeMutation);
  const [updateRecipeResult, updateRecipe] = useMutation(updateRecipeMutation);
  const [appContext, setAppContext] = useContext(AppContext);

  const formik = useFormik({
    initialValues: {
      name: '',
      description: '',
      ingredients: '',
    },
    onSubmit: (formikValues) => handleForm(formikValues),
  });

  const update = async (formikValues: FormikValues) => {
    // Update Recipe Mutation
    await updateRecipe({
      id: appContext.recipeIdToUpdate,
      ...formikValues,
      ingredients: handleIngredientsField(formikValues.ingredients),
    });
  };

  const create = async (formikValues: FormikValues) => {
    // Create Recipe Mutation
    await createRecipe({
      ...formikValues,
      ingredients: handleIngredientsField(formikValues.ingredients),
    });
  };

  const handleForm = (formikValues: any) => {
    setAppContext({
      ...appContext,
      isUpdateRecipeFormOpen: false,
      isCreateRecipeFormOpen: false,
    });

    formType === 'update' ? update(formikValues) : create(formikValues);
  };

  const handleIngredientsField = (ingredients: string) => {
    let ingredientsArray = ingredients.split(',');
    return ingredientsArray;
  };

  return (
    //...
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

Alt Text

Alt Text

Brabo! Agora só precisamos implementar o Delete.

Então, vamos criar nossa deleteRecipeMutation

export const deleteRecipeMutation = `
    mutation(
        $id: String!
    ) {
        deleteRecipe(id: $id) {
            recipe {
                id
            }
            error {
                message
            }
            success
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

E para conseguirmos enviar essa Mutation, vamos adicionar uma função em nosso botão de Deletar.

./components/RecipeCard.component.tsx

import { useMutation } from 'urql';
import { deleteRecipeMutation } from '../../graphql/mutations/deleteRecipeMutation';

interface RecipeCardProps {
  id?: string;
  name: string;
  description: string;
  ingredients: Array<string>;
}

const RecipeCard: FunctionComponent<RecipeCardProps> = ({
  id,
  name,
  description,
  ingredients,
}) => {
  const [appContext, setAppContext] = useContext(AppContext);
  const [deleteRecipeResult, deleteRecipe] = useMutation(deleteRecipeMutation);

  const handleDeleteRecipe = async () => {
    //Delete Recipe Mutation
    await deleteRecipe({ id });
  };

  return (
    <Card>
      //...

        <ActionsWrapper>
          <UpdateButton onClick={openUpdateForm}>Atualizar</UpdateButton>
          <DeleteButton onClick={handleDeleteRecipe}>Deletar</DeleteButton>
        </ActionsWrapper>
      </TextWrapper>
    </Card>
  );
};

export default RecipeCard;
Enter fullscreen mode Exit fullscreen mode

Agora sim, temos nosso CRUD com URQL 🎉 🎉

Espero que essa pequena introdução tenha sido útil 😊

Valeu! ♥️

Link do projeto no Github:

vinisaveg/urql-basics

Link do meu post sobre TypeGraphQL

TypeGraphQL, o básico em uma API de receitas

Happy Coding!

Top comments (3)

Collapse
 
stephyswe profile image
Stephanie

would it be possible to get the server code api used too

Collapse
 
vinisaveg profile image
Vinicius Savegnago • Edited

Hi! sure I can!
It is an TypeGraphQL API that I posted as well.

But nowadays I am using SWR instead of URQL, and I also have a post about it:
dev.to/vinisaveg/consumindo-uma-ap...

Here is the link to the API used in this post:
dev.to/vinisaveg/typegraphql-o-bas...

Everything is on portuguese, sorry.

Collapse
 
stephyswe profile image
Stephanie

no probkrm I love Portuguese