React native - The Practical Guide 2020
Day 1
I started my learning by quickly going through https://www.udemy.com/course/react-native-the-practical-guide just to understand the basics.
Installed expo.io
Installed Android Studio developer.android.com/studio
Need iOS phone or Mac computer (for iOS simulator) to run iOS apps
Day 2
Ctrl + M reloads App in the Android simulator
Communication Parent to Child and Child to Parent via Props
Day 3
Finished first ToDo application.
Debugging:
1. console.log()
2. Chrome Debugger (+ Breakpoints)
Android Emulator Ctrl + M and then Debugger opens a new tab in the browser
Sources -> debuggerWorker -> break point
3. React-native-debugger LINK
Ctrl + T to open Debugger port
in Android Emulator Ctrl + M and choose Debug JS remotely
IMPORTANT TIP: I had to install earlier version of React Native Debugger that would work with the current version of React installed by Expo
Day 4
View style shadow... (shadowColor) works on iOS, and elevate is used for Android
<View style={{...styles.card, ...props.style}}>{props.children}</View>
Component can be used to wrap around other components
Adds additional styles through props, to those defined in component
Hiding keyboard in iOS by clicking somewhere on the screen
<TouchableWithoutFeedback onPress={() => {
Keyboard.dismiss();
}}>
Day 5
useRef() values that do not rerender the component
useEffect() runs logic after render cycle
TODO: course on Hooks
?!?useCallback() determines when the application will be rerendered.?!?
LOCAL <Image source={require('../assets/name.png')} resizeMode="cover" style=...}
ONLINE <Image source={{uri:'http://www.image.com/i.png'}} resizeMode="cover" style=...}
Icons: LINK
ScrollView and FlatList use contentContainerStyle to control the content inside of that list
Shared styling is achieved by making a new Component which takes existing component with specific style added
const BodyText = props => (
<Text style={{ ...styles.body, ...props.style }}>{props.children}</Text}
);
add some style to BodyText and then import that component and use it same like you would use Text
<BodyText>Text with added style</BodyText>
Shared constants, like color, added to a file and imported all over as an Object
constants/colors.js
export default {
primary: 'pink',
secondary: 'green'
};
App.js
import Colors from './constants/colors.js';
Colors.primary is how it is called then
Not able to be productive for more than 5 hours a day during the entire day. Installed Pomodoro App and started making working plan from next week.
Day 6
Responsive&Adaptive UI
Dimensions - Object that tells us how much width and height we have available (among other things)
in CSS margin: Dimensions.get('window').height > 600 ? 20 : 10; will give us device width
This Dimension is only calculated once at beginning of the App, so when we change the orientation it does not look good.
To fix styling when orientation changes we put it in state and change on orientation change
const [ availableDeviceWidth, setAvailableDeviceWidth ] = useState(Dimensions.get('window').width;
const [ availableDeviceHeight, setAvailableDeviceHeight ] = useState(Dimensions.get('window').height;
useEffect(() => {
const updateLayout = () => {
setAvailableDeviceWidth (Dimensions.get('window').width);
setAvailableDeviceHeight (Dimensions.get('window').height);
};
Dimensions.addEventListener('change', updateLayout);
// this function runs before useEffect and cleans Listener, so we always have only one
return () => {
Dimensions.removeEventListener('change', updateLayout);
};
});
Then use it to set different style based on the width:
let listContainerStyle;
if (availableDeviceWidth < 350) {
listContainerStyle = styles.listContainerBig;
else {
listContainerStyle = styles.listContainerSmall;
}
Expo ScreenOrientation allows you to lock orientation during the App life or get the current orientation...
Platform from react-native tells us on which platform (and version) are we running
Platform.OS === 'android"'
Platform.Version >= 21
Setting base css styles and then additional based on the OS
<View
style={{
...styles.headerBase,
...Platform.select({
ios: styles.headerIOS,
android: styles.headerAndroid
})
}}
>
We can choose to have whole different files for different platforms
/components/MainButton.android.js
/components/MainButton.ios.js
and the it will automatically import the proper one in the file where we use the component
import MainButton from "../components/MainButton";
DO NOT IMPORT MainButton.android or MainButton.ios
SafeAreaView component should be used as upmost component
KeyboardAvoidingView - stop keyboard from overlapping some Input field
Day 7
Using Fonts in project
Copy font files to assets/fonts folder
Command line:
expo install expo-font
App.js
import * as Font from 'expo-font';
import { AppLoading } from 'expo';
const fetchFonts = () => {
return Font.loadAsync({
'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'),
'open-sans-bold': require('./assets/fonts'/OpenSans-Bold.ttf)
});
};
export default function App() {
const [fontLoaded, setFontLoaded] = useState(false);
//making sure that fonts are loaded before App is shown with AppLoading
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
Day 7
Mobx
LINK
LINK
LINK
@observable we put on a variable,arrays,objects,class instances that will be changed
@action is for the function that is changing that variable
@computed returns changed (computated) values when observable value is changed
Day 8
REACT NAVIGATION DOCS
npm install @react-navigation/native
expo install react-native-gesture-handler react-native-reanimated react-native-screens
react-native-safe-area-context @react-native-community/masked-view
Stack Navigator DOCS
npm install @react-navigation/stack
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
Tab Navigator DOCS
npm install @react-navigation/bottom-tabs
const Tab = createBottomTabNavigator();
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeDrawerNav} />
<Tab.Screen name="Settings" component={SettingsDrawerNav} />
</Tab.Navigator>
Drawer Navigator DOCS
npm install @react-navigation/drawer
const Drawer = createDrawerNavigator();
<Drawer.Navigator>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Drawer1" component={Drawer1Screen} />
<Drawer.Screen name="Drawer2" component={Drawer2Screen} />
</Drawer.Navigator>
It is hard to combine all three at once (two are easily possible).
Here is a LINK of what I was able to accomplish.
TODO: I DID NOT COVER PARAMS PART
Day 9 & 10
Redux - great and simple tutorial LINK
yarn add redux react-redux
Actions - functions that change state
/store/actions/information.js
export const TOGGLE_FAVORITE = 'TOGGLE_FAVORITE';
// function being called
export const toggleFavorite = (id) => {
return { type: TOGGLE_FAVORITE, informationId: id };
}
Reducers - functions receiving parameters state and action
/store/reducers/someReducer.js
import { TOGGLE_FAVORITE } from '../actions/information';
const initial State = {
someInformation: ['apple', 'orange', 'five', 'airplane'],
favoriteInformation: []
};
const someReducer = (state = initialState, action) => {
switch (action.type) {
case TOGGLE_FAVORITE:
const existingIndex = state.favoriteInformation.findIndex(
information => information.id === action.informationId
);
// informationId found so will be removed
if (existingIndex >= 0) {
const updatedInfo = [...state.favoriteInformation];
//remove that item
updatedInfo.splice(existingIndex, 1);
return { ...state, favoriteInformation: updatedInfo};
} else {
const info = state.someInformation.find(info => info.id === action.informationId);
return { ...state, favoriteInformation: state.favoriteInformation.concat(info) };
}
default:
return state;
}
return state;
}
/App.js
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import someReducer from './store/reducers/someReducer';
const rootReducer = combineReducers({
someData: someReducer
});
const store = createStore(rootReducer);
export default function App() {
return <Provider store={store}><MainScreen /></Provider>;
}
/screens/ConsumerScreen.js
import React, {useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleFavorite } from '../store/actions/information';
const dataFromStore = useSelector(state => state.someData.someInformation);
const dispatch = useDispatch();
// not sure if useEffect is really needed
// someInformationId is the id of some element we want to toggle
useEffect(() => {
dispatch(toggleFavorite(someInformationId))
}, [])
Day 11
Step 1) Created folder structure
1. Added all the Screens
2. Added model of data
/models/Product.js
3. Added dummy data
/data/dummy-data.js
4. Added action and reducer
/store/reducers/products.js
5. Added redux to the App.js
yarn add redux react-redux
import React from "react";
6. Populated initial Screen with dummy data
/screens/ProductsOverviewScreen.js
Day 12
Added initial navigation
/navigation/ShopNavigator.js
In App.js changed this part
with this
Link to this initial commit:
https://github.com/VladimirRodic/rn-shop-app/commit/eedcc3885651e79a0cafab84e31cab06f95070e2
To show remote image you must set it's width and height
1. Add Detailed Product page to the Stack Navigator
2. Pass data from Overview to the ProductItem component (we had to pass navigation)
3. On ProductItem click should open Detailed Product page and send product as a param
4. Change the Navigation name dynamically (title, see step 1)
Day 13
There I have an issue that I can not take the navigation logic out of the component ProductItem up to the Screen ProductOverview and pass it as a function. Here I have it as a comment what I have tried to do. I have commented it out in the code.
Day 14
Solved the issue using the debugger. I was calling wrong variable name.
Adding items to Cart (full Redux cycle example)
1. create model for Cart
/models/cart-item.js
class CartItem {
constructor(quantity, productPrice, productTitle, sum) {
this.quantity = quantity;
this.productPrice = productPrice;
this.productTitle = productTitle;
this.sum = sum;
}
}
export default CartItem;
2. create action(s)
/store/actions/cart.js
export const ADD_TO_CART = "ADD_TO_CART";
export const addToCart = (product) => {
return { type: ADD_TO_CART, product: product };
};
3. create reducer
/store/reducers/cart.js
import { ADD_TO_CART } from "../actions/cart";
import CartItem from "../../models/CartItem";
const initialState = {
items: {},
totalAmount: 0,
};
export default (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
const addedProduct = action.product;
let updatedOrNewCartItem;
// if product was previously added
if (state.items[addedProduct.id]) {
updatedOrNewCartItem = new CartItem(
state.items[addedProduct.id].quantity + 1,
addedProduct.price,
addedProduct.title,
(state.items[addedProduct.id].sum += addedProduct.price)
);
} else {
// if product is added for the first time
updatedOrNewCartItem = new CartItem(
1,
addedProduct.price,
addedProduct.title,
addedProduct.price
);
}
return {
...state,
items: { ...state.items, [addedProduct.id]: updatedOrNewCartItem },
totalAmount: state.totalAmount + action.price,
};
}
return state;
};
4. combine new reducer in App.js
App.js
import CartReducer from "./store/reducers/cart";
const rootReducer = combineReducers({
products: ProductsReducer,
cart: CartReducer,
});
5. call action from inside of the App
/components/shop/ProductItem.js
import { useDispatch } from "react-redux";
import * as cartActions from "../../store/actions/cart";
const ProductItem = (props) => {
const dispatch = useDispatch();
...
<Button
title="To Cart"
onPress={() => {dispatch(cartActions.addToCart(props.product));}}
/>
Day 15 & 16
Building Header Button for Shopping Cart icon
1. Button as a new component
/components/UI/HeaderButton.js
import { HeaderButton } from 'react-navigation-header-buttons'; LINK to docs
import { Ionicons } from '@expo/vector-icons';
const CustomHeaderButton = props => {
return (
<HeaderButton {...props} IconComponent={Ionicons} iconSize{23} color='white' />
);
}
export default CustomHeaderButton;
2. Show cart icon
// not sure which screen yet, probably Overview and Detail
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
import HeaderButton from '../../components/UI/HeaderButton';
navigationOptions CHECK V5
headerRight: <HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item title='Cart' iconName={md-cart} onPress={() => {navigation.navigate('Cart Screen')}}/>
<HeaderButtons/>
3. create Cart screen
/screens/shop/CartScreen.js
4. add Cart screen to the navigator
5. Add navigation to the Cart icon
// not sure which screen yet, probably Overview and Detail
6. fill Cart screen
total amount
order now JS function .toFixed(2)
list of items
we have them stored in list of Objects and want it as an array for the FlatList
HERE we can see how list of Objects is used instead of an array. I guess Maximilian switched it to Array to be able to check with array's length function whether there are objects i nthe Dictionary, but he could have achieved it like HERE Lodash _.isEmpty({}); DOCS
state.cart.items is an object
//defined in model
class CartItem {
constructor(quantity, productPrice, productTitle, sum) {
this.quantity = quantity;
this.productPrice = productPrice;
this.productTitle = productTitle;
this.sum = sum;
}
}
// define it initially
const initialState = {
items: {},
totalAmount: 0,
};
// add new cart item as an object
updatedOrNewCartItem = new CartItem(
1,
addedProduct.price,
addedProduct.title,
addedProduct.price
);
// adding it to state
return {
...state,
items: { ...state.items, [addedProduct.id]: updatedOrNewCartItem },
totalAmount: state.totalAmount + action.price,
};
WHEN ACCESSING IT I NEED TO TURN LIST OF OBJECTS TO ARRAY
TODO: check Lodash for this
// turning list of objects into array
const cartItems = useSelector(state => {
const transformedCartItems = [];
for (const key in state.cart.items) {
transformedCartItems.push({
productId: key,
productTitle: state.cart.items[key].productTitle,
productPrice: state.cart.items[key].productPrice,
quantity: state.cart.items[key].quantity,
sum: state.cart.items[key].sum
});
}
return transformedCartItems;
})
flatList will need to use KeyExtractor productId
CREATE CartItem.js COMPONENT
quantity, title, total amount for the item, delete from cart icon (ionicons and touchableOpacity)
DELETING ITEMS FROM THE CART
1. create action for removal
/store/actions/cart.js
export const REMOVE_FROM_CART = 'REMOVE_FROM_CART';
export const removeFromCart = productId => {
return { type: REMOVE_FROM_CART, pid: ProductId };
}
2. create reducer case for removal
/store/reducers/cart.js
import { REMOVE_FROM_CART } from '../actions/cart';
case REMOVE_FROM_CART:
getQuantity 2:40
// if quantity 1 remove whole object
//if quantity bigger than one, decrease quantity by 1
3. dispatch removal action
/screens/CartScreen.js
import { TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import * as cartActions from '../../store/actions/cart';
<TouchableOpacity
onPress={() => {
dispatch(cartActions.removeFromCart(item.id));
}}
>
<Ionicons name="md-trash" size={23} color="red" />
</TouchableOpacity>
Day 17
Orders
1. add action and reducer ADD_ORDER (orderData: cartItems, totalAmount)
2. add model order.js
Reducer
class Order {id, items, totalAmount, date} new Date()
// concat creates a new array and adds a new element
orders: state.orders.concat(newOrder)
get readableDate(){
return this.date.toLocaleDateString('en-EN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
3. add reducer to App.js
4. dispatch action from Cart and clear the Cart
5. create Orders screen
get orders with useSelector
6. create Drawer Navigation (ShopNavigator) to add Stack Navigators to it. Orders screen
7. ProductOverviewScreen and OrdersScreen create a hamburger menu that opens Drawer
import { HeaderButton, Item } from 'react-navigation-header-buttons';
import HeaderButton from '../../components/UI/HeaderButton';
headerLeft: (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title="Menu"
iconName='md-menu'
onPress={() => {navData.navigation.toggleDrawer()}}
/>
</HeaderButtons>
),
8. In Drawer navigator add icons next to the items
navigationOptions: {
drawerIcon: drawerConfig => <Ionicons />
}
9. empty cart after order
in reducer for cart.js on ADD_ORDER return initialState at the end
10. styling OrdersScreen
separate OrderItem component
TOTAL
DATE
button SHOW DETAILS (showing items)
Shadow radius styling
shadowColor: 'black',
shadowOpacity: 0.26,
shadowOffset: {width:0, height: 2},
shadowRadius: 8,
elevation: 5,
borderRadius: 10,
backgroundColor: 'white'
IMPORTANT TIP: I had to install earlier version of React Native Debugger that would work with the current version of React installed by Expo
Day 4
View style shadow... (shadowColor) works on iOS, and elevate is used for Android
<View style={{...styles.card, ...props.style}}>{props.children}</View>
Component can be used to wrap around other components
Adds additional styles through props, to those defined in component
Hiding keyboard in iOS by clicking somewhere on the screen
<TouchableWithoutFeedback onPress={() => {
Keyboard.dismiss();
}}>
Day 5
useRef() values that do not rerender the component
useEffect() runs logic after render cycle
TODO: course on Hooks
?!?useCallback() determines when the application will be rerendered.?!?
LOCAL <Image source={require('../assets/name.png')} resizeMode="cover" style=...}
ONLINE <Image source={{uri:'http://www.image.com/i.png'}} resizeMode="cover" style=...}
Icons: LINK
ScrollView and FlatList use contentContainerStyle to control the content inside of that list
Shared styling is achieved by making a new Component which takes existing component with specific style added
const BodyText = props => (
<Text style={{ ...styles.body, ...props.style }}>{props.children}</Text}
);
add some style to BodyText and then import that component and use it same like you would use Text
<BodyText>Text with added style</BodyText>
Shared constants, like color, added to a file and imported all over as an Object
constants/colors.js
export default {
primary: 'pink',
secondary: 'green'
};
App.js
import Colors from './constants/colors.js';
Colors.primary is how it is called then
Not able to be productive for more than 5 hours a day during the entire day. Installed Pomodoro App and started making working plan from next week.
Day 6
Responsive&Adaptive UI
Dimensions - Object that tells us how much width and height we have available (among other things)
in CSS margin: Dimensions.get('window').height > 600 ? 20 : 10; will give us device width
This Dimension is only calculated once at beginning of the App, so when we change the orientation it does not look good.
To fix styling when orientation changes we put it in state and change on orientation change
const [ availableDeviceWidth, setAvailableDeviceWidth ] = useState(Dimensions.get('window').width;
const [ availableDeviceHeight, setAvailableDeviceHeight ] = useState(Dimensions.get('window').height;
useEffect(() => {
const updateLayout = () => {
setAvailableDeviceWidth (Dimensions.get('window').width);
setAvailableDeviceHeight (Dimensions.get('window').height);
};
Dimensions.addEventListener('change', updateLayout);
// this function runs before useEffect and cleans Listener, so we always have only one
return () => {
Dimensions.removeEventListener('change', updateLayout);
};
});
Then use it to set different style based on the width:
let listContainerStyle;
if (availableDeviceWidth < 350) {
listContainerStyle = styles.listContainerBig;
else {
listContainerStyle = styles.listContainerSmall;
}
Expo ScreenOrientation allows you to lock orientation during the App life or get the current orientation...
Platform from react-native tells us on which platform (and version) are we running
Platform.OS === 'android"'
Platform.Version >= 21
Setting base css styles and then additional based on the OS
<View
style={{
...styles.headerBase,
...Platform.select({
ios: styles.headerIOS,
android: styles.headerAndroid
})
}}
>
We can choose to have whole different files for different platforms
/components/MainButton.android.js
/components/MainButton.ios.js
and the it will automatically import the proper one in the file where we use the component
import MainButton from "../components/MainButton";
DO NOT IMPORT MainButton.android or MainButton.ios
SafeAreaView component should be used as upmost component
KeyboardAvoidingView - stop keyboard from overlapping some Input field
Day 7
Using Fonts in project
Copy font files to assets/fonts folder
Command line:
expo install expo-font
App.js
import * as Font from 'expo-font';
import { AppLoading } from 'expo';
const fetchFonts = () => {
return Font.loadAsync({
'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'),
'open-sans-bold': require('./assets/fonts'/OpenSans-Bold.ttf)
});
};
export default function App() {
const [fontLoaded, setFontLoaded] = useState(false);
//making sure that fonts are loaded before App is shown with AppLoading
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
Day 7
Mobx
LINK
LINK
LINK
@observable we put on a variable,arrays,objects,class instances that will be changed
@action is for the function that is changing that variable
@computed returns changed (computated) values when observable value is changed
Day 8
REACT NAVIGATION DOCS
npm install @react-navigation/native
expo install react-native-gesture-handler react-native-reanimated react-native-screens
react-native-safe-area-context @react-native-community/masked-view
Stack Navigator DOCS
npm install @react-navigation/stack
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
Tab Navigator DOCS
npm install @react-navigation/bottom-tabs
const Tab = createBottomTabNavigator();
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeDrawerNav} />
<Tab.Screen name="Settings" component={SettingsDrawerNav} />
</Tab.Navigator>
Drawer Navigator DOCS
npm install @react-navigation/drawer
const Drawer = createDrawerNavigator();
<Drawer.Navigator>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Drawer1" component={Drawer1Screen} />
<Drawer.Screen name="Drawer2" component={Drawer2Screen} />
</Drawer.Navigator>
It is hard to combine all three at once (two are easily possible).
Here is a LINK of what I was able to accomplish.
TODO: I DID NOT COVER PARAMS PART
Day 9 & 10
Redux - great and simple tutorial LINK
yarn add redux react-redux
Actions - functions that change state
/store/actions/information.js
export const TOGGLE_FAVORITE = 'TOGGLE_FAVORITE';
// function being called
export const toggleFavorite = (id) => {
return { type: TOGGLE_FAVORITE, informationId: id };
}
Reducers - functions receiving parameters state and action
/store/reducers/someReducer.js
import { TOGGLE_FAVORITE } from '../actions/information';
const initial State = {
someInformation: ['apple', 'orange', 'five', 'airplane'],
favoriteInformation: []
};
const someReducer = (state = initialState, action) => {
switch (action.type) {
case TOGGLE_FAVORITE:
const existingIndex = state.favoriteInformation.findIndex(
information => information.id === action.informationId
);
// informationId found so will be removed
if (existingIndex >= 0) {
const updatedInfo = [...state.favoriteInformation];
//remove that item
updatedInfo.splice(existingIndex, 1);
return { ...state, favoriteInformation: updatedInfo};
} else {
const info = state.someInformation.find(info => info.id === action.informationId);
return { ...state, favoriteInformation: state.favoriteInformation.concat(info) };
}
default:
return state;
}
return state;
}
/App.js
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import someReducer from './store/reducers/someReducer';
const rootReducer = combineReducers({
someData: someReducer
});
const store = createStore(rootReducer);
export default function App() {
return <Provider store={store}><MainScreen /></Provider>;
}
/screens/ConsumerScreen.js
import React, {useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleFavorite } from '../store/actions/information';
const dataFromStore = useSelector(state => state.someData.someInformation);
const dispatch = useDispatch();
// not sure if useEffect is really needed
// someInformationId is the id of some element we want to toggle
useEffect(() => {
dispatch(toggleFavorite(someInformationId))
}, [])
Debugging Redux in React Native
You can debug Redux in React Native apps with help of the React Native Debugger tool: https://github.com/jhen0409/react-native-debugger/blob/master/docs/redux-devtools-integration.md
1) Make sure you got the React Native Debugger installed (https://github.com/jhen0409/react-native-debugger)
2) Enable JS Debugging in the running app (open development overlay via
CTRL + M / CMD + M on Android devices, CMD + D on iOS devices)
3) Install the
redux-devtools-extension package via npm install --save-dev redux-devtools-extension (https://www.npmjs.com/package/redux-devtools-extension)
4) Enable Redux debugging in your code:
- import { createStore, applyMiddleware } from 'redux';
- import { composeWithDevTools } from 'redux-devtools-extension';
- const store = createStore(reducer, composeWithDevTools());
Started working on Shopping App
Step 1) Created folder structure
1. Added all the Screens
2. Added model of data
/models/Product.js
class Product {
constructor(id, ownerId, title, imageUrl, description, price) {
this.id = id;
this.ownerId = ownerId;
this.imageUrl = imageUrl;
this.title = title;
this.description = description;
this.price = price;
}
}
export default Product;
3. Added dummy data
/data/dummy-data.js
import Product from "../models/Product";
const PRODUCTS = [
new Product(
"p1",
"u1",
"Red Shirt",
"https://cdn.pixabay.com/photo/2016/10/02/22/17/red-t-shirt-1710578_1280.jpg",
"A red t-shirt, perfect for days with non-red weather.",
29.99
),
new Product(
"p2",
"u1",
"Blue Carpet",
"https://images.pexels.com/photos/6292/blue-pattern-texture-macro.jpg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
"Fits your red shirt perfectly. To stand on. Not to wear it.",
99.99
)
];
export default PRODUCTS;
4. Added action and reducer
/store/reducers/products.js
import PRODUCTS from "../../data/dummy-data";
// available = all products
// user = user created products
const initialState = {
availableProducts: PRODUCTS,
userProducts: PRODUCTS.filter((prod) => prod.ownerId === "u1"),
};
export default (state = initialState, action) => {
return state;
};
5. Added redux to the App.js
yarn add redux react-redux
import React from "react";
import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";
import ProductsReducer from "./store/reducers/products";
import ProductsOverviewScreen from "./screens/shop/ProductsOverviewScreen";
const rootReducer = combineReducers({
products: ProductsReducer,
});
const store = createStore(rootReducer);
export default function App() {
return (
<Provider store={store}>
<ProductsOverviewScreen />
</Provider>
);
}
6. Populated initial Screen with dummy data
/screens/ProductsOverviewScreen.js
//import libraries
import React from "react";
import { FlatList, Text} from "react-native";
import { useSelector } from "react-redux";
// create a component
const ProductsOverviewScreen = () => {
const products = useSelector((state) => state.products.availableProducts);
return (
<FlatList
data={products}
renderItem={(itemData) => <Text>{itemData.item.title}</Text>}
/>
);
};
//make this component available to the app
export default ProductsOverviewScreen;
Day 12
Added initial navigation
/navigation/ShopNavigator.js
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";
import ProductsOverviewScreen from "../screens/shop/ProductsOverviewScreen";
const defaultNavOptions = {
headerStyle: {
backgroundColor: "red",
},
headerTintColor: "white",
};
const ProductsStackNavigator = createStackNavigator();
const ProductsNavigator = () => {
return (
<NavigationContainer>
<ProductsStackNavigator.Navigator screenOptions={defaultNavOptions}>
<ProductsStackNavigator.Screen
name="ProductsOverview"
component={ProductsOverviewScreen}
/>
</ProductsStackNavigator.Navigator>
</NavigationContainer>
);
};
export default ProductsNavigator;
In App.js changed this part
export default function App() {
return (
<Provider store={store}>
<ProductsOverviewScreen />
</Provider>
);
}
with this
import ShopNavigator from "./navigation/ShopNavigator";
export default function App() {
return (
<Provider store={store}>
<ShopNavigator />
</Provider>
);
}
Link to this initial commit:
https://github.com/VladimirRodic/rn-shop-app/commit/eedcc3885651e79a0cafab84e31cab06f95070e2
To show remote image you must set it's width and height
<Image style={styles.image} source={{ uri: props.image }} />
styles = StyleSheet.create({
image: {
width: "100%",
height: "60%",
}
});
Showing Detailed Product by clicking on a product from a list
1. Add Detailed Product page to the Stack Navigator
<ProductsStackNavigator.Screen
name="Product Detail"
component={ProductDetailScreen}
options={({ route }) => ({ title: route.params.title })}
/>
2. Pass data from Overview to the ProductItem component (we had to pass navigation)
const ProductsOverviewScreen = ({ navigation }) => {
const products = useSelector((state) => state.products.availableProducts);
return (
<FlatList
data={products}
renderItem={(itemData) => (
<ProductItem
image={itemData.item.imageUrl}
title={itemData.item.title}
price={itemData.item.price}
itemId={itemData.item.id}
navigation={navigation}
/>
)}
/>
);
};
3. On ProductItem click should open Detailed Product page and send product as a param
4. Change the Navigation name dynamically (title, see step 1)
<Button
title="View Details"
onPress={() =>
props.navigation.navigate("Product Detail", {
itemId: props.itemId,
title: props.title,
})
}
/>
Day 13
There I have an issue that I can not take the navigation logic out of the component ProductItem up to the Screen ProductOverview and pass it as a function. Here I have it as a comment what I have tried to do. I have commented it out in the code.
Day 14
Solved the issue using the debugger. I was calling wrong variable name.
Adding items to Cart (full Redux cycle example)
1. create model for Cart
/models/cart-item.js
class CartItem {
constructor(quantity, productPrice, productTitle, sum) {
this.quantity = quantity;
this.productPrice = productPrice;
this.productTitle = productTitle;
this.sum = sum;
}
}
export default CartItem;
2. create action(s)
/store/actions/cart.js
export const ADD_TO_CART = "ADD_TO_CART";
export const addToCart = (product) => {
return { type: ADD_TO_CART, product: product };
};
3. create reducer
/store/reducers/cart.js
import { ADD_TO_CART } from "../actions/cart";
import CartItem from "../../models/CartItem";
const initialState = {
items: {},
totalAmount: 0,
};
export default (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
const addedProduct = action.product;
let updatedOrNewCartItem;
// if product was previously added
if (state.items[addedProduct.id]) {
updatedOrNewCartItem = new CartItem(
state.items[addedProduct.id].quantity + 1,
addedProduct.price,
addedProduct.title,
(state.items[addedProduct.id].sum += addedProduct.price)
);
} else {
// if product is added for the first time
updatedOrNewCartItem = new CartItem(
1,
addedProduct.price,
addedProduct.title,
addedProduct.price
);
}
return {
...state,
items: { ...state.items, [addedProduct.id]: updatedOrNewCartItem },
totalAmount: state.totalAmount + action.price,
};
}
return state;
};
App.js
import CartReducer from "./store/reducers/cart";
const rootReducer = combineReducers({
products: ProductsReducer,
cart: CartReducer,
});
5. call action from inside of the App
/components/shop/ProductItem.js
import { useDispatch } from "react-redux";
import * as cartActions from "../../store/actions/cart";
const ProductItem = (props) => {
const dispatch = useDispatch();
...
<Button
title="To Cart"
onPress={() => {dispatch(cartActions.addToCart(props.product));}}
/>
Day 15 & 16
Building Header Button for Shopping Cart icon
1. Button as a new component
/components/UI/HeaderButton.js
import { HeaderButton } from 'react-navigation-header-buttons'; LINK to docs
import { Ionicons } from '@expo/vector-icons';
const CustomHeaderButton = props => {
return (
<HeaderButton {...props} IconComponent={Ionicons} iconSize{23} color='white' />
);
}
export default CustomHeaderButton;
2. Show cart icon
// not sure which screen yet, probably Overview and Detail
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
import HeaderButton from '../../components/UI/HeaderButton';
navigationOptions CHECK V5
headerRight: <HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item title='Cart' iconName={md-cart} onPress={() => {navigation.navigate('Cart Screen')}}/>
<HeaderButtons/>
3. create Cart screen
/screens/shop/CartScreen.js
4. add Cart screen to the navigator
5. Add navigation to the Cart icon
// not sure which screen yet, probably Overview and Detail
6. fill Cart screen
total amount
order now JS function .toFixed(2)
list of items
we have them stored in list of Objects and want it as an array for the FlatList
HERE we can see how list of Objects is used instead of an array. I guess Maximilian switched it to Array to be able to check with array's length function whether there are objects i nthe Dictionary, but he could have achieved it like HERE Lodash _.isEmpty({}); DOCS
state.cart.items is an object
//defined in model
class CartItem {
constructor(quantity, productPrice, productTitle, sum) {
this.quantity = quantity;
this.productPrice = productPrice;
this.productTitle = productTitle;
this.sum = sum;
}
}
// define it initially
const initialState = {
items: {},
totalAmount: 0,
};
// add new cart item as an object
updatedOrNewCartItem = new CartItem(
1,
addedProduct.price,
addedProduct.title,
addedProduct.price
);
// adding it to state
return {
...state,
items: { ...state.items, [addedProduct.id]: updatedOrNewCartItem },
totalAmount: state.totalAmount + action.price,
};
WHEN ACCESSING IT I NEED TO TURN LIST OF OBJECTS TO ARRAY
TODO: check Lodash for this
// turning list of objects into array
const cartItems = useSelector(state => {
const transformedCartItems = [];
for (const key in state.cart.items) {
transformedCartItems.push({
productId: key,
productTitle: state.cart.items[key].productTitle,
productPrice: state.cart.items[key].productPrice,
quantity: state.cart.items[key].quantity,
sum: state.cart.items[key].sum
});
}
return transformedCartItems;
})
flatList will need to use KeyExtractor productId
CREATE CartItem.js COMPONENT
quantity, title, total amount for the item, delete from cart icon (ionicons and touchableOpacity)
DELETING ITEMS FROM THE CART
1. create action for removal
/store/actions/cart.js
export const REMOVE_FROM_CART = 'REMOVE_FROM_CART';
export const removeFromCart = productId => {
return { type: REMOVE_FROM_CART, pid: ProductId };
}
2. create reducer case for removal
/store/reducers/cart.js
import { REMOVE_FROM_CART } from '../actions/cart';
case REMOVE_FROM_CART:
getQuantity 2:40
// if quantity 1 remove whole object
//if quantity bigger than one, decrease quantity by 1
3. dispatch removal action
/screens/CartScreen.js
import { TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import * as cartActions from '../../store/actions/cart';
<TouchableOpacity
onPress={() => {
dispatch(cartActions.removeFromCart(item.id));
}}
>
<Ionicons name="md-trash" size={23} color="red" />
</TouchableOpacity>
Day 17
Orders
1. add action and reducer ADD_ORDER (orderData: cartItems, totalAmount)
2. add model order.js
Reducer
class Order {id, items, totalAmount, date} new Date()
// concat creates a new array and adds a new element
orders: state.orders.concat(newOrder)
get readableDate(){
return this.date.toLocaleDateString('en-EN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
3. add reducer to App.js
4. dispatch action from Cart and clear the Cart
5. create Orders screen
get orders with useSelector
6. create Drawer Navigation (ShopNavigator) to add Stack Navigators to it. Orders screen
7. ProductOverviewScreen and OrdersScreen create a hamburger menu that opens Drawer
import { HeaderButton, Item } from 'react-navigation-header-buttons';
import HeaderButton from '../../components/UI/HeaderButton';
headerLeft: (
<HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item
title="Menu"
iconName='md-menu'
onPress={() => {navData.navigation.toggleDrawer()}}
/>
</HeaderButtons>
),
8. In Drawer navigator add icons next to the items
navigationOptions: {
drawerIcon: drawerConfig => <Ionicons />
}
9. empty cart after order
in reducer for cart.js on ADD_ORDER return initialState at the end
10. styling OrdersScreen
separate OrderItem component
TOTAL
DATE
button SHOW DETAILS (showing items)
Shadow radius styling
shadowColor: 'black',
shadowOpacity: 0.26,
shadowOffset: {width:0, height: 2},
shadowRadius: 8,
elevation: 5,
borderRadius: 10,
backgroundColor: 'white'
