DEV Community

Cover image for Building Dad Jokes using The Prisma Framework (formerly Prisma 2) and React Native
Akshay Kadam (A2K) for Open GraphQL

Posted on

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

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

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
}

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()
  })

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')

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()
  },
})

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()
  },
})

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()
  },
})

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' })
  },
})

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 }

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

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

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

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
  }
}

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
  }
}

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
  }
}

Dad Jokes - Create A Joke GraphiQL

Delete a joke

mutation deleteJoke {
  deleteJoke(where: { id: "ck2zqhwvo0001cav551f1me34" }) {
    id
    joke
  }
}

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

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

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

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;

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,
})

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

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

import AddJokeScreen from '../screens/AddJokeScreen'

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'
      }
    />
  )
}

Also, change the default export to:

export default createBottomTabNavigator({
  HomeStack,
  AddJokeStack
})

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' }
)

AddJokeStack should become:

const AddJokeStack = createStackNavigator(
  { AddJoke: AddJokeScreen },
  { headerMode: 'none' }
)

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

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

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 $_

Inside graphql folder, create mutations and queries folder.

$ mkdir mutations queries

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

$ cd queries
$ touch jokes.js

Inside jokes.js, paste the following:

import { gql } from 'apollo-boost'

export const LIST_ALL_JOKES_QUERY = gql`
  query jokes {
    jokes {
      id
      joke
    }
  }
`

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

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
    }
  }
`

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
    }
  }
`

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

$ cd ../../
$ touch Loading.js Error.js

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'
  }
})

In Loading.js, paste the following:

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

export const Loading = () => <ActivityIndicator size='small' />

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

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'

Then right below it, create a constant:

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

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>

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'
  }
})

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

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'
  }
})

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

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
  }
})

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.

Top comments (0)