React Native Project Feature Notes
# 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
To import the component use:
import DatePicker from 'react-native-modern-datepicker';
It will use a state variable to store and manage the date value. An empty string is the default value.
const [selectedDate, setSelectedDate] = useState('');
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>
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>
);
}
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.
/>;
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:
- The Array sort method (opens new window)
- The Date Object (opens new window)
- 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]'
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]'];
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;
}
});
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();
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);
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);
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)
# Modal Dialogs for Notifications
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>
)}
)
}
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 };
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');
});
}
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>
);
}
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');
}
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>
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
}
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 });
});
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,
};
2
3
4
5
6