Wrapping React Native app with Context Provider - javascript

I've been following some simple tutorial (full working source code) to get the idea how to use React's Context together with handling authentication in my React Native app.
This example is using stateful components for views and handling routing the app within component itself, for example, in SignInScreen.js:
/* SignInScreen.js */
export default class SignInScreen extends React.Component {
static navigationOptions = {
title: 'Please sign in',
};
_signInAsync = async (saveToken) => {
saveToken()
.then((data) => {
// ROUTE USER TO "PROTECTED" PART OF THE APP
this.props.navigation.navigate('App');
})
.catch((error) => {
this.setState({ error })
})
};
render() {
return (
<View style={styles.container}>
<MyContext.Consumer>
{context => ((
<Button title="Sign in!" onPress={() => this._signInAsync(context.saveToken)} />
))}
</MyContext.Consumer>
</View>
);
}
};
I tried to transform this component into function component and move the signing in logic into my Context Provider like this:
/* SignInScreen.js - MODIFIED */
import React from 'react';
import { Button, View } from 'react-native';
import { MyContext } from '../Provider';
export default const LoginScreen = () => {
return (
<View>
<MyContext.Consumer>
{context => {
return (
<Button
onPress={() => context.signIn()}
title="Sign In"
/>
)
}
}
</MyContext.Consumer>
</View>
)
};
/* Provider.js */
import React from 'react';
import { AsyncStorage } from 'react-native';
export const MyContext = React.createContext();
export default class MyProvider extends React.Component {
constructor(props) {
super(props);
this.getToken = () => AsyncStorage.getItem('userToken');
this.saveToken = () => AsyncStorage.setItem('userToken', 'abc');
this.removeToken = () => AsyncStorage.removeItem('userToken');
this.signIn = () => {
this.saveToken()
.then((data) => {
// this.props.navigation DOES NOT EXIST!!! :(
this.props.navigation.navigate('App');
})
.catch((error) => this.setState({ error }));
};
this.state = {
token: '',
signIn: this.signIn,
};
}
componentWillMount() {
AsyncStorage.getItem('userToken')
.then((token) => {
this.setState({ token })
})
.catch(error => {
this.setState({ error })
})
}
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
}
When I press that "Sign In" button, my provider errors when I try to redirect user (this.props.navigation.navigate('App');) because this.props.navigation does not exist.
As far as I understood, this is happening because I didn't properly wrap my app with my Context.
This is my main App.js file:
/* App.js */
import React from 'react';
import { View } from 'react-native';
import MyContext from './Provider';
import AppNavigator from './navigation/AppNavigator';
export default class App extends React.Component {
render() {
return (
<MyContext>
<View>
<AppNavigator />
</View>
</MyContext>
);
}
}
and my AppNavigator.js:
/* AppNavigator.js */
import React from 'react';
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import AuthLoadingScreen from '../screens/AuthLoadingScreen';
import Auth from './AuthNavigator';
import App from './AppTabNavigator';
export default createAppContainer(createSwitchNavigator(
{
AuthLoading: AuthLoadingScreen,
Auth,
App,
},
{
initialRouteName: 'AuthLoading',
}
));
(AuthNavigator and AppTabNavigator contain only createStackNavigator() with my screens defined in it.)
My question is: how can I wrap this app with my Context so that Context Provider is always aware of navigation prop and so I could handle logging in and out and routing user from the Context Provider itself?

I solved this by using NavigationActions, pretty helpful built-in module designed for this purpose.

Related

Context empty after async initialisation

I am trying to fetch data from a backend API and initialise my FieldsContext. I am unable to do it, it returns an empty fields array in the Subfields component. I have spent hours on fixing it. But I eventually give up. Please take a look into this. Thanks in advance.
Here is my code
App.js
import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css'
import './App.css';
import Index from './components/pages/index/'
import FieldsProvider from './providers/fieldProvider'
import AuthProvider from './providers/authProvider'
import {BrowserRouter as Router,Switch,Route} from 'react-router-dom';
import SubFields from './components/pages/subfields';
function App() {
return (
<Router>
<AuthProvider>
<FieldsProvider>
<Switch>
<Route exact path="/" component={Index} />
<Route exact path="/:fieldid/subfields" component={SubFields} />
</Switch>
</FieldsProvider>
</AuthProvider>
</Router>
);
}
export default App;
FieldsContext.js
import React from 'react'
const FieldsContext = React.createContext();
export default FieldsContext
FieldsProvider.js
import React, { Component } from 'react'
import FieldsContext from '../libs/fieldContext'
export default class FieldsProvider extends Component {
state = {fields:[]}
getFields()
{
fetch('/api/fields')
.then(res => res.json())
.then(fields => this.setState({fields}));
}
async componentDidMount() {
await this.getFields();
}
render() {
return (
<FieldsContext.Provider value={this.state} >
{this.props.children}
</FieldsContext.Provider>
)
}
}
Subfields.js
import React, { Component } from 'react'
import FieldsContext from '../../../libs/fieldContext'
import FieldsList from '../../Fields/fieldlist'
export default class SubFields extends Component {
componentDidMount(){
// const fieldId = this.props.match.params.fieldid;
console.log(this.context);
}
render() {
return (
<div>
</div>
)
}
}
SubFields.contextType = FieldsContext
try using an ES6 Arrow function, which binds the function to the object instance, so that this refers to the object instance of the class when it is called.
When its called asynchronously, this will refer the the class object instance you want to update.
import React, { Component } from 'react'
import FieldsContext from '../libs/fieldContext'
export default class FieldsProvider extends Component {
state = {fields:[]}
// ES6 Arrow function
getFields = () =>
{
fetch('/api/fields')
.then(res => res.json())
.then(fields => this.setState({fields}));
}
async componentDidMount() {
await this.getFields();
}
render() {
return (
<FieldsContext.Provider value={this.state} >
{this.props.children}
</FieldsContext.Provider>
)
}
}
Alternatively, Try binding of your function in the class constructor.
export default class FieldsProvider extends Component {
state = {fields:[]}
constructor(props) {
//bind the class function to this instance
this.getFields = this.getFields.bind(this);
}
//Class function
getFields()
{
fetch('/api/fields')
.then(res => res.json())
.then(fields => this.setState({fields}));
}
async componentDidMount() {
await this.getFields();
}
render() {
return (
<FieldsContext.Provider value={this.state} >
{this.props.children}
</FieldsContext.Provider>
)
}
}
As a side note: Prefer to use functional components for consuming of ContextAPI.
import React, { Component } from 'react'
import FieldsContext from '../../../libs/fieldContext'
import FieldsList from '../../Fields/fieldlist'
export default function SubFields (props) {
const {
match
} = props;
//much better way to consume mulitple Contexts
const { fields } = React.useContext(FieldsContext);
//useEffect with fields dependency
React.useEffect(() => {
console.log(fields);
},[fields]);
return (
<div>
</div>
)
}

Export JSON object to another .js without class declaration react-native

I am developing a mobile app which makes GET calls using fetch api. I am stuck in that I am trying to export json object( fetched from server with fetch method) to another .js file to be used as array, But when I import my function in another.js (below), it returns nothing. I tested my fetch method with console so it works as expected, however I am unable to process data in another.js file. By the way, I have searched a lot and found this post Helpful, but not worked.
Below code is implementation of fetch part and exporting it.(Products.js)
import React, { PureComponent,Component } from "react";
import { connect } from "react-redux";
import { View } from "react-native";
import { productsDataSelector } from "../../Products/ProductsSelectors";
import ProductsList from "../../ProductsList/ProductsList";
import Product from "../../Product/Product";
import { NavigationActions, StackActions } from "react-navigation";
import AnotherComponent from "../../Products/ProductsReducer";
class Products extends PureComponent {
render() {
const { navigation } = this.props;
const { productsData } = this.props;
return (
<View>
<ProductsList list={productsData} isSortable>
{product => <Product product={product} />}
</ProductsList>
</View>
);
}
}
const mapStateToProps = state => ({
productsData: productsDataSelector(state)
});
export const getMoviesFromApiAsync = () =>
fetch('http://localhost:8080/JweSecurityExample/rest/security/retrieveItems')
.then((response) => response.json())
export default connect(
mapStateToProps,
null
) (Products);
Below code is another.js where importing fetch function and processing returning json object without class declaration implemented.
import React, { Component } from "react";
import {getMoviesFromApiAsyncc} from "../screens/Products/Products";
const fakeData = [];
export const someFunc = () => {
fetch('http://localhost:8080/JweSecurityExample/rest/security/retrieveItems')
.then((response) => response.json())
.then((responseJson) => console.log("responsee:"+JSON.stringify(responseJson)))
.then((responseJson) => {fakeData:JSON.stringify(responseJson)})
.catch((error) => {
console.error(error);
});
};
someFunc();
const initialState = {
data:this.fakeData
};
export default (state = initialState,action) => {
return state;
};
Any recommendations ?? Thanx
I don't see where in your code do you call someFunc and one more thing you need to wrap the object that you return from someFunc in braces otherwise it will be treated as the function's body.
export const someFunc = () => {
getMoviesFromApiAsync().then(response => {
fakeData = JSON.stringify(response)
})
};
someFunc();
I suggest that you move getMoviesFromApiAsync to a separate file and call it from your component to get the list of movies.
api.js
export const getMoviesFromApiAsync = () =>
fetch('http://localhost:8080/JweSecurityExample/rest/security/retrieveItems')
.then((response) => response.json());
product.js
import React, { PureComponent,Component } from "react";
import { connect } from "react-redux";
import { View } from "react-native";
import { productsDataSelector } from "../../Products/ProductsSelectors";
import ProductsList from "../../ProductsList/ProductsList";
import Product from "../../Product/Product";
import { NavigationActions, StackActions } from "react-navigation";
import AnotherComponent from "../../Products/ProductsReducer";
// import getMoviesFromApiAsync
import { getMoviesFromApiAsync } from 'PATH_TO_API.JS'
class Products extends Component {
async componentDidMount(){
const list = await getMoviesFromApiAsync();
console.log(list);
}
render() {
const { navigation } = this.props;
const { productsData } = this.props;
return (
<View>
<ProductsList list={productsData} isSortable>
{product => <Product product={product} />}
</ProductsList>
</View>
);
}
}

Cannot update during an existing state transition, additionally function passed as boolean and I dont know why

I have an App which has a state that contains the information if the user is loggged in or not. This state is passed to a Context Provider:
App.native.js
import React, { Component } from "react";
import Orientation, { orientation } from "react-native-orientation";
import Navigator from "./navigation/Navigator";
import { createContext } from 'react';
export const LoginContext = createContext({
isLoggedIn: false,
login: () => {},
logout: () => {}
});
export default class App extends Component {
constructor(props) {
super(props);
this.login = () => {
this.setState(state => ({
isLoggedIn: true
}));
console.log("Logged in!");
};
this.logout = () => {
this.setState(state => ({
isLoggedIn: false
}));
console.log("Logged out!");
};
this.state = {
isLoggedIn: false,
login: this.login,
logout: this.logout
};
}
componentDidMount = () => {
Orientation.lockToPortrait();
};
render() {
return <LoginContext.Provider value={this.state}><Navigator /></LoginContext.Provider>;
}
}
In (almost) every screen of the App I want to show an Icon in the header, e.g.:
SearchScreen.js
import styles from './styles';
import React, { Component } from 'react';
import { Text, View,AsyncStorage } from 'react-native';
import { Button } from 'react-native-elements';
import LoginIcon from '../../components/LoginIcon'
import { StatusBar } from 'react-native'
class SearchScreen extends Component {
static navigationOptions = ({ navigation }) => ({
headerTitle: "Suchen",
headerRight: <LoginIcon navigation={navigation} />
});
render() {
return (
<View style={styles.container}>
<StatusBar barStyle = "dark-content" hidden = {false} backgroundColor = "#338A3E" />
<Text>This is the SearchScreen.</Text>
</View>
);
}
}
export default SearchScreen;
The icon should show either a LogIn or Logout image, depending on the state of the App. I try to do that by using the Context Consumer:
LoginIcon.js
import styles from "./styles";
import React, { Component } from "react";
import { View, AsyncStorage } from "react-native";
import { Icon } from "react-native-elements";
import AntIcon from "react-native-vector-icons/AntDesign";
import {LoginContext} from "../../App.native.js";
const LoginIcon = ({navigation}) => {
return (
<LoginContext.Consumer>
{({isLoggedIn, login, logout}) => {
return (isLoggedIn ?
<Icon
name="logout"
type='material-community'
containerStyle={styles.icon}
color="white"
onPress={logout}
/>
:
<Icon
name="login"
type='material-community'
containerStyle={styles.icon}
color="white"
onPress={navigation.navigate("Login")}
/> );
}
}
</LoginContext.Consumer>
);
}
export default LoginIcon;
Now, when I login (via a LoginScreen not shown here), the state changes as expected (isLoggedIn = true), and the Icon in the Heder of the SearchScreen shows the correct "Logout" button. That is great!
But when I click this button, I get two errors:
1: Warning: Cannot update during an existing state transition (such as within 'render'). Render methods should be a pure function of props and state.
This is a riddle for me, I know I have a design flaw here but don't know how to solve it because ...well, basically everything happens inside a render function?
2) Failed prop type: Invalid prop 'onPress' of type 'boolean' supplied to 'Icon', expected 'function'.
in Icon (at withTheme.js:24) in Themed.Icon (at LoginIcon.js:22) in LoginIcon (at SearchScreen.js:12)
As far as I understand passing props to a function component is possible, but this.props should be left out when calling the prop inside the component. But it does not seem to work. How can I make that work?

Error: Actions must be plain objects. Use custom middleware for async actions, in a delete button?

I am trying to get a react action to fetch a list of files after the user deletes a file from the list.
In App.js I pass a handleClick function to the nested component.
App.js
class App extends Component {
static propTypes = {
files: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
handleClick : PropTypes.func
};
componentDidMount() {
const {dispatch} = this.props;
dispatch(fetchFiles);
}
handleClick = fileId => {
const {dispatch} = this.props;
deleteFileById(dispatch,fileId);
};
render() {
const {files, isFetching, dispatch} = this.props;
const isEmpty = files.length === 0;
return (
<div>
<h1>Uploadr</h1>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>No files.</h2>)
: <div style={{opacity: isFetching ? 0.5 : 1}}>
<Files files={files} handleClick={this.handleClick}/>
</div>
}
</div>
)
}
}
const mapStateToProps = state => {
const {isFetching, items: files} = state.files;
return {
files,
isFetching,
}
};
export default connect(mapStateToProps)(App)
Files.js
import React from 'react'
import PropTypes from 'prop-types'
const Files = ({files, handleClick }) => (
<ul>
{files.map((file, i) =>
<li key={i}>{file.name}
<button onClick={() => (handleClick(file.id))}>Delete</button>
</li>
)}
</ul>
);
Files.propTypes = {
files: PropTypes.array.isRequired,
handleClick: PropTypes.func.isRequired
};
export default Files
actions.js
I am wanting to trigger a request to get a new list of files from the API after the delete action is done.
export const deleteFileById = (dispatch, fileId) => {
dispatch(deleteFile);
return fetch(`/api/files/${fileId}`, {method : 'delete'})
.then(dispatch(fetchFiles(dispatch)))
};
export const fetchFiles = (dispatch) => {
dispatch(requestFiles);
return fetch('/api/files')
.then(response => response.json())
.then(json => dispatch(receiveFiles(json)))
};
However I am getting the following error
Error: Actions must be plain objects. Use custom middleware for async actions.
What is the best way to implement this
An action will dispatch another action but not event handler function.
You no need to dispatch deleteFileById from component because this is a function exported in actions which will dispatch an action.
Please remove dispatch in handleClick to work.
Wrong one:
handleClick = fileId => {
this.props.deleteFileById(dispatch(this.props.dispatch,fileId));
};
Correct one:
handleClick = fileId => {
this.props.deleteFileById(this.props.dispatch,fileId);
};
Regarding this.props.deleteFileById is not a function.
There are many ways to access actions in your component. Below are few ways
You need to install prop-types
npm install -s prop-types
If your component is Test then set prop types as like below
import PropTypes from 'prop-types';
import React, {Component} from 'react';
class Test extends Component{
render(){
return(
<div</div>
)
}
}
Test.propTypes = {
deleteFileById: PropTypes.func
}
If you are using redux connect then
Without prop-types
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Test extends Component{
render(){
return(
<div</div>
)
}
}
export default connect(null, {...actions})(Test);
OR
With inbuilt react proptypes you no need to install prop-types separately
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../actions';
import {push} from 'react-router-redux';
class Test extends Component{
static get propTypes() {
return {
sendContactForm: React.PropTypes.func
}
}
render(){
return(
<div</div>
)
}
}
const actionsToProps = {
deleteFileById: actions.deleteFileById,
push
}
export default connect(null, actionsToProps)(Test);
Your code App.jsx should be something like below
class App extends Component {
static propTypes = {
files: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
deleteFileById : PropTypes.func,
fetchFiles: PropTypes.func
};
componentDidMount() {
this.props.fetchFiles();
}
handleClick = fileId => {
this.props.deleteFileById(fileId);
};
render() {
const {files, isFetching} = this.props;
const isEmpty = files.length === 0;
return (
<div>
<h1>Uploadr</h1>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>No files.</h2>)
: <div style={{opacity: isFetching ? 0.5 : 1}}>
<Files files={files} handleClick={this.handleClick}/>
</div>
}
</div>
)
}
}
const mapStateToProps = state => {
const {isFetching, items: files} = state.files;
return {
files,
isFetching,
}
};
export default connect(mapStateToProps)(App)
dispatch should be returned in actions but not from component to actions or vice versa
Below is sample action file for your ref.
import ajax from '../ajax';
import {Map, fromJS} from 'immutable';
import config from '../config';
import {push} from 'react-router-redux'
export const URL_PREFIX = 'http://localhost:3000/api';
export const SEND_CONTACT_FORM_REQUEST = 'SEND_CONTACT_FORM_REQUEST';
export const SEND_CONTACT_FORM_SUCCESS = 'SEND_CONTACT_FORM_SUCCESS';
export const SEND_CONTACT_FORM_ERROR = 'SEND_CONTACT_FORM_ERROR';
export function sendContactFormRequest(){
return {
type: SEND_CONTACT_FORM_REQUEST,
loading: true
}
}
export function sendContactFormSuccess(data){
return {
type: SEND_CONTACT_FORM_SUCCESS,
loading: false,
data: data
}
}
export function sendContactFormError(errors){
return {
type: SEND_CONTACT_FORM_ERROR,
loading: false,
errors: errors
}
}
export function sendContactForm(firstName, lastName, email, subject, message) {
return dispatch => {
dispatch(sendContactFormRequest());
return ajax.post(URL_PREFIX + '/communication/contact', { firstName, lastName, email, subject, message })
.then(res => {
dispatch(sendContactFormSuccess(res.data))
})
.catch(errors => {
dispatch(sendContactFormError(errors))
})
}
}

Routing with React-Native

I have a HomePage.js with only one button, Login Button, and when clicked I want to render LoginPage.js. I tried something like this but it's not working:
HomePage.js:
import React, { Component } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import LoginPage from './LoginPage';
class HomePage extends Component{
constructor () {
super();
this.state = {LoginPage:false};
}
showLogin = () => {
this.setState({LoginPage:true});
}
render() {
return(
<View>
<TouchableOpacity onPress={() => this.showLogin()}>
<Text>LogIn</Text>
</TouchableOpacity>
</View>
)
}
}
export default HomePage;
In React it's easily done with react-router but I don't know how to do it with React-Native.
EDIT 1 after including react-navigation I get the following error: Route 'navigationOptions' should declare a screen. Did I miss something?
App.js:
import React, {Component} from 'react';
import {StackNavigator} from 'react-navigation';
import HomePage from './HomePage';
import LoginPage from './LoginPage';
const App = StackNavigator({
Home: {screen: HomePage},
LoginPage: {screen: LoginPage},
navigationOptions: {
header: () => null,
}
});
export default App;
HomePage.js
import React, { Component } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import LoginPage from './LoginPage';
class HomePage extends Component{
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigate } = this.props.navigation;
return(
<View>
<TouchableOpacity onPress={() => navigate('LoginPage'), { name: 'Login' }}>
<Text>Login</Text>
</TouchableOpacity>
<Button
onPress={() => navigate('LoginPage'), { name: 'Login' }}
>Log In</Button>
</View>
)
}
}
export default HomePage;
LoginPage.js
import React, { Component } from 'react';
import { Text, View } from 'react-native';
class LoginPage extends Component {
static navigationOptions = ({navigation}) => ({
title: navigation.state.params.name,
});
render() {
return (
<View>
<Text>This is login page</Text>
</View>
);
}
}
export default LoginPage;
Use React Navigation. Its the best and simple solution right now for react native.
for adding the library use npm install --save react-navigation
You can define class for routing like below using a StackNavigator.
import {
StackNavigator,
} from 'react-navigation';
const BasicApp = StackNavigator({
Main: {screen: MainScreen},
Profile: {screen: ProfileScreen},
, {
headerMode: 'none',
}
});
Then u can define classes like the one u mentioned in the question.
eg:
class MainScreen extends React.Component {
static navigationOptions = {
title: 'Welcome',
};
render() {
const { navigate } = this.props.navigation;
return (
<Button
title="Go to Jane's profile"
onPress={() =>
navigate('Profile', { name: 'Jane' })
}
/>
);
}
}
and second class
class ProfileScreen extends React.Component {
static navigationOptions = ({navigation}) => ({
title: navigation.state.params.name,
});
render() {
const { goBack } = this.props.navigation;
return (
<Button
title="Go back"
onPress={() => goBack()}
/>
);
}
}
The navigate function can be used for navigating to a class and goBack can be used for going back to a screen.
For hiding header u can use header mode or specify navigation options in each screen
For details please refer : https://reactnavigation.org/
If you're just looking for a plugin like react-router to use on React Native, you can use react-native-router-flux, available here: https://github.com/aksonov/react-native-router-flux.
Also, it's better if you just put a reference to the function in there instead of invoking it. Something like:
<TouchableOpacity onPress={this.showLogin}>...</TouchableOpacity>

Categories