Thursday, April 2, 2020

100 days of React Native

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

Added eslint for React Hooks LINK and LINK and LINK
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))
}, [])

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:
Day 11

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(idownerIdtitleimageUrldescriptionprice) {
    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 = initialStateaction=> {
  return state;
};

5. Added redux to the App.js
yarn add redux react-redux

import React from "react";
import { createStorecombineReducers } 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 { FlatListTextfrom "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;
};

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'