URQL, základy

Nedávno jsem napsal příspěvek o TypeGraphQL a o tom, jak používat framework pro neuvěřitelné výhody ve vašem GraphQL API s Typescriptem. Pro doplnění vám tentokrát ukážu něco o URQL, klientovi GraphQL. S ním spotřebujeme API receptury – což je v tomto případě stejné API, jaké jsem vytvořil ve svém předchozím příspěvku.

K tomu použijeme React.js k vytvoření našeho CRUD 😊

Nejprve vám dlužím krátké představení tohoto klienta.

URQL je klient GraphQL se zaměřením na použitelnost a přizpůsobivost, s rychlým nastavením a snadným používáním, který je schopen podporovat velmi pokročilé infrastruktury v GraphQL.

urql dokumentace

POJĎME KÓDOVAT!

Nejprve vytvořte nový projekt.

Vytvoříme složku pro projekt. (Jméno je na vás, abyste se rozhodli)

mkdir urql-basics
cd urql-basics

Pojďme inicializovat projekt pomocí šablony React.js s Typescriptem. Můžete použít npx nebo příze . Použiji přízi .

yarn create react-app . --template typescript

Po inicializaci projektu nainstalujme URQL.

yarn add urql graphql

Nyní, když je vše nainstalováno, můžeme odstranit některé soubory, které nebudeme používat.

Budeme potřebovat pouze následující:

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

Aplikace musí běžet na portu 3000 👍🏼

V příkladech budu používat styled-components pomoci se stylem aplikace. Pokud chcete jinak, žádný problém.

btw CSS v JS =💘

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

Pomocí stylizovaných komponent můžeme ve skutečnosti vytvořit komponentu React se všemi jejími styly. Z "Tagged Model Literals" vytvoříme celý styl komponenty. Toto označení je jednoduše CSS/Sass.

Více zde:

styled-components:Basics

Nejprve nakonfigurujeme URQL a vytvoříme našeho poskytovatele.

Ve složce ./api, Vytvořil jsem soubor s názvem urql.ts.

V tomto souboru vyexportujeme klienta

import { createClient } from 'urql';

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

Aby vše fungovalo, předáme objekt s nějakým nastavením funkci, která vrací Klient .

V našem případě předáme pouze minimum, což by byla url z našeho GraphQL API

Nyní pro začátek vytvoříme poskytovatele pro naši aplikaci, abychom mohli využívat klienta.

Vzhledem k tomu, že tento poskytovatel používá kontextové rozhraní API, zabalíme do něj naši aplikaci.

V naší app.tsx

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

const App: FunctionComponent = () => {

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

Ve své aplikaci jsem nakonec vytvořil Wrapper komponenty, abych vycentroval obsah uprostřed obrazovky

Všechny mé komponenty budou ve složce ./components, A každý z nich ve složce s vlastními styly.

Aby tento příspěvek nebyl příliš dlouhý, projdu si styling a zaměřím se více na URQL. Ale nebojte se, vše zpřístupním v úložišti na Github 😎

Nyní, když máme našeho klienta nakonfigurovaného, ​​vytvoříme náš první dotaz, který bude načítat recepty z mého API.

Uvnitř ./src Vytvořím složku ./graphql. Do něj můžeme vložit naše Mutace a dotazy

.src/graphql/queries/recipesQuery.ts

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

Jednoduše můj dotaz je String , se syntaxí GraphQL.

Chcete-li spustit náš dotaz, vytvořte komponentu, která bude vypisovat všechny naše recepty.

./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;

Pomocí háčku useQuery poskytnutý samotným URQL, odešleme náš dotaz, který přinese n-tici obsahující objekt s výsledkem dotazu a funkci opětovného spuštění.

Tento objekt bude obsahovat:

  • data ⇒ Data získaná z rozhraní API
  • načítání ⇒ Indikace načítání dat.
  • chyba ⇒ Chyby připojení nebo dokonce GraphQLErrors

Takže pomocí dat zobrazíme na obrazovce všechny existující receptury.

K tomu jsem vytvořil komponentu RecipeCard, která je naplněna informacemi o receptech.

./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;

Neuvěřitelný! 🚀

Nyní přidáme mutaci vytvořit nový recept.

Pojďme vytvořit 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
            }
        }
    }
`;

V případě API receptur musíme poslat název, popis a seznam ingrediencí, přičemž každou uvedeme na začátku naší mutace.

S naší createRecipeMutation připravenou, pojďme vytvořit formulář pro registraci receptu. K tomu použiji Formik, což je knihovna pro správu formulářů.

Pokud nevíte, doporučuji se podívat:

Formik

Aby byla aplikace čistší a jednodušší, použiji jeden formulář, a to jak pro aktualizaci, tak pro vytvoření.

Pro otevření formuláře Vytvořit jsem vytvořil tlačítko a přidal ho do app.tsx

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

          <RecipesList />

          <Recipeform />

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

Pro sdílení toho, který formulář je otevřený a který je zavřený, jsem použil Context API ke sdílení dvou atributů, které označují, který z formulářů se otevře. Buď Vytvořit, nebo Aktualizovat.

Uvnitř ./context jsem vytvořil kontext aplikace.

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

Abych zkontroloval stav formulářů, vytvořil jsem komponentu, která vykreslí pouze formulář, který byl požadován.

./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;

A náš formulář vypadá takto:

./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;

Nyní přidáme naši 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;

Pomocí háčku useMutation , budeme mít objekt s výsledkem a funkcí pro provedení mutace .

Pojďme to otestovat!

Ukázat! 🔥

Nyní pro naši aktualizaci Mutation uděláme něco velmi podobného.

Tentokrát však budeme muset odeslat ID receptu, který chceme aktualizovat.

./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
        }
    }
`;

Takže na naší RecipeCard použijeme tlačítko aktualizace ke spuštění procesu aktualizace.

V aplikaci jsem také použil kontextové API ke sdílení ID receptu, který bude aktualizován. A v tomto případě, jak víme, otevřeme formulář Aktualizace.

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

A naše v našem formuláři:

./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;

Rozzlobený! Nyní zbývá implementovat Delete .

Pojďme tedy vytvořit naši deleteRecipeMutation

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

A abychom mohli odeslat tuto mutaci, přidejte funkci do našeho tlačítka Smazat.

./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;

Nyní ano, máme náš CRUD s URQL 🎉 🎉

Doufám, že tento malý úvod byl užitečný 😊

Stálo to! ♥️

Odkaz na projekt na Github:

vinisaveg/urql-basics

Odkaz na můj příspěvek o TypeGraphQL

TypeGraphQL, základy rozhraní API receptů

Šťastné kódování!