React Native

React Native Project Feature Notes

Module Still Under Development

# Date Picker

The Built-in date pickers for Android and iOS have been deprecated in favour of ones that have been developed by the community.

One that I have tested in react-native-modern-datepicker. You can find the notes about it here: https://hosseinshabani.github.io/react-native-modern-datepicker/ (opens new window)

To install the component:

npm install react-native-modern-datepicker
1

To import the component use:

import DatePicker from 'react-native-modern-datepicker';
1

It will use a state variable to store and manage the date value. An empty string is the default value.

const [selectedDate, setSelectedDate] = useState('');
1

In your screen, whereever you want to place the datepicker just add a component like this:

<DatePicker
  onSelectedChange={(selectedDate) => {
    //do something with the string `selectedDate`
    //that comes from the datepicker
  }}
  options={{
    backgroundColor: 'black',
    textHeaderColor: 'white',
    textDefaultColor: 'white',
    selectedTextColor: 'red',
    mainColor: 'red', //arrows
    textSecondaryColor: '#777', //dow
    borderColor: 'blue',
  }}
  style={
    {
      //additional style like padding
    }
  }
  current={'1990-01-01'}
  selected={'1990-01-15'}
  maximumDate={new Date().toDateString()}
  mode="calendar"
></DatePicker>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

The <DatePicker> can be shown all the time as the default, or you can use a state variable and a ternary operator to show a <Button> or a <TextInput>.

{
  showDatePicker ? (
    <DatePicker />
  ) : (
    <View>
      <TextInput />
      <Button
        onPress={() => {
          setShowDatePicker(true);
        }}
        title="show"
      />
    </View>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# TextInput

The TextInput component was briefly covered in module 2.1 but here are some more highlights.

const [num, setNum] = useState(0);

<TextInput
  style={styles.input}
  onChangeText={(val) => {
    //function to run when text is updated
    //val will be the current value in the input
    setNum(val);
  }}
  value={num} //state variable
  placeholder={42} //if value is empty show this placeholder
  keyboardType="numeric" // type of mobile keyboard to show
  multiline={false} //default will be a single line input
  enterKeyHint="next" // 'enter', 'done', 'next', 'previous', 'search', 'send'
  autoFocus={true} // use on one TextInput so it gets focus when the component loads.
/>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

The keyboardType property will accept one of these values 'default', 'email-address', 'numeric', 'phone-pad', 'url', 'number-pad', or 'decimal-pad'. These will help users to fill in the value in the TextInput.

Full reference for TextInput (opens new window) to see all the properties of the Input

# Dates

In the final project you will need to sort dates based on Month and Day. You will also need to format the dates as just month then day Eg: March 3.

This will require you to use:

  1. The Array sort method (opens new window)
  2. The Date Object (opens new window)
  3. The Intl DateTimeFormat Object (opens new window)

# Sorting Objects

JavaScript Arrays all have the built-in sort method. The default functionality of the sort method will sort all the values in the array as if they were Strings. That means that each item in the Array will be converted to a String before the sort happens, if you are using the default myarray.sort().

If you take any object and convert it to a String, you will always get the same value.

let obj1 = { a: 1, b: 2, c: 3 };
obj1.toString(); //'[object Object]'
1
2

So, if you have an array of objects it will look like this to the sort method.

let arr = ['[object Object]', '[object Object]', '[object Object]', '[object Object]'];
1

So, sorting the array will not make any changes to the array.

You need to use a custom method passed to the sort method.

let arr = [
  { a: 34, b: true, c: 'abc', d: DateObjA },
  { a: 22, b: false, c: 'ghi', d: DateObjB },
  { a: 11, b: false, c: 'def', d: DateObjC },
];

arr.sort((a, b) => {
  //a and b represent two objects from arr
  //the sort method picks two different objects to compare
  //the sort method returns 1 if you want a ranked higher than b
  //it returns -1 if you want b ranked higher than a
  //it returns 0 if a and b are the same ranking
  //you decide what property(s) that you want to compare inside a and b
  if (a.DateObjA.getFullYear() > b.DateObjB.getFullYear()) {
    return 1;
  } else if (a.DateObjA.getFullYear() < b.DateObjB.getFullYear()) {
    return -1;
  } else {
    return 0;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Working with Date Values

When you are working with Date values that are collected from users, you have to consider what the value is that you are getting from the input control. Is it a <TextInput> or a <DatePicker> or some other type of data entry?

What value do you want to save in your database or AsyncStorage? -> Date? DateTime? TimeStamp? String?

In most of the data storage methods, Date objects are not really an option. So, you will be saving a String or a Timestamp (which is just a number).

If you don't need the time portion saved then you can save a string like 1996-07-08. Use the international date order of yyyy-mm-dd.

If you do need the time portion then use a timestamp. To get a timestamp you will need to create a Date object by passing the values for the date and time into the Date constructor. The format of the string works best if you use the same format that you would put into an HTML <time> element. yyyy-mm-ddThh:mm:ss. The capital T is the marker for the end of the date and start of the time.

let datetimeString = `1998-10-05T14:33:00`;
let dtObj = new Date(datetimeString);
let timestamp = dtObj.valueOf(); //this creates the timestamp
let offset = dtObj.getTimezoneOffset();
1
2
3
4

When you are saving this value you need to consider the timezone too. If you don't provide a time value in the String that gets passed to the new Date() constructor then it will use 00:00:00 as the default time value and UTC as the default timezone. Being in Ottawa, means that we are 4 or 5 hours behind the UTC timezone. The time will get converted from 00:00:00 to 4 hours earlier 20:00:00.

So, if you enter 2000-01-01 as the string passed to the Date() constructor and it uses 00:00:00 as the default time, then that will be 1999-12-31T20:00:00 when you retrieve the value and display it. The lack of provided timezone makes it use local when displaying it.

So, we need to fix the time string before passing it to the Date() constructor.

Date objects have a getTimezoneOffset() method that you can use when your component loads to get the number of minutes offset between UTC and the user's timezone. So, you need to increase or reduce the time by the proper amount before saving it or creating your timestamp.

If you save your time as just a date String with no time then you can do the conversion when the time is being loaded into the interface. If you save your time as a timestamp then you need to do the conversion before creating the timestamp.

let hourOffset = new Date().getTimezoneOffset() / 60;
//divide offset by 60 to get the number of hours. rounded up to handle 30 minute increments
let hour = hourOffset >= 0 ? Math.ceil(hourOffset) : 24 - Math.ceil(hourOffset);
//create a value for the hours that will keep the date what you entered originally
let dateTimeString = `${dateString}T${hour}:00:00`;
let dateObj = new Date(dateTimeString);
1
2
3
4
5
6

Date object reference (opens new window)

# Formating Date Strings

Often you will want to display a day of the week or name of a month as part of your date. Doing this properly means being able to handle different languages too. This can be A LOT of work to do your self.

Thankfully, browsers added an Intl object a few years ago that can do formatting for currencies and dates.

let options = {
  month: 'long',
  day: 'numeric',
};
let dateString = new Intl.DateTimeFormat('en-CA', options).format(dateObj);
1
2
3
4
5

Create an options object that defines the parts of the date that you want to display along with the format for each part.

Then pass that options object as the second parameter to the Intl.DateTimeFormat() method. The first parameter will be the locale. The locale gets used in conjunction with the options to prepare the formatting information.

Then chain the format() method which accepts a date object that you want to format.

Reference for Intl.DateTimeFormat (opens new window)

When you want to display a message to a user within your App, React Native has a built-in <Modal> component that you can use to display the message and force the user to acknowledge the message by interacting with it.

Nothing to install, you just need to import the {Modal} component from react-native.

function App(){
  const [isVisible, setIsVisible] = useState(false)
  //animationType 'slide' | 'fade' | 'none'

  return (
    {isVisible ? (
      <Modal
        transparent={true}
        visible={isVisible}
        animationType="slide"
        onShow={()=>{
          //called when the modal is shown
        }}
        onRequestClose={()=>{
          //triggered by Android Back button, drag dismiss on iOS, menu button on TVOS
          setModalVisible(!isVisible);
        }}
      >
      <View>
        <Text>This message is inside the modal</Text>
        <Button onPress={()=>{setIsVisible(!isVisible)}}
      </View>
        </Modal>
    ) : (
      <View>
      <Text>Click the button to show the modal.</Text>
      <Button onPress={()=>{setIsVisible(!isVisible)}} title="Click Me" />
    </View>
    )}
  )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# Returning Promises from Functions

When building applications that have multiple Screens in a Stack, you will often be saving some data before navigating from one screen to the next.

To properly do this you need to FINISH saving the data BEFORE you navigate back or navigate to the next screen.

This can be achieved by having the functions that save the date (often inside your Context object) return a Promise. Then inside the function that called the context function can chain a then() on to that function.

Here is an example.

Starting with a pretend context object that we will call our ColorContext.

const ColorContext = createContext();

function ColorProvider() {
  const [color, setColor] = useState('red');

  function red(id) {
    //pretend we need an id to do this
    setColor('red');
  }
  function green(id) {
    //pretend we need an id to do this
    setColor('green');
  }
  function blue(id) {
    //pretend we need an id to do this
    setColor('blue');
  }
  return <ColorContext.Provider value={[color, red, green, blue]} {...props} />;
}

function useColor() {
  const context = useContext(ColorContext);
  return context;
}

export { ColorProvider, useColor };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

So, the first version above is not doing anything that takes a lot of time. Changing a state value is asynchronous but it will happen before navigation. So, to make this more realistic, let's update the code so the functions red, green, and blue are actually asynchronous and will take longer than the navigation potentially.

function red(id) {
  let p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 2000);
  });
  return p.then(() => {
    //this function containing the setColor()
    //will be returned wrapped in a Promise
    setColor('red');
  });
}
function green(id) {
  let p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 2000);
  });
  return p.then(() => {
    //this function containing the setColor()
    //will be returned wrapped in a Promise
    setColor('green');
  });
}
function blue(id) {
  let p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 2000);
  });
  return p.then(() => {
    //this function containing the setColor()
    //will be returned wrapped in a Promise
    setColor('blue');
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

In this second version, we are really just wrapping the call to setColor inside a Promise which will only resolve after a 2 second delay. Our delay is hard coded at 2 seconds. However, talking to an API or AsyncStorage or some other asynchronous task takes an unknown amount of time.

Now, switching over to the screen code, we need to call one of these functions.

// /screens/MyScreen.js
import { useColor } from '../context/ColorContext';
import { useNavigation, useRoute } from '@react-navigation/native';

function MyScreen(props) {
  const [color, red, green, blue] = useColor();
  const route = useRoute(); //get an id from the route params
  const thing_id = route.params.thingid; //to be used in saving data
  const navigation = useNavigation();

  function saveData() {
    //we need to save the data BEFORE navigating back
    blue(thing_id)
      .then(() => {
        //this part of the code runs AFTER updating the color state variable
        //inside the context object
        navigation.navigate('OtherScreen', { thingid: thing_id });
      })
      .catch((err) => {
        //display an error message to the user
      });
  }

  return (
    <View>
      <Text>
        Form for {thing_id} and {color}
      </Text>
      <TextInput />
      <TextInput />
      <TextInput />
      <Button title="Save" onPress={() => saveData()} />
    </View>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

Now we have a screen with a title made with an id that comes out of Route params and a color value from the ColorContext object. There are several TextInputs just to appear like a form, plus a button to press to save some data.

When the button is pressed, the saveData function gets called and we call one of the functions from the context object. The function in the context object returns a Promise, so we need to chain a then() on to it. Inside the then method is where we will call the navigation.navigate() method and pass whatever params we need.

# Detecting if a Function is Async

On a side note, if you want to know whether a function you are calling is asynchronous or not, you can check the name of the constructor for the function. If it is asynchronous then the constructor name will be 'AsyncFunction'.

Let's say that these are the functions that you passing from one component to another.

async function someFunc() {
  let resp = await fetch(url);
  let data = await resp.json();
  return data;
}
function otherFunc() {
  //not async
  console.log('not async');
}
1
2
3
4
5
6
7
8
9

Then we pass those functions to another component via props. Here, we pass a reference to someFunc to all the <Item> components inside the <ScrollView>.

<ScrollView>
  {dataArr.map((item) => (
    <Item func={someFunc} other={otherFunc} />
  ))}
</ScrollView>
1
2
3
4
5

Inside of the Item component we destructure the functions from props. If the name of the constructor for a function is AsyncFunction then we know that it came from a declaration like this: async function x(){}

function Item({ func, other }) {
  const isAsyncA = func.constructor.name === 'AsyncFunction';
  const isAsyncB = other.constructor.name === 'AsyncFunction';
  //isAsyncA will be true
  //isAsyncB will be false
}
1
2
3
4
5
6

# Camera Picture Sizes

The Camera component has an asynchronous method that will return an array of possible sizes that you can use when taking a photo. It will be something like this: ["1088x1088", "2992x2992"].

camera.getAvailablePictureSizesAsync().then((sizes) => {
  console.log({ sizes });
});
1
2
3

If the sizes array returns nothing then you can enter the size that you want based on the aspect ratio that you are using, as a value in the options object that gets passed to the camera.takePictureAsync(options) method.

const options = {
  quality: 0.8,
  pictureSize: sizes ? sizes[1] : '1200x1800',
  imageType: 'jpg',
  skipProcessing: false,
};
1
2
3
4
5
6
Last Updated: 10/19/2023, 9:17:57 PM