Building Dad Jokes using The Prisma Framework (formerly Prisma 2) and React Native

Akshay Kadam (A2K) - Jan 15 '20 - - Dev Community

The Prisma Framework (formerly known as Prisma 2) is a complete rewrite of the original Prisma. It is being rewritten in Rust while the original was written in Scala. The original version had memory issues with it and it required JVM to run. It also needed an additional server to run in addition to a backend server. The newest version of Prisma does not require any such thing. With The Prisma Framework, the query engine is now a bundled executable that is run alongside the backend on the same server.

The Prisma Framework consists of 3 standalone tools to tackle the problems of data access, data migrations, and admin UI:

  • Photon: Type-safe and auto-generated database client ("ORM replacement")
  • Lift: Declarative migration system with custom workflows
  • Studio: Provides Admin UI to support various database workflows

So now let's get started with building a server with The Prisma Framework.

To keep it fun and corny, we will be making a Dad Jokes App.

Prerequisites

For this tutorial, you need a basic knowledge of React Native. You also need to understand React Hooks.

Since this tutorial is primarily focused on Prisma, it is assumed that you already have a working knowledge of React and its basic concepts.

Throughout the course of this tutorial, we’ll be using yarn. If you don’t have yarn already installed, install it from here.

To make sure we’re on the same page, these are the versions used in this tutorial:

  • Node v12.12.0
  • npm v6.11.3
  • npx v6.11.3
  • yarn v1.19.1
  • prisma2 v2.0.0-preview016.2
  • expo-cli v3.7.1
  • expo v35.0.0

Server-Side (The Prisma Framework)

Start a new Prisma 2 project

Install prisma2 CLI globally and run the init command then:

$ yarn global add prisma2 // or npm install --global prisma2
$ prisma2 init server
Enter fullscreen mode Exit fullscreen mode

Run the interactive prisma2 init flow & select boilerplate

Select the following in the interactive prompts:

  1. Select Starter Kit
  2. Select JavaScript
  3. Select GraphQL API
  4. Select SQLite

Once terminated, the init command will have created an initial project setup in the server/ folder.

Now open the schema.prisma file and replace it with the following:

generator photon {
  provider = "photonjs"
}

datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
}

model Joke {
  id   String @default(cuid()) @id
  joke String @unique
}
Enter fullscreen mode Exit fullscreen mode

schema.prisma contains the data model as well as the configuration options.

Here, we specify that we want to connect to the SQLite datasource called dev.db as well as target code generators like photonjs generator.

Then we define the data model Joke which consists of id and joke.

id is a primary key of type String with a default value of cuid().

joke is of type String but with a constraint that it must be unique.

Open seed.js file and paste the following:

const { Photon } = require('@generated/photon')
const photon = new Photon()

async function main() {
  const joke1 = await photon.jokes.create({
    data: {
      joke:
        'Did you hear the one about the guy with the broken hearing aid? Neither did he.',
    },
  })
  const joke2 = await photon.jokes.create({
    data: {
      joke:
        'My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.',
    },
  })
  const joke3 = await photon.jokes.create({
    data: {
      joke: "I don't trust stairs. They're always up to something.",
    },
  })
  const joke4 = await photon.jokes.create({
    data: {
      joke:
        "Dad died because he couldn't remember his blood type. I will never forget his last words. Be positive.",
    },
  })
  console.log({ joke1, joke2, joke3, joke4 })
}

main()
  .catch(e => console.error(e))
  .finally(async () => {
    await photon.disconnect()
  })
Enter fullscreen mode Exit fullscreen mode

We are basically adding jokes into our SQLite database.

Now go inside src/index.js file and remove the contents of it. We'll start adding content from scratch.

First go ahead and import the necessary packages and declare some constants:

const { GraphQLServer } = require('graphql-yoga')
const {
  makeSchema,
  objectType,
  queryType,
  mutationType,
  idArg,
  stringArg,
} = require('nexus')
const { Photon } = require('@generated/photon')
const { nexusPrismaPlugin } = require('nexus-prisma')
Enter fullscreen mode Exit fullscreen mode

We have declared a constant photon which instantiates a new Photon class.

Let us declare our Joke model. Paste the code below it:

const Joke = objectType({
  name: 'Joke',
  definition(t) {
    t.model.id()
    t.model.joke()
  },
})
Enter fullscreen mode Exit fullscreen mode

We make use of objectType from the nexus package to declare Joke.

The name parameter should be the same as defined in the schema.prisma file.

The definition function lets you expose a particular set of fields wherever Joke is referenced. Here, we expose id and joke field.

If we expose only joke field, then id will not get exposed and only joke will get exposed wherever Joke is referenced.

Below that paste the Query constant:

const Query = queryType({
  definition(t) {
    t.crud.joke()
    t.crud.jokes()
  },
})
Enter fullscreen mode Exit fullscreen mode

We make use of queryType from the nexus package to declare Query.

The Photon generator generates an API that exposes CRUD functions on Joke model. This is what allows us to expose t.crud.joke() and t.crud.jokes() method.

We can also write t.crud.jokes() as follows:

t.list.field('jokes', {
  type: 'Joke',
  resolve: (_, _args, ctx) => {
    return ctx.photon.jokes.findMany()
  },
})
Enter fullscreen mode Exit fullscreen mode

Both the above code and t.crud.jokes() will give the same results.

In the above code, we make a field named jokes. The return type is Joke. We then call ctx.photon.jokes.findMany() to get all the jokes from our SQLite database.

Note that the name of the jokes property is auto-generated using the pluralize package. It is therefore recommended to name our models singular i.e. Joke and not Jokes.

We use the findMany method on jokes which returns a list of objects. We find all the jokes as we have mentioned no condition inside of findMany. You can learn more about how to add conditions inside of findMany here.

Below Query, paste Mutation as follows:

const Mutation = mutationType({
  definition(t) {
    t.crud.createOneJoke({ alias: 'createJoke' })
    t.crud.deleteOneJoke({ alias: 'deleteJoke' })
  },
})
Enter fullscreen mode Exit fullscreen mode

Mutation uses mutationType from the nexus package.

The CRUD API here exposes createOneJoke and deleteOneJoke.

createOneJoke, as the name suggests, creates a joke whereas deleteOneJoke deletes a joke.

createOneJoke is aliased as createJoke so while calling the mutation we call createJoke rather than calling createOneJoke.

Similarly, we call deleteJoke instead of deleteOneJoke.

Finally, put the following code below Mutation:

const photon = new Photon()

new GraphQLServer({
  schema: makeSchema({
    types: [Query, Mutation, Joke],
    plugins: [nexusPrismaPlugin()],
  }),
  context: { photon },
}).start(() =>
  console.log(
    `🚀 Server ready at: http://localhost:4000\n⭐️ See sample queries: http://pris.ly/e/js/graphql#5-using-the-graphql-api`,
  ),
)

module.exports = { Joke }
Enter fullscreen mode Exit fullscreen mode

We use the makeSchema method from the nexus package to combine our model Quote, add Query and Mutation to the types array. We also add nexusPrismaPlugin to our plugins array. Finally, we start our server at http://localhost:4000/. Port 4000 is the default port for graphql-yoga. You can change the port as suggested here.

Let's start the server now. But first, we need to make sure our latest schema changes are written to the node_modules/@generated/photon directory. This happens when you run prisma2 generate. After that, we need to migrate our database to create tables.

Migrate your database with Lift

Migrating your database with Lift follows a 2-step process:

  1. Save a new migration (migrations are represented as directories on the file system)
  2. Run the migration (to migrate the schema of the underlying database)

In CLI commands, these steps can be performed as follows (the CLI steps are in the process of being updated to match):

$ prisma2 lift save --name 'init'
$ prisma2 lift up
Enter fullscreen mode Exit fullscreen mode

Now the migration process is done. We've successfully created the table. Now we can seed our database with initial values.

Go ahead and run the following command in the terminal:

$ yarn seed
Enter fullscreen mode Exit fullscreen mode

This will seed our database with 8 habits as specified in our seed.js file.

Now you can run the server by typing:

$ yarn dev
Enter fullscreen mode Exit fullscreen mode

This will run your server at http://localhost:4000/ which you can open and query all the APIs you've made.

List all jokes

query jokes {
  jokes {
    id
    joke
  }
}
Enter fullscreen mode Exit fullscreen mode

Dad Jokes - List All Jokes GraphiQL

Find A Particular Joke

query joke {
  joke(
    where: {
      joke: "Did you hear the one about the guy with the broken hearing aid? Neither did he."
    }
  ) {
    id
    joke
  }
}
Enter fullscreen mode Exit fullscreen mode

Dad Jokes - Find A Particular Joke GraphiQL

Create A Joke

mutation createJoke {
  createJoke(
    data: { joke: "To the guy who invented zero... thanks for nothing." }
  ) {
    id
    joke
  }
}
Enter fullscreen mode Exit fullscreen mode

Dad Jokes - Create A Joke GraphiQL

Delete a joke

mutation deleteJoke {
  deleteJoke(where: { id: "ck2zqhwvo0001cav551f1me34" }) {
    id
    joke
  }
}
Enter fullscreen mode Exit fullscreen mode

Dad Jokes - Delete A Joke GraphiQL

This is all we need for the backend. Let's work on the frontend now.

Client-Side (React Native)

Bootstrap a new Expo project

Let’s set up a new Expo project using expo-cli. Firstly, make sure to install it globally and then run the init command:

$ yarn global add expo-cli
$ expo init DadJokes
Enter fullscreen mode Exit fullscreen mode

Select the following in the interactive prompts:

  1. Select tabs
  2. Type name of the project to be DadJokes
  3. Press y to install dependencies with yarn

This should bootstrap a new React Native project using expo-cli.

Now run the project by typing:

$ yarn start
Enter fullscreen mode Exit fullscreen mode

Press i to run the iOS Simulator. This will automatically run the iOS Simulator even if it’s not opened.

Press a to run the Android Emulator. Note that the emulator must be installed and started already before typing a. Otherwise, it will throw an error in the terminal.

It should look like this:

Dad Jokes - Expo Init

React Navigation

The initial setup has already installed react-navigation for us. The bottom tab navigation also works by default because we chose tabs in the second step of expo init. You can check it by tapping on Links and Settings.

The screens/ folder is responsible for the content displayed when the tabs are changed.

Now, completely remove the contents of HomeScreen.js and replace them with the following:

import React from 'react'
import { Text, View } from 'react-native'

class HomeScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>Home Screen</Text>
      </View>
    )
  }
}

export default HomeScreen
Enter fullscreen mode Exit fullscreen mode

Now we’ll adapt the tabs according to the application we’re going to build. For our Dad Jokes app, we’re going to have 2 screens: Home and Add Joke.

We can completely delete LinksScreen.js and SettingsScreen.js from the screens/ folder. Notice our app breaks, with a red screen full of errors.

This is because we’ve linked to it in the navigation/ folder. Open MainTabNavigator.js in the navigation/ folder. It currently looks like this:

import React from 'react';
import { Platform } from 'react-native';
import { createStackNavigator, createBottomTabNavigator } from 'react-navigation';

import TabBarIcon from '../components/TabBarIcon';
import HomeScreen from '../screens/HomeScreen';
import LinksScreen from '../screens/LinksScreen';
import SettingsScreen from '../screens/SettingsScreen';

const config = Platform.select({
  web: { headerMode: 'screen' },
  default: {},
});

const HomeStack = createStackNavigator(
  {
    Home: HomeScreen,
  },
  config
);

HomeStack.navigationOptions = {
  tabBarLabel: 'Home',
  tabBarIcon: ({ focused }) => (
    <TabBarIcon
      focused={focused}
      name={
        Platform.OS === 'ios'
          ? `ios-information-circle${focused ? '' : '-outline'}`
          : 'md-information-circle'
      }
    />
  ),
};

HomeStack.path = '';

const LinksStack = createStackNavigator(
  {
    Links: LinksScreen,
  },
  config
);

LinksStack.navigationOptions = {
  tabBarLabel: 'Links',
  tabBarIcon: ({ focused }) => (
    <TabBarIcon focused={focused} name={Platform.OS === 'ios' ? 'ios-link' : 'md-link'} />
  ),
};

LinksStack.path = '';

const SettingsStack = createStackNavigator(
  {
    Settings: SettingsScreen,
  },
  config
);

SettingsStack.navigationOptions = {
  tabBarLabel: 'Settings',
  tabBarIcon: ({ focused }) => (
    <TabBarIcon focused={focused} name={Platform.OS === 'ios' ? 'ios-options' : 'md-options'} />
  ),
};

SettingsStack.path = '';

const tabNavigator = createBottomTabNavigator({
  HomeStack,
  LinksStack,
  SettingsStack,
});

tabNavigator.path = '';

export default tabNavigator;
Enter fullscreen mode Exit fullscreen mode

Remove references to LinksStack and SettingsStack completely, because we don’t need these screens in our app. It should look like this:

import React from 'react'
import { Platform } from 'react-native'
import {
  createBottomTabNavigator,
  createStackNavigator,
} from 'react-navigation'
import TabBarIcon from '../components/TabBarIcon'
import HomeScreen from '../screens/HomeScreen'

const HomeStack = createStackNavigator({
  Home: HomeScreen,
})

HomeStack.navigationOptions = {
  tabBarLabel: 'Home',
  tabBarIcon: ({ focused }) => (
    <TabBarIcon
      focused={focused}
      name={
        Platform.OS === 'ios'
          ? `ios-information-circle${focused ? '' : '-outline'}`
          : 'md-information-circle'
      }
    />
  ),
}

export default createBottomTabNavigator({
  HomeStack,
})
Enter fullscreen mode Exit fullscreen mode

Now reload the app to see the error gone.

Go ahead and create AddJokeScreen.js inside the screens/ folder.

Add the following inside AddJokeScreen.js:

import React from 'react'
import { Text, View } from 'react-native'

class AddJokeScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>Add Joke Screen</Text>
      </View>
    )
  }
}

export default AddJokeScreen
Enter fullscreen mode Exit fullscreen mode

Open up MainTabNavigator.js and import AddJokeScreen at the top:

import AddJokeScreen from '../screens/AddJokeScreen'
Enter fullscreen mode Exit fullscreen mode

Now go ahead and add the following code above our default export:

const AddJokeStack = createStackNavigator({
  AddJoke: AddJokeScreen
})

AddJokeStack.navigationOptions = {
  tabBarLabel: 'Add Joke',
  tabBarIcon: ({ focused }) => (
    <TabBarIcon
      focused={focused}
      name={
        Platform.OS === 'ios'
          ? `ios-add-circle${focused ? '' : '-outline'}`
          : 'md-add-circle'
      }
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Also, change the default export to:

export default createBottomTabNavigator({
  HomeStack,
  AddJokeStack
})
Enter fullscreen mode Exit fullscreen mode

Now you should see 2 screens: Home and AddJoke with their respective icons as follows:

Dad Jokes - Bottom Tab Navigation Icons

We now need to get rid of the header that’s showing on each screen, taking up some top space. To get rid of it, we need to add headerMode: 'none' in the createStackNavigator config.

We need to add it to HomeStack and AddJokeStack.

HomeStack should become:

const HomeStack = createStackNavigator(
  { Home: HomeScreen },
  { headerMode: 'none' }
)
Enter fullscreen mode Exit fullscreen mode

AddJokeStack should become:

const AddJokeStack = createStackNavigator(
  { AddJoke: AddJokeScreen },
  { headerMode: 'none' }
)
Enter fullscreen mode Exit fullscreen mode

Now if you check the text goes up to the top-left right above the clock.

Dad Jokes - Text Above Right Clock

There’s an easy fix for this. We need to use SafeAreaView. SafeAreaView renders content within the safe area boundaries of a device. Let’s go into the screens/ directory and change HomeScreen.js to use SafeAreaView so that it looks like this:

import React from 'react'
import { SafeAreaView, Text } from 'react-native'

class HomeScreen extends React.Component {
  render() {
    return (
      <SafeAreaView>
        <Text>Home Screen</Text>
      </SafeAreaView>
    )
  }
}

export default HomeScreen
Enter fullscreen mode Exit fullscreen mode

It now renders the content inside the boundaries of the device.

Dad Jokes - SafeAreaView

Also, do it for AddJokeScreen like so:

import React from 'react'
import { SafeAreaView, Text } from 'react-native'

class AddJokeScreen extends React.Component {
  render() {
    return (
      <SafeAreaView>
        <Text>Add Joke Screen</Text>
      </SafeAreaView>
    )
  }
}

export default AddJokeScreen
Enter fullscreen mode Exit fullscreen mode

It’s repetitive to wrap SafeAreaView inside every component instead of setting it up on a root component like App.js. But be aware that this won’t work if you try doing it on App.js.

Remember, SafeAreaView should always be set up on screen components or any content in them, and not wrap entire navigators. You can read more about it on this blog post.

GraphQL Queries and Mutations

Lets add GraphQL queries to our app which we triggered through the GraphiQL editor.

Inside components folder, create a graphql folder.

$ mkdir graphql && cd $_
Enter fullscreen mode Exit fullscreen mode

Inside graphql folder, create mutations and queries folder.

$ mkdir mutations queries
Enter fullscreen mode Exit fullscreen mode

Inside queries folder, create a file named jokes.js.

$ cd queries
$ touch jokes.js
Enter fullscreen mode Exit fullscreen mode

Inside jokes.js, paste the following:

import { gql } from 'apollo-boost'

export const LIST_ALL_JOKES_QUERY = gql`
  query jokes {
    jokes {
      id
      joke
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

Notice that the above query is similar to what we typed in the GraphiQL editor. This is how GraphQL is used. First, we type the query in the GraphiQL editor and see if it gives the data that we need and then we just copy-paste it into the application.

Inside mutations folder, create 2 files createJoke.js and deleteJoke.js.

$ cd ../mutations
$ touch createJoke.js deleteJoke.js
Enter fullscreen mode Exit fullscreen mode

Inside createJoke.js, paste the following:

import { gql } from 'apollo-boost'

export const CREATE_JOKE_MUTATION = gql`
  mutation createJoke($joke: String!) {
    createJoke(data: { joke: $joke }) {
      id
      joke
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

Again we have copied the mutation from our GraphiQL editor above. The main difference is we have replaced the hardcoded value with a variable so we can type in whatever user has specified.

Inside deleteJoke.js, paste the following:

import { gql } from 'apollo-boost'

export const DELETE_JOKE_MUTATION = gql`
  mutation deleteJoke($id: ID) {
    deleteJoke(where: { id: $id }) {
      id
      joke
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

Now create 2 files in components/ folder namely Error.js and Loading.js.

$ cd ../../
$ touch Loading.js Error.js
Enter fullscreen mode Exit fullscreen mode

In Error.js, paste the following:

import React from 'react'
import { StyleSheet, View } from 'react-native'
import { Text } from 'react-native-elements'

export const Error = () => (
  <View>
    <Text h3 h3Style={styles.error}>
      Sorry, looks like we've run into an error
    </Text>
  </View>
)

const styles = StyleSheet.create({
  error: {
    color: 'red'
  }
})
Enter fullscreen mode Exit fullscreen mode

In Loading.js, paste the following:

import React from 'react'
import { ActivityIndicator } from 'react-native'

export const Loading = () => <ActivityIndicator size='small' />
Enter fullscreen mode Exit fullscreen mode

These components will be used later in the application.

Screens

Now that our navigation is taken care of, we can start working on the layout.

We’re going to be using a UI toolkit called React Native Elements. We will also be using Apollo Client to connect to our Prisma GraphQL backend.

So go ahead and install them:

$ yarn add react-native-elements @apollo/react-hooks apollo-boost graphql
Enter fullscreen mode Exit fullscreen mode

Now open up App.js and connect our client to the backend.

First, import the following:

import { ApolloProvider } from '@apollo/react-hooks'
import ApolloClient from 'apollo-boost'
Enter fullscreen mode Exit fullscreen mode

Then right below it, create a constant:

const client = new ApolloClient({
  uri: 'http://localhost:4000/'
})
Enter fullscreen mode Exit fullscreen mode

The uri inside of ApolloClient is pointing out to Prisma GraphQL backend.

Then in the return wrap AppNavigator with ApolloProvider and pass in the client:

<ApolloProvider client={client}>
  <AppNavigator />
</ApolloProvider>
Enter fullscreen mode Exit fullscreen mode

Now anything that will be inside of AppNavigator can use Apollo Hooks.

Make sure your whole App.js file looks like:

import { ApolloProvider } from '@apollo/react-hooks'
import { Ionicons } from '@expo/vector-icons'
import ApolloClient from 'apollo-boost'
import { AppLoading } from 'expo'
import { Asset } from 'expo-asset'
import * as Font from 'expo-font'
import React, { useState } from 'react'
import { Platform, StatusBar, StyleSheet, View } from 'react-native'
import AppNavigator from './navigation/AppNavigator'

const client = new ApolloClient({
  uri: 'http://localhost:4000/'
})

export default function App(props) {
  const [isLoadingComplete, setLoadingComplete] = useState(false)

  if (!isLoadingComplete && !props.skipLoadingScreen) {
    return (
      <AppLoading
        startAsync={loadResourcesAsync}
        onError={handleLoadingError}
        onFinish={() => handleFinishLoading(setLoadingComplete)}
      />
    )
  } else {
    return (
      <View style={styles.container}>
        {Platform.OS === 'ios' && <StatusBar barStyle='default' />}
        <ApolloProvider client={client}>
          <AppNavigator />
        </ApolloProvider>
      </View>
    )
  }
}

async function loadResourcesAsync() {
  await Promise.all([
    Asset.loadAsync([
      require('./assets/images/robot-dev.png'),
      require('./assets/images/robot-prod.png')
    ]),
    Font.loadAsync({
      // This is the font that we are using for our tab bar
      ...Ionicons.font,
      // We include SpaceMono because we use it in HomeScreen.js. Feel free to
      // remove this if you are not using it in your app
      'space-mono': require('./assets/fonts/SpaceMono-Regular.ttf')
    })
  ])
}

function handleLoadingError(error) {
  // In this case, you might want to report the error to your error reporting
  // service, for example Sentry
  console.warn(error)
}

function handleFinishLoading(setLoadingComplete) {
  setLoadingComplete(true)
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  }
})
Enter fullscreen mode Exit fullscreen mode

Now we’ll start working on the Home screen.

Home Screen

Before starting to work on HomeScreen.js, let’s delete unnecessary files. Go to the components/ folder and delete StyledText.js and the __tests__ folder.

Open up HomeScreen.js and paste the following:

import React from 'react'
import { SafeAreaView, StyleSheet } from 'react-native'
import { Text } from 'react-native-elements'
import { ListJokes } from '../components/ListJokes'

class HomeScreen extends React.Component {
  render() {
    return (
      <SafeAreaView>
        <Text h1 h1Style={styles.h1}>
          Dad Jokes
        </Text>
        <ListJokes />
      </SafeAreaView>
    )
  }
}

const styles = StyleSheet.create({
  h1: {
    textAlign: 'center'
  }
})

export default HomeScreen
Enter fullscreen mode Exit fullscreen mode

Create a new file inside the components/ folder called ListJokes.js and paste the following in it:

import { useMutation, useQuery } from '@apollo/react-hooks'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { ListItem, Text } from 'react-native-elements'
import { Error } from './Error'
import { DELETE_JOKE_MUTATION } from './graphql/mutations/deleteJoke'
import { LIST_ALL_JOKES_QUERY } from './graphql/queries/jokes'
import { Loading } from './Loading'

const removeJoke = (id, deleteJoke) => {
  deleteJoke({
    variables: {
      id
    },
    update: (cache, { data }) => {
      const { jokes } = cache.readQuery({
        query: LIST_ALL_JOKES_QUERY
      })
      cache.writeQuery({
        query: LIST_ALL_JOKES_QUERY,
        data: {
          jokes: jokes.filter(joke => joke.id !== id)
        }
      })
    }
  })
}

export const ListJokes = () => {
  const { loading, error, data } = useQuery(LIST_ALL_JOKES_QUERY)
  const [deleteJoke] = useMutation(DELETE_JOKE_MUTATION)

  if (loading) return <Loading />

  if (error) return <Error />

  const jokes = data.jokes
  return (
    <View style={styles.container}>
      {!jokes.length ? (
        <Text h4 h4Style={styles.center}>
          No jokes in the database. Add one :)
        </Text>
      ) : (
        jokes.map((item, i) => (
          <ListItem
            key={i}
            title={item.joke}
            bottomDivider
            rightIcon={{
              name: 'delete',
              onPress: () => removeJoke(item.id, deleteJoke)
            }}
          />
        ))
      )}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    margin: 10
  },
  center: {
    textAlign: 'center',
    color: 'red'
  }
})
Enter fullscreen mode Exit fullscreen mode

Here, we use the useQuery API from @apollo/react-hooks. We pass in LIST_ALL_JOKES_QUERY to it. And we get back 3 parameters, loading, error and data.

We show <Loading /> component if loading is true.

We show <Error /> component if error is true.

Then if we don't have jokes we display a friendly message No jokes in the database. Add one :).

Dad Jokes - No Jokes

If we do have jokes in the database then we display the jokes.

Dad Jokes - List Jokes

We use ListItem to render the jokes.

We specify a delete icon in the rightIcon parameter of ListItem and onPress it calls removeJoke function.

We pass in deleteJoke function to removeJoke function. This deleteJoke function we get when we call useMutation with DELETE_JOKE_MUTATION. When this function is called with an appropriate joke.id, it deletes the joke from the database.

Later, we update the cache to filter it from our local cache. This optimistically updates the UI to remove deleted results from the UI without having to refresh the app.

Add Joke Screen

Open up AddJokeScreen.js and paste the following:

import React from 'react'
import { SafeAreaView, StyleSheet } from 'react-native'
import { Text } from 'react-native-elements'
import { CreateJoke } from '../components/CreateJoke'

class HomeScreen extends React.Component {
  render() {
    return (
      <SafeAreaView>
        <Text h1 h1Style={styles.h1}>
          Add Joke
        </Text>
        <CreateJoke />
      </SafeAreaView>
    )
  }
}

const styles = StyleSheet.create({
  h1: {
    textAlign: 'center'
  }
})

export default HomeScreen
Enter fullscreen mode Exit fullscreen mode

Now lets create a new file called CreateJoke.js in the components/ folder and paste the following in it:

import { useMutation } from '@apollo/react-hooks'
import React, { useState } from 'react'
import { Alert, StyleSheet, View } from 'react-native'
import { Button, Input } from 'react-native-elements'
import { Error } from './Error'
import { CREATE_JOKE_MUTATION } from './graphql/mutations/createJoke'
import { LIST_ALL_JOKES_QUERY } from './graphql/queries/jokes'

const saveJoke = (joke, changeJoke, createJoke) => {
  if (joke.trim() === '') {
    return
  }
  createJoke({
    variables: { joke },
    update: (cache, { data }) => {
      const { jokes } = cache.readQuery({
        query: LIST_ALL_JOKES_QUERY
      })

      cache.writeQuery({
        query: LIST_ALL_JOKES_QUERY,
        data: {
          jokes: jokes.concat(data.createJoke)
        }
      })
    }
  })
  Alert.alert('Joke added to the database')
  changeJoke('')
}

export const CreateJoke = () => {
  const [joke, changeJoke] = useState('')
  const [createJoke, { error, data }] = useMutation(CREATE_JOKE_MUTATION)
  if (error) {
    return <Error />
  }
  return (
    <View style={styles.wrapper}>
      <Input
        placeholder='Enter the joke'
        value={joke}
        onChangeText={changeJoke}
      />
      <Button
        type='outline'
        title='Save Joke'
        onPress={() => saveJoke(joke, changeJoke, createJoke)}
        containerStyle={styles.button}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  wrapper: {
    margin: 8
  },
  button: {
    marginTop: 16,
    padding: 4
  }
})
Enter fullscreen mode Exit fullscreen mode

It should look like:

Dad Jokes - Add Joke

Here, we simply add an Input from react-native-elements to enter the joke. Then we have Button which when submitted calls saveQuote with 3 parameters, namely, joke, changeJoke and createJoke. We get createJoke by calling in useMutation with CREATE_JOKE_MUTATION.

In saveQuote function, we call in createJoke with joke variable. This creates a joke in the database. Then we optimistically update the UI to add the new joke to the list so we don't have to refresh the app to see the results.

Later, we throw an Alert that the joke has been added and then we clear the Input by calling in changeJoke with empty string ''.

Dad Jokes - With Alert

Conclusion

In this tutorial, we built a Dad Jokes app with The Prisma Framework and React Native, totally inspired by icanhazdadjoke. You can find the complete code available here on Github.

The Prisma Framework (formerly, Prisma 2) allows us to write a query in our language of choice and then it maps everything out to a database so we don't have to worry about writing it in the database language. We can easily swap out any database by using it. Right now, it only supports SQLite, mySQL, and PostgreSQL but soon other databases will be supported when it comes out of beta.

Give it a shot and I'm sure you'll like the experience.

. . . . . . . . .