CS 466 - React Native Data

Due: 2019-11-12 5:00pm

Goals

  • Learn how data flows between pages in React Native
  • Learn how to create a list view (specifically a FlatList)
  • Get more practice working with styles

Prerequisites

  1. Accept the assignment on our Github Classroom.
  2. Download the git repository to your local computer. This code is the solution to the last practical plus some additional data files.
  3. Update the package.json file with your name and email address.
  4. Install the package dependencies with npm install

Assignment

In the last practical, we laid in the basic navigation for the Ghost Hunter app. For this practical we are going to add some data so we don't just have blank screens everywhere. Specifically, we will add a button to the radar screen that will randomly "capture" a ghost. We will then populate the GhostsScreen with a listing of our captured ghosts, and show details on the GhostDetailScreen.

Part 1: Add state to the app

We are going to store our state as an Array of objects. So the first thing you are going to do is create a state hook to store the Array in the root component (in App.js):

const [ghosts, setGhosts] = useState([]);

This creates a new state variable, initializes the state to the empty Array, and provides a "setter" function to allow us to update the state value. Note: don't forget to import useState to use this.

Now, as you hopefully recall, we don't want to mutate state variables directly (say, for example, by pushing a new item into an Array). If we make changes to the internal values of our state variable, React won't be aware of them and won't re-render the components. Even if we pass this modified version of our state object to the setter function, React will still decide that the state hasn't changed because the object itself is the same (even if the internal values have changed).

So, if we aren't going to make use of a library like immutable, we need to make a fresh copy of our Array and pass that to our state setter. One quick and dirty way to copy Arrays is to call .slice() on the Array with no arguments. This performs a shallow copy of the array (i.e., it copies the values out of the original Array, but if those values are objects or Arrays themselves, their contents are not copied), but frequently that is all we need.

We don't want other components to have to worry about this detail, so we are going to write a convenience function called addGhost. Write this function inside of App.js. The function should take one argument: a new ghost object to add to the state. It should copy the Array, add the new ghost to the Array, and then use setGhosts to save the new Array.

Part 2: Moving data around the app

When we were using "plain" React, all of our data movement was done through props. As you hopefully recall, a web application has a DOM in the form of a tree, and our goal was to find a place in the tree that was just high enough so that we could pass the state value as props to all components that relied on it.

In React Native, we no longer have a single page consisting of a collection of components, the way that we used to. Instead, we have a collection of pages that are bound together by navigators.

To solve this problem, react-navigation provides screenProps. We can set these on the main application container, and they will show up as a prop for other screens.

Add a screenProps prop to the AppNavigator in App.js. We want to pass both the ghosts and our new function, so it should look like this:

<AppNavigator screenProps={{
        ghosts:ghosts,
        addGhost:addGhost
  }}/>

Part 3: Adding data

The user will be able to capture ghosts from the radar screen, so this is where we will produce data. While we will eventually want some sort of visually interesting screen, for now we will just add a Button to RadarScreen with the title "Catch Ghost".

We will need access to the addGhost function. The addGhost function should be available as an attribute of the screenProps prop of the component (i.e., it is in props.screenProps.addGhost).

To make our ghost hunting at least a little bit interesting, we are going to randomly generate ghosts. Write a new function called createNewGhost. The function should take no arguments and return an object containing values for name, age, type, and captured. For the age, just pick a random number between 1 and 200, and for captured, set it to Date.now(). For the name and type, I have provided some data files for you. You can import them like this:

import names from '../data/names.json';
import ghostTypes from '../data/ghost-types.json';

These two JSON files contain lists of values, so the imported values for names and ghostTypes are Arrays of strings. Pick one at random from each list for the ghost.

In JavaScript, our main tool for generating random numbers of Math.random(). This generates a number between 0 and 1 (including 0, excluding 1). Judicious use of multiplication and addition can give you any range that you like, and Math.floor() can help if you need integers

For the onPress property of the Button, call your ghost creation function and pass the result to addGhost.

You can check that this is working by adding a console.log(ghosts) in App. Every time you press the button, it should add a new ghost, which should cause a state update and thus a re-render of App.

Part 4: Listing the data

Of course, the user doesn't want to go looking for log output -- we need to put these values in the app.

We are going to display the list of captured ghosts on the GhostsScreen. Start by extracting the list of ghosts from the props of the component (remember they are hiding in screenProps).

As a refresher, when we made lists in normal React, we were creating ordered (<ol>) or unordered (<ul>) lists. Our usual technique was to take an Array of data, and call map() on it, mapping each data item to a corresponding list item (<li>). We then just dropped this Array of list items between the appropriate list tags in the output.

We are going to display our ghosts with a FlatList. The principle is somewhat the same to what we have done before, but with some significant differences. The primary one is that instead of directly creating the collection of sub-views from the data, we are going to tell FlatList how to display a data item and then separately what data we would like displayed. This is done for efficiency. On small screens, we don't necessarily want to create components for every element in a long list if we can only see a few of them. The FlatList will handle rendering list items on demand.

Start by replacing the Text and View components in GhostsScreen with a new FlatList. Give it a prop called data and pass it the list of ghosts.

Now, we need to tell the list how to make a list item. Write a new function called makeListItem. This is a function that takes in a single data item and returns a React component for displaying it. This could be a simple Text component, but more typically it will be a View with some other components inside. As a warning, the object passed to this function is not just one element out of the Array. It is an object with three attributes: index, item, and separators. The index is obviously the location in the list. The item is the original data object from the data source. The separators has information about what happens between the items, but we won't bother with them today.

For your list items, please return a View with two Text components inside. One should list the ghost type, and the other should say when the ghost was captured (if you had a ghost stored in a variable ghost, you can get a reasonable date string with new Date(ghost.item.captured)).toLocaleDateString()).

Pass the name of your function to FlatList as the renderItem prop.

If you try this now, you will get a warning you may recall from working with React before about missing "key" values. We are going to generate keys the same way we generated the list items. Add this function to your code:

const generateKey = (ghost) => {
    return JSON.stringify(ghost.captured);
  }

You can then add the name of this function as the keyExtractor prop on FlatList. Somewhat perversely, this function actually receives the original data object without a wrapper, so we don't have access to the index.

You should now have a list of ghosts that updates when you capture ghosts on the radar screen.

Part 5: Connecting to the details screen

The next step is to hook up the navigation to the list items so we can view the ghost details.

To handle touches we use the TouchableOpacity component. This provides something that can detect touch interaction, as well as providing feedback to the user (in the form of dimming of the component) that the touch happened.

Surround the View in makeListItem in a TouchableOpacity component. Give it the same onPress prop that you previously had on the Button (onPress={()=>props.navigation.navigate('GhostDetails')}). Now tapping on one of the list items will take you to the details page.

Of course, we don't just want to visit this page, we want it to be populated with details about the ghost we clicked. As it turns out, the navigate function takes an optional second argument. You can pass in data, that will be available in the props of the component you are navigating too.

Unfortunately, this data is not sent as a normal prop, nor is it in screenProps. Instead, it is available from within the navigation prop. Curiously, it isn't just an object we can read out, instead, the navigation prop has a function called getParam, which allows you to access the individual attributes of the object you passed in. So, for example, if you passed in the original ghost object to navigate (which you should), you would then get the name of the ghost back with props.navigation.getParam('name', undefined). The undefined there is the value to use if the given key doesn't have a value.

Go ahead and pass the ghost through to the details page through the navigate function. Fix the details screen so that it displays all of the attributes of a ghost (name, type, age, and captured).

Part 6: Styling cleanup

If you have been just pushing through, following the directions and nothing else, your list view and detail page will not look great.

Take a moment to do some styling so they look better. I will mostly leave it to you to determine what that means. However, in the list view, I would like the ghost type to be emphasized, with the date smaller and less prominent. On the details page, I would the name of the ghost to be the first and most dominant element. Beyond that, show all the data and make it look good.

Finishing up

Commit your changes to git and push them back up to GitHub. I will find them there.

Grading

Points Requirement
✓/✗ Ghost state added to App
✓/✗ Ghosts can be "captured" on the radar screen
✓/✗ Ghost values are randomized
✓/✗ The GhostsScreen displays a list of ghosts
✓/✗ The details are accessible through a tap and shows all of the details
✓/✗ The styling has been cleaned up