21
loading...
This website collects cookies to deliver better user experience
npx react-native init rickmortyshop --template react-native-template-typescript
/**
* @format
*/
import { AppRegistry } from 'react-native';
import App from './src/index';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
yarn add graphql @apollo/client graphql-tag
import { HttpLink } from '@apollo/client/link/http';
export function createHttpLink() {
return new HttpLink({
uri: 'https://rickandmortyapi.com/graphql',
});
}
import { onError } from '@apollo/client/link/error';
export const errorLink = onError(({ graphQLErrors, networkError, response, operation }) => {
if (graphQLErrors) {
for (const error of graphQLErrors) {
console.error(
`[GraphQL error]: Message: ${error.message}, Location: ${error.locations}, Path: ${error.path}`,
operation,
response
);
}
}
if (networkError) {
console.error(`[Network error]: ${networkError}`, operation, response);
}
});
import {InMemoryCache} from '@apollo/client';
export const localCache = new InMemoryCache();
freezeResults
anymore because now this is the default behavior.import { ApolloClient, ApolloLink } from '@apollo/client';
import { errorLink } from './apollo/error-link';
import { createHttpLink } from './apollo/http-link';
import { localCache } from './apollo/local-cache';
export function createApolloClient() {
const httpLink = createHttpLink();
const apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, httpLink]),
connectToDevTools: process.env.NODE_ENV !== 'production',
cache: localCache,
assumeImmutableResults: true,
});
return apolloClient;
}
ApolloClient
object. Now at src/index.tsx
we write:import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { Text, View } from 'react-native';
import { createApolloClient } from './common/config/apollo-client';
const apolloClient = createApolloClient();
const App = () => {
return (
<ApolloProvider client={apolloClient}>
<View>
<Text>Hello</Text>
</View>
</ApolloProvider>
);
};
export default App;
yarn graphql-codegen init
src/**/*.graphql
) for the fragment and operations.TypeScript
, TypeScript Operators
, TypeScript React Apollo
and Introspection Fragment Matcher
.src/common/generated/graphql.tsx
.gen-graphql
when it asks the name of the script in the package.json that will be used to generate the graphql files.yarn install
to install all necessary plugins.query GetCharacters {
characters {
__typename
results {
id
__typename
name
image
species
origin {
id
__typename
name
}
location {
id
__typename
name
}
}
}
}
yarn gen-graphql
import React from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
interface Props {
data: {
image?: string | null;
name?: string | null;
};
}
const CharacterCard: React.FC<Props> = ({ data }) => {
return (
<View style={styles.container}>
{data.image && (
<Image source={{ uri: data.image }} style={styles.image} />
)}
<View style={styles.details}>
<Text style={styles.text}>{data.name}</Text>
</View>
</View>
);
};
export default CharacterCard;
const styles = StyleSheet.create({
container: {
width: '100%',
borderRadius: 20,
marginVertical: 8,
paddingHorizontal: 8,
paddingVertical: 24,
backgroundColor: '#F0F0F0',
flexDirection: 'row',
},
image: { width: 70, height: 70 },
details: {
marginLeft: 8,
justifyContent: 'space-between',
flex: 1,
},
text: {
fontSize: 16,
fontWeight: 'bold',
},
});
import React from 'react';
import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native';
import { Character, useGetCharactersQuery } from '../common/generated/graphql';
import CharacterCard from '../common/components/CharacterCard';
const Home = () => {
const { data, loading } = useGetCharactersQuery();
if (loading) {
return (
<View style={styles.container}>
<ActivityIndicator color="#32B768" size="large" />
</View>
);
}
return (
<View style={styles.container}>
<FlatList
data={data?.characters?.results}
renderItem={({ item }) => <CharacterCard data={item as Character} />}
contentContainerStyle={styles.characterList}
/>
</View>
);
};
export default Home;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
characterList: {
padding: 16,
},
});
useGetCharactersQuery
hook provided by the graphql codegen. The hook provided useful fetch tools like the server response (data
) and the loading
state. Then, finally at src/index.tsx we write:import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { createApolloClient } from './common/config/apollo-client';
import Home from './screens/Home';
const apolloClient = createApolloClient();
const App = () => {
return (
<ApolloProvider client={apolloClient}>
<Home />
</ApolloProvider>
);
};
export default App;
yarn start
you will se your app cards with all Ricky Morty characters presented! type Query {
shoppingCart: ShoppingCart!
}
extend type Character {
chosenQuantity: Int!
unitPrice: Int!
}
type ShoppingCart {
id: ID!
totalPrice: Int!
numActionFigures: Int!
}
fragment characterData on Character {
id
__typename
name
unitPrice @client
chosenQuantity @client
}
query GetCharacters {
characters {
__typename
results {
...characterData
image
species
origin {
id
__typename
name
}
location {
id
__typename
name
}
}
}
}
query GetShoppingCart {
shoppingCart @client {
id
totalPrice
numActionFigures
}
}
import { gql, InMemoryCache } from '@apollo/client';
export const localCache = new InMemoryCache();
export const LocalCacheInitQuery = gql`
query LocalCacheInit {
shoppingCart
}
`;
export function initialLocalCache() {
localCache.writeQuery({
query: LocalCacheInitQuery,
data: {
shoppingCart: null,
},
});
}
import { ApolloClient, ApolloLink } from '@apollo/client';
import { errorLink } from './apollo/error-link';
import { createHttpLink } from './apollo/http-link';
import { initialLocalCache, localCache } from './apollo/local-cache';
export function createApolloClient() {
const httpLink = createHttpLink();
const apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, httpLink]),
connectToDevTools: process.env.NODE_ENV !== 'production',
cache: localCache,
assumeImmutableResults: true,
});
return apolloClient;
}
initialLocalCache();
import { gql, InMemoryCache } from '@apollo/client';
export const localCache = new InMemoryCache({
typePolicies: {
Character: {
fields: {
chosenQuantity: {
read(chosenQuantity) {
if (chosenQuantity === undefined || chosenQuantity === null) {
return 0;
}
return chosenQuantity;
},
},
unitPrice: {
read(_, { readField }) {
const charName = readField('name');
switch (charName) {
case 'Albert Einstein':
return 25;
case 'Rick Sanchez':
case 'Morty Smith':
return 10;
default:
return 5;
}
},
},
},
},
},
});
export const LocalCacheInitQuery = gql`
query LocalCacheInit {
shoppingCart
}
`;
export function initialLocalCache() {
localCache.writeQuery({
query: LocalCacheInitQuery,
data: {
shoppingCart: null,
},
});
}
chosenQuantity
field. If this field is not falsy
we return its value, if not, return 0
. Since this is a local field, the value obtained from the server is initially falsy.readField
function helper, passing the name of the field that we are interested. In this particular case, we are looking for change the character's unitPrice
based on its own name
.overwrite: true
schema: "https://rickandmortyapi.com/graphql"
documents: "src/**/*.graphql"
generates:
src/common/generated/graphql.tsx:
schema: "./src/common/config/local-schema.graphql"
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
- "fragment-matcher"
src/common/generated/fragment-matcher.json:
schema: "./src/common/config/local-schema.graphql"
plugins:
- "fragment-matcher"
yarn gen-graphql
yarn add react-native-vector-icons @types/react-native-vector-icons @react-navigation/native react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
import React from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
import { RectButton } from 'react-native-gesture-handler';
import Icon from 'react-native-vector-icons/Entypo';
interface Props {
data: {
image?: string | null;
name?: string | null;
unitPrice?: number;
chosenQuantity?: number;
};
}
const CharacterCard: React.FC<Props> = ({ data }) => {
return (
<View style={styles.container}>
{data.image && (
<Image source={{ uri: data.image }} style={styles.image} />
)}
<View style={styles.details}>
<Text style={styles.text}>{data.name}</Text>
<Text style={styles.text}>{`U$ ${data.unitPrice}`}</Text>
</View>
<View style={styles.choseQuantityContainer}>
<RectButton>
<Icon name="minus" size={24} color="#3D7199" />
</RectButton>
<Text style={styles.choseQuantityText}>{data.chosenQuantity}</Text>
<RectButton>
<Icon name="plus" size={24} color="#3D7199" />
</RectButton>
</View>
</View>
);
};
export default CharacterCard;
const styles = StyleSheet.create({
container: {
width: '100%',
borderRadius: 20,
marginVertical: 8,
paddingHorizontal: 8,
paddingVertical: 24,
backgroundColor: '#F0F0F0',
flexDirection: 'row',
},
image: { width: 70, height: 70 },
details: {
marginLeft: 8,
justifyContent: 'space-between',
flex: 1,
},
text: {
fontSize: 16,
fontWeight: 'bold',
},
choseQuantityContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
},
choseQuantityText: {
padding: 8,
borderRadius: 8,
backgroundColor: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
unitPrice
, chosenQuantity
and the RectButton
to increase and decrease the quantities. Now we will build this logic to update the chosenQuantity
at src/common/hooks/use-update-chosen-quantity.ts:import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import {
CharacterDataFragment,
CharacterDataFragmentDoc,
GetShoppingCartDocument,
GetShoppingCartQuery,
} from '../generated/graphql';
interface UpdateChosenQuantity {
(): {
onIncreaseChosenQuantity: (id: string) => void;
onDecreaseChosenQuantity: (id: string) => void;
};
}
export const useUpdateChosenQuantity: UpdateChosenQuantity = () => {
const client = useApolloClient();
const getCharacter = useCallback(
(id: string) =>
client.readFragment<CharacterDataFragment>({
fragment: CharacterDataFragmentDoc,
id: `Character:${id}`,
}),
[client],
);
const getShoppingCartParams = useCallback(() => {
const shoppingCart = client.readQuery<GetShoppingCartQuery>({
query: GetShoppingCartDocument,
})?.shoppingCart;
if (!shoppingCart) {
return {
id: 'ShoppingCart:1',
totalPrice: 0,
numActionFigures: 0,
};
}
return {
...shoppingCart,
};
}, [client]);
const increaseShoppingCart = useCallback(
(unitPrice: number) => {
let { id, totalPrice, numActionFigures } = getShoppingCartParams();
totalPrice = totalPrice + unitPrice;
numActionFigures = numActionFigures + 1;
client.writeQuery<GetShoppingCartQuery>({
query: GetShoppingCartDocument,
data: {
shoppingCart: {
id,
numActionFigures,
totalPrice,
},
},
});
},
[client, getShoppingCartParams],
);
const decreaseShoppingCart = useCallback(
(unitPrice: number) => {
let { id, totalPrice, numActionFigures } = getShoppingCartParams();
totalPrice = totalPrice - unitPrice;
numActionFigures = numActionFigures - 1;
if (totalPrice < 0) {
totalPrice = 0;
}
if (numActionFigures < 0) {
numActionFigures = 0;
}
client.writeQuery<GetShoppingCartQuery>({
query: GetShoppingCartDocument,
data: {
shoppingCart: {
id,
numActionFigures,
totalPrice,
},
},
});
},
[client, getShoppingCartParams],
);
const onIncreaseChosenQuantity = useCallback(
(id: string) => {
const character = getCharacter(id);
client.writeFragment<CharacterDataFragment>({
fragment: CharacterDataFragmentDoc,
id: `Character:${id}`,
data: {
...(character as CharacterDataFragment),
chosenQuantity: (character?.chosenQuantity ?? 0) + 1,
},
});
increaseShoppingCart(character?.unitPrice as number);
},
[client, getCharacter, increaseShoppingCart],
);
const onDecreaseChosenQuantity = useCallback(
(id: string) => {
const character = getCharacter(id);
let chosenQuantity = (character?.chosenQuantity ?? 0) - 1;
if (chosenQuantity < 0) {
chosenQuantity = 0;
}
client.writeFragment<CharacterDataFragment>({
fragment: CharacterDataFragmentDoc,
id: `Character:${id}`,
data: {
...(character as CharacterDataFragment),
chosenQuantity,
},
});
decreaseShoppingCart(character?.unitPrice as number);
},
[client, getCharacter, decreaseShoppingCart],
);
return {
onIncreaseChosenQuantity,
onDecreaseChosenQuantity,
};
};
useApolloClient()
hook.getCharacter
to read our character fragment, passing the fragment doc and the id
of the fragment (usually the apollo saves the fragments in a normalized way, using the typename:id
as a unique key).getShoppingCartParams
to retrive the shoppingCart
data from the cache. If the shoppingCart
is null we return some default values.increaseShoppingCart
we retrieve the data from getShoppingCartParams
and add the unitPrice
from the character being edited. The same happens to decreaseShoppingCart.onIncreaseChosenQuantity
we getCharacter
, update his chosenQuantity properly and passes its unitPrice
to the increaseShoppingCart
. The similar ocurs with the onDecreaseChosenQuantity.import React from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
import { RectButton } from 'react-native-gesture-handler';
import Icon from 'react-native-vector-icons/Entypo';
import { useUpdateChosenQuantity } from '../hooks/use-update-chosen-quantity';
interface Props {
data: {
id?: string | null;
image?: string | null;
name?: string | null;
unitPrice?: number;
chosenQuantity?: number;
};
}
const CharacterCard: React.FC<Props> = ({ data }) => {
const { onIncreaseChosenQuantity, onDecreaseChosenQuantity } =
useUpdateChosenQuantity();
return (
<View style={styles.container}>
{data.image && (
<Image source={{ uri: data.image }} style={styles.image} />
)}
<View style={styles.details}>
<Text style={styles.text}>{data.name}</Text>
<Text style={styles.text}>{`U$ ${data.unitPrice}`}</Text>
</View>
<View style={styles.choseQuantityContainer}>
<RectButton
onPress={onDecreaseChosenQuantity.bind(null, data.id as string)}>
<Icon name="minus" size={24} color="#3D7199" />
</RectButton>
<Text style={styles.choseQuantityText}>{data.chosenQuantity}</Text>
<RectButton
onPress={onIncreaseChosenQuantity.bind(null, data.id as string)}>
<Icon name="plus" size={24} color="#3D7199" />
</RectButton>
</View>
</View>
);
};
export default CharacterCard;
const styles = StyleSheet.create({
container: {
width: '100%',
borderRadius: 20,
marginVertical: 8,
paddingHorizontal: 8,
paddingVertical: 24,
backgroundColor: '#F0F0F0',
flexDirection: 'row',
},
image: { width: 70, height: 70 },
details: {
marginLeft: 8,
justifyContent: 'space-between',
flex: 1,
},
text: {
fontSize: 16,
fontWeight: 'bold',
},
choseQuantityContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-between',
flexDirection: 'row',
},
choseQuantityText: {
padding: 8,
borderRadius: 8,
backgroundColor: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
onPress
event listener using our local api useUpdateChosenQuantity
.src/screens/Cart.tsx
):import React, { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { StyleSheet, Text, View, SafeAreaView, Button } from 'react-native';
import { useGetShoppingCartQuery } from '../common/generated/graphql';
const Cart = () => {
const navigation = useNavigation();
const { data } = useGetShoppingCartQuery();
const handleNavigation = useCallback(() => {
navigation.navigate('Home');
}, [navigation]);
return (
<SafeAreaView style={styles.container}>
{data?.shoppingCart?.numActionFigures ? (
<>
<View style={styles.content}>
<Text style={styles.emoji}>🤗</Text>
<Text
style={
styles.subtitle
}>{`Total number of items: ${data?.shoppingCart.numActionFigures}`}</Text>
<Text
style={
styles.subtitle
}>{`Total price: U$ ${data?.shoppingCart.totalPrice}`}</Text>
</View>
</>
) : (
<>
<View style={styles.content}>
<Text style={styles.emoji}>😢</Text>
<Text style={styles.title}>Empty cart!</Text>
<View style={styles.footer}>
<Button title="Go back to shop" onPress={handleNavigation} />
</View>
</View>
</>
)}
</SafeAreaView>
);
};
export default Cart;
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
content: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
title: {
fontSize: 24,
marginTop: 15,
lineHeight: 32,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
lineHeight: 32,
marginTop: 8,
textAlign: 'center',
paddingHorizontal: 20,
},
emoji: {
fontSize: 44,
textAlign: 'center',
},
footer: {
width: '100%',
paddingHorizontal: 20,
},
});
shoppingCart
query to printing the info. Finally we install the react-native bottom tabs:yarn add @react-navigation/bottom-tabs
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useGetShoppingCartQuery } from './common/generated/graphql';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import Home from './screens/Home';
import Cart from './screens/Cart';
const TabRoutes = createBottomTabNavigator();
const Routes: React.FC = () => {
const { data } = useGetShoppingCartQuery();
return (
<TabRoutes.Navigator
tabBarOptions={{
labelPosition: 'beside-icon',
style: {
height: 64,
alignItems: 'center',
},
}}>
<TabRoutes.Screen
name="Home"
component={Home}
options={{
tabBarIcon: ({ size, color }) => (
<MaterialIcons size={size * 1.2} color={color} name="home" />
),
}}
/>
<TabRoutes.Screen
name="Cart"
component={Cart}
options={{
tabBarIcon: ({ size, color }) =>
data?.shoppingCart?.numActionFigures ? (
<View style={styles.badgeIconView}>
<Text style={styles.badge}>
{data?.shoppingCart?.numActionFigures}
</Text>
<MaterialIcons
size={size * 1.2}
color={color}
name="shopping-cart"
/>
</View>
) : (
<MaterialIcons
size={size * 1.2}
color={color}
name="shopping-cart"
/>
),
}}
/>
</TabRoutes.Navigator>
);
};
export default Routes;
const styles = StyleSheet.create({
badgeIconView: {
position: 'relative',
},
badge: {
position: 'absolute',
zIndex: 10,
left: 24,
bottom: 20,
padding: 1,
borderRadius: 20,
fontSize: 14,
},
});
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { NavigationContainer } from '@react-navigation/native';
import { createApolloClient } from './common/config/apollo-client';
import Routes from './routes';
const apolloClient = createApolloClient();
const App = () => {
return (
<ApolloProvider client={apolloClient}>
<NavigationContainer>
<Routes />
</NavigationContainer>
</ApolloProvider>
);
};
export default App;