Saleor
Published in

Saleor

Creating a Type-Safe React Application with TypeScript using the Saleor GraphQL API

Using Saleor with React and TypeScript

Introduction

Prerequisites

Creating a Basic React Application with TypeScript

npx create-react-app saleor-demo --template typescript
yarn start

Fetching the Saleor GraphQL API

query getLatestProducts {
products(first: 5) {
edges {
node {
id
name
description
}
}
}
}

Setup Apollo Client

yarn add @apollo/client graphql
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { ApolloProvider } from '@apollo/client';
import App from './App';

const client = new ApolloClient({
uri: 'https://demo.saleor.io/graphql/',
cache: new InMemoryCache(),
});

ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById('root'),
);

Query Data using Apollo React Hooks

// src/config.tsx
import { gql } from '@apollo/client';

export const GET_LATEST_PRODUCTS = gql`
query getLatestProducts {
products(first: 5) {
edges {
node {
id
name
description
}
}
}
}
`;
// src/Products.tsx
import { useQuery } from '@apollo/client';
import { GET_LATEST_PRODUCTS } from './config';

function Products() {
const { loading, error, data } = useQuery<any>(GET_LATEST_PRODUCTS);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;

if (data) {
const latestProducts = data.products?.edges || [];

return (
<div>
{latestProducts?.length > 0 &&
latestProducts.map(({ node: { id, name, description } }) => (
<div key={id}>
<h3>{name}</h3>
<p>{description}</p>
</div>
))}
</div>
);
}

return null;
}

export default Products;
// src/Products.tsx

//..

type Product = {
id: string;
name: string;
description: string;
};

type LatestProducts = {
products: {
edges: Array<{
node: Product;
}>;
};
};

function Products() {
const { loading, error, data } = useQuery<LatestProducts>(GET_LATEST_PRODUCTS);

// ...
}
// src/App.tsx
import Products from './Products';

function App() {
return (
<div>
<header>
<h1>Saleor React Application</h1>
</header>
<Products />
</div>
);
}

export default App;

Filtering and Sorting Products

query getLatestProducts {
products(first: 5, filter: { search: "juice" }) {
edges {
node {
id
name
description
}
}
}
}
// src/config.tsx
import { gql } from '@apollo/client';

export const GET_LATEST_PRODUCTS = gql`
query getLatestProducts($keyword: String) {
products(first: 5, filter: { search: $keyword }) {
edges {
node {
// ...
// src/Products.tsx

type ProductsProps = {
keyword?: string;
};

function Products({ keyword = 'juice' }: ProductsProps) {
const { loading, error, data } = useQuery<LatestProducts>(GET_LATEST_PRODUCTS, {
variables: {
keyword,
},
});

// ...
// src/App.tsx
import { useState } from 'react';
import Products from './Products';
import SearchBar from './SearchBar';

function App() {
const [keyword, setKeyword] = useState('');

return (
<div>
<header>
<h1>Saleor React Application</h1>
</header>
<SearchBar setKeyword={setKeyword} />
<Products keyword={keyword} />
</div>
);
}

export default App;
// src/SearchBar.tsx
import { Dispatch, SetStateAction, useState } from 'react';

type SearchBarProps = {
setKeyword: Dispatch<SetStateAction<string>>;
};

function SearchBar({ setKeyword }: SearchBarProps) {
const [value, setValue] = useState('');

function onSubmit(e: React.FormEvent<EventTarget>) {
e.preventDefault();

setKeyword(value);
}

return (
<form onSubmit={onSubmit}>
<label>
Search:
<input
type='text'
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
</label>
<button type='submit'>
Submit
</button>
</form>
);
}

export default SearchBar;

Using GraphQL Code Generator to autogenerate type-safe components

yarn add -D @graphql-codegen/cli
yarn graphql-codegen init
Welcome to GraphQL Code Generator!
Answer few questions and we will set up everything for you.? What type of application are you building? Application built with React
? Where is your schema?: (path or url) https://demo.saleor.io/graphql/
? Where are your operations and fragments?: src/**/*.tsx
? Pick plugins: TypeScript (required by other typescript plugins), TypeScript Operations (operations and fragmen
ts), TypeScript React Apollo (typed components and HOCs)
? Where to write the output: src/generated/graphql.tsx
? Do you want to generate an introspection file? Yes
? How to name the config file? codegen.yml
? What script in package.json should run the codegen? generate
yarn && yarn generate
// src/Products.tsx
import { useGetLatestProductsQuery } from './generated/graphql';

type ProductsProps = {
keyword?: string;
};

function Products({ keyword = 'juice' }: ProductsProps) {
const { loading, error, data } = useGetLatestProductsQuery({
variables: {
keyword,
},
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;

if (data) {
const latestProducts = data.products?.edges || [];

return (
<div>
{latestProducts?.length > 0 &&
latestProducts.map(({ node: { id, name, description } }) => (
<div key={id}>
<h3>{name}</h3>
<p>{description}</p>
</div>
))}
</div>
);
}

return null;
}

export default Products;
// src/config.tsx
import { gql } from '@apollo/client';

export const GET_LATEST_PRODUCTS = gql`
query getLatestProducts($keyword: String) {
products(first: 5, filter: { search: $keyword }) {
edges {
node {
id
name
description
category {
name
}
}
}
}
}
`;
// src/Products.tsx

// ...

return (
<div>
{latestProducts?.length > 0 &&
latestProducts.map(({ node: { id, name, description, category } }) => (
<div key={id}>
<h3>{name}</h3>
<p>{description}</p>
<p>{category?.name}</p>
</div>
))}
</div>
);
}

return null;
}

export default Products;

Styling with Bootstrap

yarn add bootstrap@next
// src/index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { ApolloProvider } from '@apollo/client';
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';

const client = new ApolloClient({
// ...
// src/App.tsx

// ...

function App() {
const [keyword, setKeyword] = useState('');

return (
<div className='container'>
<div className='row'>
<header className='navbar navbar-light bg-light'>
<div className='container-fluid'>
<h1 className='navbar-brand'>Saleor React Application</h1>
</div>
</header>
</div>
<div className='row'>
<SearchBar setKeyword={setKeyword} />
</div>
<div className='row'>
<Products keyword={keyword} />
</div>
</div>
);
}

export default App;
// src/SearchBar.tsx

// ...

return (
<form className='row g-3' onSubmit={onSubmit}>
<div className='col-auto'>
<label htmlFor='search' className='col-sm-2 col-form-label'>
Search:
</label>
</div>
<div className='col-auto'>
<input
id='search'
className='form-control'
type='text'
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
</div>
<div className='col-auto'>
<button type='submit' className='btn btn-primary mb-3'>
Submit
</button>
</div>
</form>
);
}

export default SearchBar;
// src/Products.tsx

// ...

return (
<div className='d-grid gap-3'>
{latestProducts?.length > 0 &&
latestProducts.map(
({ node: { id, name, description, category } }) => (
<div key={id} className='card'>
<div className='card-body'>
<h3 className='card-title'>{name}</h3>
<p className='card-subtitle'>{category?.name}</p>
<p>{description}</p>
</div>
</div>
),
)}
</div>
);
}

return null;
}

export default Products;

Conclusion

--

--

A GraphQL-first, headless e-commerce platform for perfectionists. Written in Python. Best served as a bespoke, high-performance e-commerce solution.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Roy Derks (@gethackteam)

Roy is an entrepreneur, speaker and author from The Netherlands. Most recently he wrote the books Fullstack GraphQL and React Projects.