React Native: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
Line 552: Line 552:
       )}
       )}
...
...
</syntaxhighlight>
==Wordpress REST API and React Render==
Finally built the render blog screen. The wordpress REST API was something I did not know came free but the results were disappointing. Anyway here is how to use it
<syntaxhighlight lang="jsx">
...
  const getBlogDetail = () => {
    const url = `http://xxx.xxx.xxx.xxx/wordpress/wp-json/wp/v2/posts/${blogId}?_embed`;
    fetch(url)
      .then((response) => response.json())
      .then((responseJson) => {
        setPostLoaded(true);
        setPostTitle(responseJson.title);
        setPostImage(mediaUrl(responseJson));
        setPostContent(responseJson.content);
        setPostID(responseJson.id);
      })
      .catch((error) => {
        console.error(error);
      });
  };
...
// Extraction of the Featured Media Url
  const mediaUrl = (obj) => {
    try {
      return obj._embedded["wp:featuredmedia"][0].source_url;
    } catch (error) {
      return "";
    }
  };
</syntaxhighlight>
</syntaxhighlight>

Revision as of 03:27, 14 December 2020

Introduction

Why

  • True native app
  • Performance is great
  • Easy to learn
  • Shared across platforms
  • Good community

==Components

  • React uses components to build apps
  • React Native has many components
  • Including translate features

Installation

sudo npm i -g react-native-cli
sudo npm i -g create-react-native

Create Project

Note ignite is a useful tool for creating components in react-native

react-native init reactiveNativeCLI
npx create-react-native-app globo
expo start

Sample App.js

This is not much different to java and Xamarin.
App.js

import React from 'react';
import Home from './app/views/Home.js'

export default class App extends React.Component {
  render() { 
    return (
      <Home />
    )
  }
}


Home.js

import React from "react";
import { Text, View } from "react-native";

export default class Home extends React.Component {
  render() {
    return (
      <View>
        <Text>This will be the homepage</Text>
        <Text>These other lines are here</Text>
        <Text>So you are not mad</Text>
      </View>
    )
  }
}

Styles

Inline Styles

These can be defined with double brackets. Note React supports flex :)

..
    return (
        <View style={styles.container}>
          <Header message = 'Press to Login'></Header>
          <Text style={{flex:8}}>This will be the homepage</Text>
          <Text style={{flex:6}}>These other lines are here</Text>
        </View>
      )
..

Using const Styles

Simple create a style and attach it to the component

import React, { useState } from 'react';
import {StyleSheet, Text, View} from 'react-native';

const Header = (props) => {

    const [loggedIn, setLoggedIn] = useState(false);

    const toggleUser = () => {
        const newLoggedIn = loggedIn ? false : true
        setLoggedIn( newLoggedIn)
     }

    const display = loggedIn ? 'Sample User' : props.message

    return (
        <View style={styles.headStyle}>
            <Text style={styles.headText} onPress={toggleUser}>{display}</Text>
        </View>
    )
}
const styles = StyleSheet.create({

    headText: {
        textAlign: 'right',
        color: '#ffffff',
        fontSize: 30,
    },
    headStyle: {
        paddingTop: 30,
        paddingBottom: 10,
        paddingRight: 10,
        backgroundColor: '#35605a'
    },
})

export default Header

Platform Support

There are API in the Platform package to support the platforms. These provide helpers for things which are platform specific e.g. version, dimensions and others. You can have React load the appropriate js by name a file Home.ios.js and Home.android.js and it will load the correct one.

Using Images

This is how to use the image without assets.

...
import { StyleSheet, Text, View, Image } from "react-native";
...
  return (
    <View style={styles.headStyle}>
      <Image
        style={styles.logoStyle}
        source={require("./img/Globo_logo_REV.png")}
      />
      <Text style={styles.headText} onPress={toggleUser}>
        {display}
      </Text>
    </View>
  );
....
const styles = StyleSheet.create({
...
  logoStyle: {
    flex: 1,
    width: undefined,
    height: undefined,
  },
});

Detecting Touch

Alert

Could not get this to work on the web so here is an implementation

import { Alert, Platform } from 'react-native'

const alertPolyfill = (title, description, options, extra) => {
    const result = window.confirm([title, description].filter(Boolean).join('\n'))

    if (result) {
        const confirmOption = options.find(({ style }) => style !== 'cancel')
        confirmOption && confirmOption.onPress()
    } else {
        const cancelOption = options.find(({ style }) => style === 'cancel')
        cancelOption && cancelOption.onPress()
    }
}

const alert = Platform.OS === 'web' ? alertPolyfill : Alert.alert

export default alert

Detecting Press

This was the approach. Not sure the benefits between TouchableOpacity. Probably looks nice.

        <TouchableOpacity style={styles.buttonStyles} onPress={onPress}>
          <Text style={styles.buttonText}>ABOUT</Text>
        </TouchableOpacity>

Navigation

Install

We are going to use react-navigation

    "@react-native-community/masked-view": "^0.1.10",
    "@react-navigation/native": "^5.8.10",
    "@react-navigation/stack": "^5.12.8",

Configure

In App.js create an object containing all of the routes and the initial route

import React from "react";

import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";

import Contact from "./views/Contact";
import Home from "./views/Home";

const Stack = createStackNavigator();

export function MyStack() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="HomeRoute"
        screenOptions={{
          headerShown: false,
        }}
      >
        <Stack.Screen name="HomeRoute" component={Home} />
        <Stack.Screen name="ContactRoute" component={Contact} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default MyStack;

Implement in App

We need to change the app to use the routing instead on a hardcoded activity

import React from 'react';

import { MyStack } from './app/Routing';

function App() {
    return (
      <MyStack />
    )
}
export default App

Implement in Component(Activity)

In the initial component make sure we pass the navigation to the menu.

const Home = (props) => {
  const { navigate } = props.navigation;

  return (
    <View style={styles.container}>
      <Header message="Press to Login"></Header>
      <Hero />
      <Menu navigate={navigate} />
    </View>
  );
};

Passing Parameters

Passing parameters with React Navigation changed in later versions. You can pass parameters to the navigated page with

    const onPress = () => {
        props.navigate('VideoDetailRoute', { ytubeId: props.id})
    }

To receive the values

  const { route, navigation } = props;
  const tubeId = route.params.ytubeId;

Implement in Menu

So now we have navigate passed around we can use it in the Menu.

      <View style={styles.buttonRow}>
        <TouchableOpacity style={styles.buttonStyles} onPress={onPress}>
          <Text style={styles.buttonText}>BLOG</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.buttonStyles} onPress={()=>props.navigate('ContactRoute')}>
          <Text style={styles.buttonText}>CONTACT</Text>
        </TouchableOpacity>
      </View>

Using External Data and Libraries

Video Library

They used google to start this project. Log onto https://console.developers.google.com/ and create a project from the My Project dropdown. Hit the + to create a new project and select YouTube Data API v3 and Enable it. Got to credentials and create an API key. To implement the fetch we use fetch and useEffect to retrieve the data at startup.

 {
    getVideo();
  }, []); // <-- empty dependency array

  const getVideo = () => {
    const key = "xxxxxx";
    const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&q=pluralsight&type=video&key=${key}`
    return fetch(url)
      .then((response) => {
        return response.json();
      })
      .then((responseJson) => {
        const data = Array.from(responseJson.items);
        setVideoList(data);
        setListLoaded(true);
      })
      .catch((error) => {
        console.log("error received is :", error);
      });
  };

An example of Flatlist

Just in case I need it an example of formatting the json in a flatlist

          <FlatList
            data={videoList}
            keyExtractor={(item, index) => item.id.videoId}
            renderItem={({ item }) => (
              <TubeItem
                navigate={props.navigate}
                id={item.id.videoId}
                title={item.snippet.title}
                imageSrc={item.snippet.thumbnails.high.url}
              />
            )}
          />
        </View>

Extra Notes

The WebView component does not work on Web with Expo but does on Android

AsyncStorage

Login

Here is an example login screen demonstrating AsyncStorage. The onChangeText required react-native-gesture-handler to be installed. Not clear in the errors

 
import React, { useState, useEffect } from "react";
import { Text,View, Alert, StyleSheet } from "react-native";
import { TextInput, TouchableHighlight } from "react-native-gesture-handler";
import AsyncStorage from '@react-native-community/async-storage'
const Register = (props) => {

  useEffect(() => {
    console.log(`Register Props =`, props);
  }, []); // <-- empty dependency array

  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');

  const cancelRegistration = () => {
    props.navigation.navigate("HomeRoute");
  };

  const registerAccount = () => {
    if (!username) {
      Alert.alert("Please Enter Username");
      console.log('Username is empty')
    } else if (password !== passwordConfirm) {
        Alert.alert("Password must match");
        console.log('passwords must match')
    } else {
      AsyncStorage.getItem(username, (err, result) => {
        if (result !== null) {
          Alert.alert(`${username} already exists`);
          console.log('account already exists')
        } else {
          AsyncStorage.setItem(username, password, (err, result) => {
            Alert.alert(`${username} account created`);
            console.log('account created')
            props.navigation.navigate("HomeRoute");
          });
        }
      });
    }
  };

  return (
    <View style={styles.container}>
        <Text style={styles.label}>Register Account</Text>

        <TextInput
            style={styles.inputs}
            onChangeText={setUsername}
            value={username}
        />
        <Text style={styles.label}>Enter Username</Text>

        <TextInput
            style={styles.inputs}
            onChangeText={setPassword}
            value={password}
            secureTextEntry={true}
        />
        <Text style={styles.label}>Enter Password</Text>

        <TextInput
            style={styles.inputs}
            onChangeText={setPasswordConfirm}
            value={passwordConfirm}
            secureTextEntry={true}
        />
        <Text style={styles.label}>Confirm Password</Text>

        <TouchableHighlight onPress={() => registerAccount()} underlayColor="#31e981">
            <Text style={styles.buttons}>Register</Text>
        </TouchableHighlight>

        <TouchableHighlight onPress={ () => cancelRegistration()} underlayColor="#31e981">
            <Text style={styles.buttons}>Cancel</Text>
        </TouchableHighlight>
    </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        paddingBottom: '45%',
        paddingTop: '10%'
    },
    heading: {
        fontSize: 16,
        flex: 1
    },
    inputs: {
        flex: 1,
        width: '80%',
        padding: 10,
    },
    buttons: {
        marginTop: 15,
        fontSize: 16,
    },
    labels: {
        paddingBottom: 10,
    },
})
export default Register;

Header

Here is the user of login. The import thing was the useEffect which required me to add a listener to work properly

 
import AsyncStorage from "@react-native-community/async-storage";
import React, { useState, useEffect } from "react";
import { StyleSheet, Text, View, Image, Alert } from "react-native";


const Header = (props) => {
  useEffect(() => {
      console.log(`Header Props =`, props);
    const unsubscribe = props.navigation.addListener("focus", () => {
      AsyncStorage.getItem("userLoggedIn", (err, result) => {
        if (result === "none") {
          console.log("NOME");
        } else if (result === null) {
          AsyncStorage.setItem("userLoggedIn", "none", (err, result) => {
            console.log("User set to none");
          });
        } else {
          setIsLoggedIn(true);
          setLoggedUser(result);
          console.log("Setting to logged in");
        }
      });
      return unsubscribe;
    });
  }, [props.navigation]); // <-- empty dependency array

  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [loggedUser, setLoggedUser] = useState(false);

  const toggleUser = () => {
    if (isLoggedIn) {
      AsyncStorage.setItem("userLoggedIn", "none", (err, result) => {
        setIsLoggedIn(false);
        setLoggedUser(false);
      });
      Alert.alert("User logged out");
      console.log("User logged out");
    } else {
      navigate("LoginRoute");
    }
  };

  const {navigate} = props.navigation

  const display = isLoggedIn ? loggedUser : props.message;

  return (
    <View style={styles.headStyle}>
      <Image
        style={styles.logoStyle}
        source={require("./img/Globo_logo_REV.png")}
      />
      <Text style={styles.headText} onPress={toggleUser}>
        {display}
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  headText: {
    textAlign: "right",
    color: "#ffffff",
    fontSize: 20,
    flex: 1,
  },
  headStyle: {
    paddingTop: 30,
    paddingRight: 10,
    backgroundColor: "#35605a",
    flex: 1,
    flexDirection: "row",
    borderBottomWidth: 2,
    borderColor: "#000000",
  },
  logoStyle: {
    flex: 1,
    width: undefined,
    height: undefined,
  },
});

export default Header;

Quiz

RenderItem Revisited

Well the quiz proved to be the hard part here and it was down to render item. In the flatlist the items are not re-rendered by default and therefore any update to the state are not known to the update function. To solve this you pass extraData with a toggled state. In my case refresh

...
// useEffect tracks the changes to questionAnswered and toggles the refresh value in state
  useEffect(() => {
    console.log("Doing useEffect again");
    setQuestLoaded(true);
    setRefresh(refresh => !refresh)
    console.log(`questionAnswered in useEffect = `, questionAnswered);

  }, [questionAnswered]); // <-- empty dependency array
...
// A much nice declarative renderItem
  const renderItem = ({item,index}) => (
    <Questions
      question={item.question}
      answer1={item.answer1}
      answer2={item.answer2}
      answer3={item.answer3}
      answer4={item.answer4}
      correctAnswer={item.correctAnswer}
      scoreUpdate={updateScore}
    />
  )
...
// Finally the Flatlist which passes the refresh flag
  return (
    <View style={styles.container}>
      {questLoaded && (
        <View>
          <FlatList
            extraData={refresh}
            data={questList}
            renderItem={(item,index)=>RenderItem(item,index)}
            keyExtractor={item=>item.key}
            />
        </View>
      )}
...

Wordpress REST API and React Render

Finally built the render blog screen. The wordpress REST API was something I did not know came free but the results were disappointing. Anyway here is how to use it

...
  const getBlogDetail = () => {
    const url = `http://xxx.xxx.xxx.xxx/wordpress/wp-json/wp/v2/posts/${blogId}?_embed`;
    fetch(url)
      .then((response) => response.json())
      .then((responseJson) => {
        setPostLoaded(true);
        setPostTitle(responseJson.title);
        setPostImage(mediaUrl(responseJson));
        setPostContent(responseJson.content);
        setPostID(responseJson.id);
      })
      .catch((error) => {
        console.error(error);
      });
  };
...
// Extraction of the Featured Media Url
  const mediaUrl = (obj) => {
    try {
      return obj._embedded["wp:featuredmedia"][0].source_url;
    } catch (error) {
      return "";
    }
  };