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.
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
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
Com o projeto inicializado, vamos instalar o URQL.
yarn add urql graphql
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
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 = 💘
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:
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/',
});
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>
);
};
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
}
}
`;
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;
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;
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
}
}
}
`;
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:
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>
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, () => {}]);
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;
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;
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;
Utilizando o hook de useMutation, vamos ter um objeto com o resultado e uma função para executar a Mutation.
Vamos testar!
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
}
}
`;
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, () => {}]);
./RecipeCard.component.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
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;
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
}
}
`;
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;
Agora sim, temos nosso CRUD com URQL 🎉 🎉
Espero que essa pequena introdução tenha sido útil 😊
Valeu! ♥️
Link do projeto no Github:
Link do meu post sobre TypeGraphQL
TypeGraphQL, o básico em uma API de receitas
Happy Coding!
Top comments (3)
would it be possible to get the server code api used too
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.
no probkrm I love Portuguese