URQL, o básico
Tags: urql, typescript, react, graphql
Published: 2020-12-17 00:58:22

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](https://formidable.com/open-source/urql/)

BORA CODAR!

Primeiro, vamos criar um novo projeto.

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

```bash
mkdir urql-basics
cd urql-basics
```

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

```bash
yarn create react-app . --template typescript
```

Com o projeto inicializado, vamos instalar o URQL.

```bash
yarn add urql graphql
```

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

Só vamos precisar dos sequintes:

```bash
/public
	index.html
/src
	App.tsx
	index.tsx
	index.css
  react-app-env.d.ts
```

```bash
yarn start
```

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 = 💘

```bash
yarn add styled-components @typed/styled-components -D
```

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](https://styled-components.com/docs/basics#getting-started)

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*

```tsx
import { createClient } from 'urql';

export const urqlClient = createClient({
  url: 'http://localhost:4000/',
});
```

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*

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

const App: FunctionComponent = () => {

  return (
      <Provider value={urqlClient}>
        <Wrapper>
					//...App
        </Wrapper>
      </Provider>
  );
};
```

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

```tsx
export const recipesQuery = `
    query {
        recipes {
            id
            name
            description
            ingredients
        }
    }
`;
```

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

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

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

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

![Lista de Receitas](https://dev-to-uploads.s3.amazonaws.com/i/th0qo1m255uz8d0sb401.png)

Incrível! 🚀

Agora vamos adicionar a *Mutation* para criarmos uma nova receita.

Vamos criar a *createRecipeMutation.ts*

./graphql/mutations/createRecipeMutation.ts

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

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](https://formik.org)

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*

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

          <RecipesList />

          <Recipeform />

          <CreateRecipeButton />
        </Wrapper>
</Provider>
```

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

```tsx
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, () => {}]);
```

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

./components/RecipeForm.component.tsx

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

E nosso formulário fica assim:

./components/Form.component.tsx

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

Agora vamos adicionar nossa *createRecipeMutation:*

./components/Form.tsx

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

```

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

Vamos testar!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ejliec9b4cfhal8juca6.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4iybxawrkfkyq48dheul.png)

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

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

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

```tsx
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, () => {}]);
```

./RecipeCard.component.tsx

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

<ActionsWrapper>
          <UpdateButton onClick={openUpdateForm}>Atualizar</UpdateButton>
          <DeleteButton>Deletar</DeleteButton>
</ActionsWrapper
```

E nosso em nosso Form:

./components/Form.component.tsx

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/c8synlpm0tz5ut5zn71v.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wn9ti8tex8hkx5eq5ksp.png)

Brabo! Agora só precisamos implementar o *Delete*.

Então, vamos criar nossa *deleteRecipeMutation*

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

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

./components/RecipeCard.component.tsx

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

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](https://github.com/vinisaveg/urql-basics)

Link do meu post sobre TypeGraphQL

[TypeGraphQL, o básico em uma API de receitas](https://dev.to/vinisaveg/typegraphql-o-basico-em-uma-api-de-receitas-3ehb)

Happy Coding!