Async actions within a React component - javascript

I'm working on a login form in a project with React, Redux and Redux-Thunk. Using Redux-Thunk, I'm able to dispatch async actions like delivering the submitted login form to the back-end and bringing back validated data back to the state via reducers. Once the component gets the data it needs, it can then redirect to the page it needs without a problem.
The issue is, right before redirecting the user I need to write some data which came from asynchronous network request to the localStorage. If I don't do this async, the user gets redirected with the initial state values written to the local storage.
As a solution, I'm using promises and timeouts right in the React component to wait for the incoming data.
This approach seems to work but it doesn't feel right, can someone suggest me a better practice?
Here's the code in the component, I filtered most of the irrelevant things to make it as short as possible here.
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {browserHistory} from 'react-router';
import {reduxForm} from 'redux-form';
import {connect} from 'react-redux';
import {validate} from './../utils/login/surfaceValidation';
import {inputAlerts} from './../utils/login/inputAlerts';
import {submitLogin} from './../redux/actions/index';
class Form extends Component {
componentWillReceiveProps(nextProps) {
if(nextProps.loginApproved) {
this.handleValidLogin();
}
}
handleLogin(props) {
this.props.submitLogin(props);
// submitLogin is async action handled by redux-thunk it returns
// this.props.loginApproved and if it's true componentWillReceiveProps
// will trigger.
}
handleValidLogin() {
this.writeToStorage()
.then(() => {
browserHistory.push('/main');
}).catch((err) => {
console.log(err);
});
}
writeToStorage(){
return new Promise((resolve, reject) => {
setTimeout(() =>{
localStorage.setItem('user',
JSON.stringify({
authenticated: true,
username: this.props.username,
id: this.props.id,
token: this.props.token
}));
}, 3000);
setTimeout(() => {
if(this.props.token != null) {
resolve();
console.log('got a token - resolving.');
} else {
reject();
console.log('no token - rejecting. ');
}
}, 4000);
});
}
render() {
return (
// Login form component is here.
// When user submits form, this.handleLogin() is called.
);
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators({submitLogin});
}
function mapStateToProps(state) {
return {
loginApproved: state.login.loginApproved,
username: state.login.username,
id: state.login.id,
token: state.login.token
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Form);

As far as I know localStorage.seItem is synchronous so you can call function saving data to storage before redirecting.

Related

How to access a function from React Functional Component in a Normal Javascript File?

Greetings Javascript Developers. I'm stuck in a complex situation now where I need to access a function inside one of my functinal components outside in a normal js file.
Ok So here's what I'm doing: This is my Authorizer.js functional Component.
import React, { createContext, useState, useEffect, useContext } from "react";
import SplashScreen from "react-native-splash-screen";
import { useStore } from "../config/Store";
import { useDatabase } from "../config/Persistence";
import { getSessionCredentials } from "../config/Persistence";
import NavigationDrawer from "./NavigationDrawer";
import AuthStacks from "./AuthStacks";
const AuthContext = createContext();
export const useAuthorization = () => useContext(AuthContext);
export function Authorizer() {
//TODO check whether user is already signed in or not.
const realm = useDatabase();
const { state, dispatch } = useStore();
const [isAuthorized, setAuthorization] = useState(false);
useEffect(() => {
VerifyCredentials();
}, []);
async function VerifyCredentials() {
//TODO Check from Async Storage?
var session = await getSessionCredentials();
console.log("saved session", session);
if (session) {
await DispatchShopData();
await setAuthorization(true);
} else {
await setAuthorization(false);
}
sleep(1000).then(() => {
SplashScreen.hide();
});
}
async function DispatchShopData() {
try {
let shop = await realm.objects("Shop");
await dispatch({ type: "UPDATE_SHOP_DETAILS", payload: shop[0] });
} catch (error) {
console.log("failed to retrieve shop object", error);
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
return (
<AuthContext.Provider value={{ setAuthorization }}>
{isAuthorized ? <NavigationDrawer /> : <AuthStacks />}
</AuthContext.Provider>
);
}
This component basically handles my Authentication Flow, whether to show the Navigation Drawer or the Login Screen. Now I have another simple javascript file ApiService.js which does not have any components, only simple js functions.
import Axios from "axios";
import { getAuthToken } from "../config/Persistence";
import { LogoutUser } from "../config/Persistence";
import { Alert } from "react-native";
const BASE_URL = "#########################";
/** Defined my Api Endpoints Here */
let service = Axios.create({
baseURL: BASE_URL,
timeout: 10000,
});
service.interceptors.response.use((response) => {
console.log("[API] response intercepted data", response.data.message);
if (!response.data.status && response.data.tokenExpired) {
//Auth token has Expired. Show user Alert for Session Expired & redirect to login screen.
Alert.alert(
"Your Session has Expired!",
"Don't worry though. You just need to login again & you're set.",
[
{
text: "Continue",
style: "default",
onPress: () => {
LogoutUser()
.then((success) => {
if (success) {
//TODO Find a way to Access this function from Authorizer.js Component.
//setAuthorization(false);
}
})
.catch((error) => {
console.log("failed to logout after session expiry", error);
});
},
},
]
);
}
return response;
});
/** Defined my other api functions called inside my other components */
function TestSampleApi() {
try {
return new Promise(async function (resolve, reject) {
const response = await service.get("https://jsonplaceholder.typicode.com/users");
if (response.data != null) {
resolve(response.data);
} else {
reject(response.status);
}
});
} catch (error) {
console.log("request error", error.message);
}
}
export {
TestSampleApi,
/** Exporting other api functions as well */
};
In my ApiService.js file, I've setup a response interceptors whose job is to catch the default auth token expired response and SignOut user immediately and take him to the Login Screen. Here's now where my issue comes.
In normal scenarios, where I need to access functions from one component inside another component, I can manage is using CreateContext() and useContent() hooks. However, how do I access the useState function setAuthorization in my Authorizer.js components in my ApiService.js file as a normal js function.
I only need to call setAuthorization(false) from my response interceptor block to make the user return to the Login Screen. Problem is idk how to access that state setter function. So any help would be greatly appreciated.

NuxtJS / Vuex | nuxtServerInit and fetchData action not filling user on state

Built API with NodeJS, Express & MongoDB, used JWT and Cookies for user authentication.
Fetched user data from API with axios service using store (vuex). Created auth.js in store folder, created fetchData action which GETs the data from backend (axios.get(apiRoute)) and sets the user to state.
Wanted to do this using nuxtServerInit, so i craeted index.js file in store folder. Added empty state & actions. Action containts nuxtServerInit which uses dispatch() to call fetchData method in auth.js.
Yet after all of this, it doesn't work at all. For example: User is logged in, but account page is not rendering with user data (name, email, image etc.).
I tried returning a promise from fetchData action in auth.js, and it didn't work.
Also i tried setting up fetchData action insite of the index.js file and calling dispatch directly on it.
store/auth.js
// Importing Files
import axios from 'axios';
// State
export const state = () => ({
user: null
});
// Mutations
export const mutations = {
SET_USER (store, data) {
store.user = data
},
RESET_USER (store) {
store.user = null
}
};
// Actions
export const actions = {
// Fetch User Account
async fetchData ({ commit }) {
try {
const response = await axios.get('http://localhost:3000/api/v1/users/account');
commit('SET_USER', response.data.doc);
return response;
} catch (err) {
commit('RESET_USER');
}
}
};
store/index.js
// State
export const state = () => ({
});
// Actions
export const actions = {
async nuxtServerInit({ dispatch }) {
console.log('Testing');
const res = dispatch('auth/fetchData');
return res;
}
};
components/Settings.vue
<template>
<section class="data-block-wrap" v-if="user">
<BlockHeader :blockHeaderName="`Welcome Back, ${user.name.split(' ')[0]}`" btnText="More Details" />
<img :src="getPhotoUrl(user.photo)" alt="User Photo" class="user-data__image">
<p class="user-data__short-bio">{{ user.shortBio }}</p>
</section>
</template>
<script>
export default {
// Computed
computed: {
user() {
return this.$store.state.auth.user;
}
}
...
};
</script>
I expect to render user data properly on Vue components but currently it doesn't work at all. The render is static, no data from database / api showing.
EDIT / UPDATE
App renders user data properly when calling fetchData on created() hook in default.vue file ('Parent' file for all of the components).
default.vue
<template>
<div class="container">
<TopNav />
<SideNav />
<nuxt />
</div>
</template>
// Importing Components
import TopNav from '#/components/navigation/TopNav';
import SideNav from '#/components/navigation/SideNav';
import axios from 'axios';
import { mapActions } from 'vuex';
export default {
components: {
TopNav,
SideNav
},
methods: {
// Map Actions
...mapActions('auth', ['fetchData']),
async checkUser() {
const user = await this.fetchData();
},
},
// Lifecycle Method - Created
created() {
this.checkUser();
}
}
</script>
It seems that something very interesting is happening here. The problem is calling axios.get('http://localhost:3000/api/v1/users/account') from within nuxtServerInit().
This is causing what is essentially an infinite recursion. nuxtServerInit makes a call to http://localhost:3000, which hits the same server, runs nuxtServerInit again, and calls http://localhost:3000, and so on until the javascript heap is out of memory.
Instead of using nuxtServerInit for this, use the fetch method:
The fetch method is used to fill the store before rendering the page,
it's like the asyncData method except it doesn't set the component
data.
Note: You do not have access to the Nuxt component in fetch, so you must use the context object instead of "this"
// inside your page component
export default {
fetch (context) {
return context.store.dispatch('auth/fetchData');
}
}
As a general rule:
Use fetch to fill store data on the server or client
Use asyncData to fill component data on the server or client
Use nuxtServerInit for things like setting up the store with values on the request object, like sessions, headers, cookies, etc, which is only required server side
The solution to this question is to use the NuxtServerInt Action this way inside your store.js
1. you will need to run npm install cookieparser and npm install js-cookie
const cookieparser = process.server ? require('cookieparser') : undefined
export const state = () => {
return {
auth: null,
}
}
export const mutations = {
SET_AUTH(state, auth) {
state.auth = auth
},
}
export const actions = {
nuxtServerInit({ commit }, { req }) {
let auth = null
if (req.headers.cookie) {
try {
const parsed = cookieparser.parse(req.headers.cookie)
auth = parsed.auth
} catch (err) {
console.log('error', err)
}
}
commit('SET_AUTH', auth)
},
}
Then in your login page component, you call your backend API, just like this
import AuthServices from '#/ApiServices/AuthServices.js'
import swal from 'sweetalert'
const Cookie = process.client ? require('js-cookie') : undefined
async onSubmit() {
try {
const body = {
email: this.email,
password: this.password,
}
const res = await AuthServices.loginUrl(body)
console.log('res', res)
console.log('res', res.data.message)
setTimeout(() => {
// we simulate the async request with timeout.
const auth = {
accessToken: res.data.payload.token, // from your api call, you get the user token
userData: res.data.payload.user,
}
swal('Logged in', `${res.data.message}`, 'success')
this.email = this.password = ''
this.$refs.loginForm.reset()
this.$store.commit('setAuth', auth) // mutating to store for client rendering
Cookie.set('auth', auth) // saving token in cookie for server rendering
this.$router.push('/')
}, 1000)
} catch (error) {
console.log('error', error)
swal('Error!', `${error.message}`, 'error')
}
},
your AuthServices.js looks like this
import axios from 'axios'
const apiClient = axios.create({
baseURL: `http://localhost:3000`,
})
export default {
loginUrl(body) {
return apiClient.post('/login', body, {
headers: {
'Content-Type': 'application/json',
},
})
}
}
then you get the user data using computed in the navbar or say dashboard e.g to say Hi,Xavier
inside where you want place the user data, just add this
<template>
<section>
<p class="firtname_data">Hi, {{ user.firstnam }}</p>
</section>
</template>
<script>
export default {
// Computed
computed: {
user() {
return this.$store.state.auth.userData
}
...
};
</script>
Hope this help... it worked for me
I think you forgot to write await before dispatch
export const actions = {
async nuxtServerInit({ dispatch }) {
console.log('Testing');
const res = await dispatch('auth/fetchData');
return res;
}
}

Rerender Parent Component in React

I'm learning React and TypeScript and I am trying to write a login form, but after checking the user's data and creating the cookie, I want to rerender the parent component.
I have index.tsx (short version):
import React from 'react';
import ReactDOM from 'react-dom';
import cookie from 'react-cookies'
function Main() {
let hmCookies = cookie.loadAll();
console.log(hmCookies.auth);
if (hmCookies.auth === 'true') {
return (<Logout />)
} else {
return (<Login />)
}
}
ReactDOM.render(<Main />, document.getElementById('root'));
and Logint.tsx:
import React from 'react';
import cookie from 'react-cookies'
const axios = require('axios');
class Login extends React.Component<any, any> {
...
handleSubmit(event) {
axios.post('http://localhost/php/Login.php', {
login: this.state.login,
password: this.state.password
}, {headers: {'Content-Type': 'application/x-www-form-urlencoded'}})
.then(function (response) {
if (response.data.auth == true) {
cookie.save('auth', true);
}
})
.catch(function (error) {
console.log(error);
});
event.preventDefault();
}
render() { return ( <LoginFormHere /> ) }
}
export default Login;
After posting the user data in the form and making a request to PHP script via ajax, PHP returns a response. If it's true, save cookie. So, after changing cookie, I want to rerender component Main. However, I have no idea how to do this and I can't find any examples of this in the documentation.
How can this be achieved?
You can create a function in your Main component to serve to rerender. Let's call it - updateParent. Then you can pass updateParent function to your Login component as props and use it if response.data.auth == true. updateParent can be implemented as in the following question.
Here the example.
Also, you could use a stateful(class) component instead of functional. This allows you to use forceUpdate

Login With Reactjs not working as they say Dispatch is not a function

I am trying to implement a login in my reactjs application. I am quite new to react as well.
My Application contains several components such as about, navigation , login, signup, post, notification etc.
Here, There are components such as about, signup that a user can access without login but there are others such as notifications, posting that you can only access after login.
I am currently working on the login to the application. I have created a Login Component and in the login component, I have the function
handleSubmit(event) {
event.preventDefault();
this.setState({ submitted: true });
const credential = this.state.user;
const { dispatch } = this.props;
if( credential.username && credential.password){
dispatch(userActions.login(credential.username, credential.password));
//this.loginUser(credential);
}
}
I have imported import { userActions } from '../_actions'; to the login component.
In the UserActions, I have defined the login function as follows:
But when I submit the form, I get the error, TypeError: dispatch is not a function.
function login(username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
};
return fetch(URL_LOGIN, requestOptions)
.then(handleResponse)
.then(user => {
// login successful if there's a jwt token in the response
if (user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
}
return user;
});
}
Any help on this error will be greatly appreciated.
Also, I will be grateful if someone can give me a very simple guide that can help me with this login process on my application.
Note: I didn't start the application with redux and I just installed into my application, and I try to follow the tutorials here
http://jasonwatmore.com/post/2017/09/16/react-redux-user-registration-and-login-tutorial-example
though I am facing lots of difficulties with it.
Also, My API is in PHP
You have to get the access to dispatch method which is available with the store through
react-redux connect and Provider. you need to send your actions bound with dispatch. you can do this using two methods. The first method is simple but gets redundant when using multiple actions. The second one is what you use normally in react-redux applications
Method 1:
import { connect } from "react-redux"
function mapStateToProps(state) {
return {
yourValues: 1,
}
}
function mapDispatchToProps(dispatch) {
return {
login: (...rest) => {
dispatch(login(...rest))
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(YourComponent)
Method 2:
import { connect } from "react-redux"
import { bindActionCreators } from "redux"
import * as LoginActions from "/your-login-path"
function mapStateToProps(state) {
return {
yourValues: 1,
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
...LoginActions,
})
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(YourComponent)
Once you are done with this implementation, you will be able to access
login method in props. Now you can call login directly like this
const { login } = this.props
login(yourarguments)
Please read more about these on the redux website.

How to get the global state in an action creator in react-native and Redux?

I have successfully logged in my user using JWT with a rails backend. And have stored the user object in the global store for my react-native app using Redux and Redux-thunk.
However, I now need to access the state.user.authentication_token inside my other actions where I need to do a backend fetch request to get my data with the appropriate header have the authorised tokens.
Here is my AssetList.js component:
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { connect } from 'react-redux';
import { fetchAssets } from '../actions';
class AssetsList extends Component {
componentWillMount() {
this.props.fetchAssets();
}
render() {
return (
<View>
<Text>Asset name...</Text>
<Text>Asset name...</Text>
<Text>Asset name...</Text>
<Text>Asset name...</Text>
<Text>Asset name...</Text>
<Text>Asset name...</Text>
</View>
);
}
}
export default connect(null, { fetchAssets })(AssetsList);
And here is my Action file which dispatches the action:
/*global fetch:false*/
import { API_ENDPOINT } from './api';
export const fetchAssets = () => {
return (dispatch) => {
fetch(`${API_ENDPOINT}/assets`, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-User-Token': 'ztUUQuxsnFKhYf8JMLKY', // I need access the user state here
'X-User-Email': 'somecoolemail#gmail.com' // AND HERE...
}
}).then(response => {
dispatch({
type: 'FETCH_SUCCESSFUL'
});
response.json().then(data => {
console.log(data);
}).catch(data => {
console.log(data);
});
}).catch(error => {
console.log(error);
});
};
};
I need to access the user object from the state and replace the headers in the fetch request for the X-User-Token and the X-User-Email with the one stored in the state with the user object. However, I am not able to access my state inside the action file.
What am I missing here? How do I access the state in other files globally?
I haven't used redux in a little while, but this sounds like a perfect situation for a thunk.
EDIT 1 - rereading your question, appears you already are. So just pass in a second param, which will be the function getState.
export const fetchAssets = () => {
return (dispatch, getState) => {
const { user } = getState();
// user.token == token
// .. rest
};
}
https://github.com/gaearon/redux-thunk
http://redux.js.org/docs/advanced/AsyncActions.html
EDIT 2
Not specific to your question, and I will make a huge assumption here, but is your token actually part of state - I mean should it be? Unless the application changes it, it doesn't need to be part of your store. Don't fall into the common trap of thinking everything has to be in the redux store.

Categories

Resources