React Context and Provider data doesn't get passed down as expected - javascript

I am trying to update an object that another component is using.
My Context:
import React, {Component, createContext} from "react";
import jwt from 'jsonwebtoken';
const LOCAL_STORAGE_AUTH_TOKEN = 'authToken';
interface AuthenticatedUser {
username?: string;
guildName?: string;
guildId?: string;
}
interface AuthContextType {
authenticated: boolean; // to check if authenticated or not
user: AuthenticatedUser; // store user details
token: string; //jwt token
refreshToken: string; //jwt refresh token
handleAuthentication: (username: string, password: string) => Promise<void>; // handle login process
logout: () => Promise<void>; // log out the user
}
export const AuthContext = createContext<AuthContextType>({
authenticated: false, // to check if authenticated or not
user: {}, // store all the user details
token: '', // store all the user details
refreshToken: '', //jwt refresh token
handleAuthentication: (username: string, password: string): Promise<void> =>
Promise.resolve(), // handle login process
logout: (): Promise<void> => Promise.resolve(), // logout the user
});
AuthContext.displayName = 'AuthContext';
export class AuthProvider extends Component {
state = {
authenticated:false,
user: {},
token:'',
refreshToken:''
};
constructor(props: any) {
super(props);
const token = window.localStorage.getItem(LOCAL_STORAGE_AUTH_TOKEN) || '';
const jwtData = jwt.decode(token);
let user = {};
let authenticated = false;
let refreshToken = '';
if(jwtData && typeof jwtData !== 'string'){
authenticated = true;
user = {
username: jwtData.data?.username || '',
// TODO: Add the other sources too
}
}
this.state = {
authenticated,
user,
token,
refreshToken
}
}
handleAuthentication = async (username: string, password: string):Promise<void> => {
fetch(process.env.REACT_APP_API_ENDPOINT + "users/login", {
method:"POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({username:username, password:password})
})
.then(response => response.json())
.then((data) => {
/*
This data is coming back correctly
*/
const user = {
username: data?.user.username,
guildName: data?.user.guild_information[0].guildName,
guildId: "123123"
}
this.setState({
authenticated: true,
user: user,
token: data?.token,
refreshToken: data?.refreshToken
})
window.localStorage.setItem(LOCAL_STORAGE_AUTH_TOKEN, data?.token)
//TODO: Also save token in localstorage
})
.catch((err) => {
console.log(err)
})
}
logout = async ():Promise<void> => {
//TODO: Log out the current user
}
render() {
const authProviderValue = {
...this.state,
handleAuthentication: this.handleAuthentication,
logout: this.logout
}
return (
<AuthContext.Provider value={authProviderValue}>
{this.props.children}
</AuthContext.Provider>
)
}
}
And the App component where I use it:
import React, {useContext} from "react";
import { useStyles} from "./style";
import {AuthContext, AuthProvider} from "../../context/UserContext";
import RoutesProvider from "./Routes";
import Login from "../Login";
export default function App() {
const classes = useStyles();
const { user } = useContext(AuthContext)
return (
<AuthProvider>
{typeof user.username !== "undefined" ? (
<div className={classes.content}>
<RoutesProvider />
</div>
):(
<Login />
)}
</AuthProvider>
)
}
Also wrapped the App component within the AuthProvider as recommended:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { SnackbarProvider } from 'notistack';
import { AuthProvider} from "./context/UserContext";
import './styles/index.css'
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
It does take the initial value if I change it in the AuthContext. But after updating it does not update in the App component. I'm a little confused as to why this doesn't work as expected. If anyone knows what causes this (or if I'm doing it wrong) let me know please. Currently 5 hours into debugging...

If I understand your question correctly, you're asking:
"Why your user constant doesn't update when your handleAuthentication method is successful"
The answer is because you're initializing your user constant outside of your provider.
Use your AuthProvider in your index.tsx instead of your App component like this:
<AuthProvider>
<App />
</AuthProvider>
Or transfer your context logic in a children component

Related

Setting up Access Token Middleware in React when using a hook/context provider for your token

tl;dr:
How does one go about setting up a hook to handle token headers across axios api calls in a meaningfully maintainable way, with the assumption that the token itself is exposed as a hook.
I am currently handling authentication by exposing an access token/permissions in a context, and providing a protected route implementation that conditionally exposes the outlet OR a navigation call based on whether the token exists (which is retrieved from the hook).
Initially this works alright, and every component/hook in my application will have access to the hook to get the token. However, what I really want to do now is gain access to that hook where I make my api calls to set up an axios interceptor to manage the auth header for my api calls.
The issue I'm running into is I think any api call will have to be nested within a hook in order for me to use the token on it, and I'm not really sure what that looks like.
I'm using react-query, and was hoping I'd be able to use a mutation to set something to be accessed throughout the app, but that suffers the same pitfall of needing a component to be able to access the hook.
Is it possible to implement a hook for your token - appending middleware with axios?
the protected route implementation:
import React from 'react';
import { Outlet, useLocation, Navigate } from 'react-router-dom';
import { useAuth } from './AuthProvider';
const ProtectedRouterOutlet = () => {
const { token } = useAuth();
const location = useLocation();
if (!token) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet/>;
};
export default ProtectedRouterOutlet;
Auth provider context wrapper
const AuthContext = React.createContext<any>(null);
export const useAuth = () => {
return React.useContext(AuthContext);
};
const loginApiCall = (userName: string, password: string) =>{
if(!userName || !password) { return Promise.reject('Missing Credentials') }
return axios.post(`${auth_service}/oauth/token`, {username: userName, password: password})
}
const AuthProvider = ({ children }: any) => {
const navigate = useNavigate();
const [token, setToken] = React.useState<string | null>(null);
const location = useLocation();
useEffect(() => {
if(location.pathname === '/login' && token) {
navigate('/');
} else if (!token) {
navigate('/login');
}
}, [token])
const loginCall = useMutation( (data: any) => loginApiCall(data.username, data.password), {onSuccess: token => {
console.log('success', token);
setToken(token.data);
// I could do a settimeout here Or use the useeffect hook
// setTimeout(() => navigate('/'))
}})
const handleLogin = async (username: string, password: string) => {
loginCall.mutate({username, password});
};
const handleLogout = () => {
setToken(null);
// todo: call logout api to invalidate token
};
const value = useMemo(() => ({
token,
onLogin: handleLogin,
onLogout: handleLogout,
}), [token]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
and the main app file:
const rootComponent = () => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Shell>
<Outlet/>
</Shell>
</AuthProvider>
</QueryClientProvider>
);
};
EDIT:
I found this (setting defaults in axios), but I'm not sold on it yet:
useEffect(() => {
if(token) { // setting default common header if token exists
axios.defaults.headers.common = {...axios.defaults.headers.common, Authorization: `Bearer ${token.access_token}`};
}
if(location.pathname === '/login' && token) {
navigate('/');
} else if (!token) {
navigate('/login');
}
}, [token])

Why is my redux action firing twice and how can i fix that?

I am a complete beginner to redux and I am trying to create a discord clone. I am trying to create login functionality using google authentication with firebase and using redux to save the state. However, whenever I login or logout, the action is fired twice and I'm not exactly sure why. This is what shows up in react dev tools:
redux devtools trace 1
redux devtools trace 2
This is my store.js code
import { configureStore } from '#reduxjs/toolkit';
import userReducer from '../components/Reducers/userSlice';
import appReducer from '../components/Reducers/appSlice';
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({trace: true});
export const store = configureStore({
reducer: {
user: userReducer,
app: appReducer,
},
});
this is my userSlice.js code:
import { createSlice } from '#reduxjs/toolkit';
export const userSlice = createSlice({
name: 'user',
initialState: {
user: null,
},
reducers: {
login: (state, action ) => {
state.user = action.payload;
},
logout: (state) => {
state.user = null;
}
},
});
export const { login, logout } = userSlice.actions;
export const selectUser = (state) => state.user.user;
export default userSlice.reducer;
and these are the relevant lines of code from app.js
import { login, logout, selectUser } from './components/Reducers/userSlice'
import Login from './components/Login/Login';
import { auth } from './firebase';
function App() {
//allows us to shoot things into the datalayer
const dispatch = useDispatch()
//selects user from the data layer
const user = useSelector(selectUser)
useEffect(() => { //listens for change in user and reloads accordingly
auth.onAuthStateChanged((authUser) => {
if (authUser){
//user log in
dispatch(
login({
uid: authUser.uid,
photo: authUser.photoURL,
email: authUser.email,
displayName: authUser.displayName
}))
} else {
//user log out
dispatch(logout())
}
})
}, [dispatch])
return (
<div className="app">
{user ? (
<>
<Sidebar/>
<Chat />
</>
) : (
<Login />
)}
</div>
);
}
export default App;
and this is where I call the logout function
<div className="sidebar__profile">
<Avatar onClick={() => auth.signOut()} src={user.photo} />
<div className="sidebar__profileInfo">
<h3>{user.displayName}</h3>
<p>#{user.uid.substring(0, 5)}</p>
</div>
<div className="sidebar__profileIcons">
<MicIcon />
<SettingsIcon />
<HeadsetIcon />
</div>
</div>
this is where I login
import { Button } from '#mui/material'
import React from 'react'
import { auth, provider, signInWithPopup } from '../../firebase'
import './Login.css'
function Login() {
const signIn = () => {
//google authentication stuff
signInWithPopup(auth, provider)
.catch((error) => {
alert(error.message)
})
}
return (
<div className='login'>
<div className="login__logo">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Discord_white_D.svg/91px-Discord_white_D.svg.png?20180117191712" alt="Disclone logo" />
</div>
<Button onClick={signIn}>Sign In</Button>
</div>
)
}
export default Login
You are most likely subscribing twice to auth.onAuthStateChange(). This function returns an unsubscribe function which should be called to clean up (https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#returns-firebase.unsubscribe_1). useEffect can take a function as a return value that will be called before it runs again or finishes.
Change your useEffect() to capture the unsubcribe function returned and then return it:
useEffect(() => { //listens for change in user and reloads accordingly
const unsubscribe = auth.onAuthStateChanged((authUser) => {
if (authUser){
//user log in
dispatch(
login({
uid: authUser.uid,
photo: authUser.photoURL,
email: authUser.email,
displayName: authUser.displayName
}))
} else {
//user log out
dispatch(logout())
}
})
return unsubscribe
}, [dispatch])

Redux and Token Authentication : how to redirect the user

I made my own Django rest Framework API and I included token authentication. Everything works fine, I can get tokens and stuff, BUT, I have some trouble redirecting my user, using :
this.props.push('/');
Here is the Logic behind the authentication for the UI :
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {authLogin} from '../../actions/authentication.js';
class Login extends Component {
constructor(props){
super(props);
this.state = {
email: "",
password: ""
}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(e){
this.setState({[e.target.name]: e.target.value});
}
handleSubmit(e){
e.preventDefault();
this.props.authLogin(this.state.email, this.state.password);
console.log(this.props.isAuthenticated);
console.log(this.props.loading);
if (this.props.isAuthenticated){
this.props.history.push('/');
}
}
render(){
return(
<div className="Login">
<h1> This is a form </h1>
<form>
<input type="text" onChange={this.handleChange} name="email"/>
<input type="password" onChange={this.handleChange} name="password"/>
<input type="submit" onClick={this.handleSubmit}/>
{ this.props.isAuthenticated &&
<p>Hello </p>
}
</form>
</div>
)
}
}
const mapStateToProps = state => {
console.log(state);
return {
token: state.authentication.token
}
}
export default connect(mapStateToProps, {authLogin})(Login);
This Login Component is inside a Container Component and this is where I want my Redux to tell " hey, here is the new token " :
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import {connect } from 'react-redux';
import AppHeader from './header/AppHeader';
import HomePageStream from './HomePageStream/HomePageStream.js';
import HeaderCategories from './header/HeaderCategories.js';
import ArticleDetails from './ArticleDetails/ArticleDetails.js';
import { Route, Switch } from 'react-router-dom';
import { transitions, positions, Provider as AlertProvider } from 'react-alert'
import AlertTemplate from 'react-alert-template-basic'
import Alerts from './Alerts/Alerts.js';
import {authStateCheck} from '../actions/authentication.js';
import { Provider } from 'react-redux'
import { store } from '../store.js';
import { BrowserRouter } from "react-router-dom";
import Login from './Authentication/Login.js';
const options = {
timeout: 2000,
position: "top center"
}
class Container extends Component {
componentDidMount(){
this.props.authStateCheck();
}
render(){
return(
<div className="Container">
<Provider store={store}>
<AlertProvider template={AlertTemplate}
{...options}
>
<AppHeader />
<Alerts />
<HeaderCategories />
<Switch>
<Route exact
path="/:category"
render={routerProps => <HomePageStream {...routerProps}/>}
/>
<Route exact
path="/article/:slug"
render={routerProps => <ArticleDetails {...routerProps}/>}
/>
<Route exact
path="/authentication/login"
render={ routerProps => <Login {...routerProps}{...this.props}/>}
/>
</Switch>
</AlertProvider>
</Provider >
</div>
)
}
}
const mapStateToProps = state => {
console.log(state.authentication);
return {
isAuthenticated: state.authentication.token !== null,
loading: state.authentication.loading
}
}
export default connect(mapStateToProps, {authStateCheck} )(Container);
Here is the Problem :
When I click on submit, isAuthenticated is false, because I only get the token AFTER handleSubmit() has been called !
Here is the code for my action :
import axios from 'axios';
import {AUTH_START, AUTH_FAIL, AUTH_SUCESS, AUTH_LOGOUT} from './type';
export const authStart = () => {
return {
type: AUTH_START,
loading: true
};
}
export const authSucess = token => {
console.log(token);
return {
type: AUTH_SUCESS,
token: token,
error: null,
loading: false
};
}
export const authFail = error => {
console.log(error)
return {
token: null,
type: AUTH_FAIL,
error: error,
loading: false
};
}
export const logout = () => {
window.localStorage.removeItem('token');
window.localStorage.removeItem('expiration_time');
return {
type: AUTH_LOGOUT
}
}
export const checkAuthTimeOut = expiration_time => dispatch => {
return setTimeout(()=> {
dispatch(logout());
}, expiration_time * 1000);
}
export const authLogin = (email, password) => dispatch => {
dispatch(authStart());
console.log("I'm authlogin ! ");
axios.post('http://127.0.0.1:8000/rest-auth/login/',{
"email": email,
"password": password
})
.then( res => {
console.log("RESPONSE !")
const token = res.data.key
const expiration_time = new Date(new Date().getTime() + 3600 * 1000);
window.localStorage.setItem('token', token);
window.localStorage.setItem('expiration_time', expiration_time);
dispatch(authSucess(token));
console.log(token);
dispatch(checkAuthTimeOut(3600));
})
.catch( err => {
console.log(err);
dispatch(authFail());
})
}
export const authStateCheck = () => dispatch => {
const expiration_time = window.localStorage.getItem('expiration_time');
const token = window.localStorage.getItem('token');
if (!token){
return dispatch(logout());
} else if ( expiration_time <= new Date()){
console.log("EXPIRATION");
console.log( expiration_time <= new Date() )
return dispatch(logout());
} else {
console.log("NEW TIMER !!!! ");
return checkAuthTimeOut((expiration_time - new Date().getTime()) / 1000)
}
}
Here is my reducer :
import {AUTH_START, AUTH_FAIL, AUTH_SUCESS, AUTH_LOGOUT} from '../../actions/type';
const initialState = {
error: null,
token: null,
loading: false
}
export function authenticationReducer(state = initialState, action){
switch (action.type) {
case AUTH_START:
return {
...state,
loading: true
}
case AUTH_SUCESS:
console.log(action.token);
console.log(action.loading);
return {
...state,
error: null,
token: action.token,
loading: false
}
case AUTH_FAIL:
return {
...state,
error: action.error
}
default:
return state
}
}
But if i hit submit one more time, it works. But i really want my user to be immediately redirected. How can I fix this ??
B
Thank you so much ;(
I would suggest removing the history.push code and let react-router handle the redirection:
import { Route, Redirect } from 'react-router'
...
<Switch>
<Route exact
path="/:category"
render={routerProps => <HomePageStream {...routerProps}/>}
/>
<Route exact
path="/article/:slug"
render={routerProps => <ArticleDetails {...routerProps}/>}
/>
<Route exact
path="/authentication/login"
render={ routerProps => (this.props.isAuthenticated ? (<Redirect to="/"/>) :(<Login {...routerProps}{...this.props}/>))}
/>
</Switch>

How to use React Native AsyncStorage with Redux?

I have made login and logout actions and userReducer. How can I integrate AsyncStorage with Redux? I am using Redux Thunk as a middleware.
I am able to implement login and logout using internal state variable but I am not able to understand how to break it down into action and reducer as well as make use of AsyncStorage for storing accessToken.
Original Code:
_onLogin = () => {
auth0.webAuth
.authorize({
scope: 'openid profile',
audience: 'https://' + credentials.domain + '/userinfo'
})
.then(credentials => {
this.setState({ accessToken: credentials.accessToken });
})
.catch(error => console.log(error));
};
_onLogout = () => {
if (Platform.OS === 'android') {
this.setState({ accessToken: null });
} else {
auth0.webAuth
.clearSession({})
.then(success => {
this.setState({ accessToken: null });
})
.catch(error => console.log(error));
}
};
loginAction.js:
import { LOGIN_USER } from './types';
import Auth0 from 'react-native-auth0';
var credentials = require('./auth0-credentials');
const auth0 = new Auth0(credentials);
export const loginUser = () => dispatch => {
auth0.webAuth
.authorize({
scope: 'openid profile',
audience: 'https://' + credentials.domain + '/userinfo'
})
.then(credentials =>
dispatch({
type: LOGIN_USER,
payload: credentials.accessToken
})
)
.catch(error => console.log(error));
}
logoutAction.js:
import { LOGOUT_USER } from './types';
import Auth0 from 'react-native-auth0';
var credentials = require('./auth0-credentials');
const auth0 = new Auth0(credentials);
export const logoutUser = () => dispatch => {
auth0.webAuth
.clearSession({})
.then(success =>
dispatch({
type: LOGOUT_USER,
payload: null
})
)
.catch(error => console.log(error));
}
userReducer.js:
import { LOGIN_USER, LOGOUT_USER } from '../actions/types';
const initialState = {
accessToken: null
}
export default function (state = initialState, action) {
switch (action.type) {
case LOGIN_USER:
_storeData = async () => {
try {
await AsyncStorage.setItem('accessToken', action.payload);
} catch (error) {
console.log(error)
}
}
return {
...state,
accessToken:action.payload
};
case LOGOUT_USER:
_removeData = async (accessToken) => {
try {
await AsyncStorage.removeItem(accessToken);
} catch (error) {
console.log(error)
}
}
return {
...state,
accessToken:action.payload
};
default:
return state;
}
}
I am new to Redux so I tried converting original code into actions and reducers but I am not sure whether I have implemented AsyncStorage in userReducer.js correctly?
To persist redux state I recommend you redux-persist.
Installation:
npm i -S redux-persist
Usage:
First, configure redux store
// configureStore.js
import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web and AsyncStorage for react-native
import rootReducer from './reducers'
const persistConfig = {
key: 'root',
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export default () => {
let store = createStore(persistedReducer)
let persistor = persistStore(store)
return { store, persistor }
}
Then, wrap your root component with PersistGate
import { PersistGate } from 'redux-persist/integration/react'
// ... normal setup, create store and persistor, import components etc.
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};
You can conveniently use AsyncStorage alone OR redux to manage authentication state. Depends on which you are comfortable with. I will give you an example of both.
For AsyncStorage:
Assuming you have authentication keys that is valid for 2 weeks only. You can take note when your user logs in and save the time. eg:
//LoginScreen
import { onSignIn } from '../actions/auth'; //I will describe the onSignInMethod below
import axios from 'axios'; //lets use axios. You may use fetch too.
export default class LoginScreen extends Component {
//your code: state, static etc
loginMethod = () => {
const url = yourauthUrl;
const payload = {
email: this.state.email,
password: this.state.password
};
axios.post(url, payload)
.then((response) => {
if (response.status == 200) {
const dateOfLastLogin = new Date().getTime().toString(); //take note of the time the user logs in.
AsyncStorage.setItem('dateOfLastLogin', dateOfLastLogin);
}
})
.then(() => {
onSignIn() //onSignIn handles your sign in. See below.
.then(() => this.props.navigation.navigate('AfterSignInPage'));
})
.catch(() => { // your callback if onSignIn Fails
});
})
.catch((error) => { //your callback if axios fails
});
}
}
In ../actions/auth.js
import { AsyncStorage } from 'react-native';
export const onSignIn = () => AsyncStorage.setItem('auth_key', 'true');
//in LoginScreen we called this to set that a user has successfully logged in
//why is true a string? -- Because Asyncstorage stores only strings
export const onSignOut = () => AsyncStorage.multiRemove(['auth_key', 'dateOfLastLogin']);
//now lets create a method that checks if the user is logged in anytime
export const isSignedIn = () => {
return new Promise((resolve, reject) => {
AsyncStorage.multiGet(['auth_key', 'dateOfLastLogin'])
.then((res) => {
const userKey = res[0][1];
const lastLoginDate = parseInt(res[1][1]);
const today = new Date().getTime();
const daysElapsed = Math.round(
(today - lastLoginDate) / 86400000
);
if (userKey !== null && (daysElapsed < 14)) {
resolve(true);
} else {
resolve(false);
}
})
.catch((err) => reject(err));
});
};
now we can import { isSignedIn } from '../actions/auth'; from any of our components and use it like this:
isSignedIn()
.then((res) => {
if (res) {
// user is properly logged in and the login keys are valid and less than 14 days
}
})
////////////////////////////////////////////////////////////////////////////
If you want to use redux
Handling login in redux
In your types.js
//types.js
export const LOGGED_IN = 'LOGGED_IN';
In your redux actions
//loginActions.js
import {
LOGGED_IN,
} from './types';
export function login() {
let dateOfLastLogin = null;
let isLoggedIn = 'false';
AsyncStorage.multiGet(['auth_key', 'dateOfLastLogin'])
.then((res) => {
isLoggedIn = res[0][1];
dateOfLastLogin = parseInt(res[1][1]);
}); //note this works asynchronously so, this may not be a good approach
return {
type: LOGGED_IN,
isLoggedIn,
dateOfLastLogin
};
}
In your loginReducer
//LoginReducer.js
import {
LOGGED_IN
} from '../actions/types';
const initialState = {
userIsLoggedIn: false
};
export function loginReducer(state=initialState, action) {
switch (action.type) {
case LOGGED_IN:
const userKey = action.isLoggedIn;
const lastLoginDate = action.dateOfLastLogin;
const today = new Date().getTime();
const daysElapsed = Math.round(
(today - lastLoginDate) / 86400000
);
let trulyLoggedIn = false;
if (userKey !== null && (daysElapsed < 14)) {
trulyLoggedIn = true;
} else { trulyLoggedIn = false }
return {
userIsLoggedIn: trulyLoggedIn
};
default:
return state;
}
}
In your ./reducers/index.js
//reducers index.js
import { combineReducers } from 'redux';
import { loginReducer } from './LoginReducers';
const rootReducer = combineReducers({
loggedIn: loginReducer
});
export default rootReducer;
In your store where you used redux-thunk, applyMiddleWare. Lets call it configureStore.js
//configureStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}
In your App.js
//App.js
import { Provider } from 'react-redux';
import configureStore from './src/store/configureStore'; //where you configured your store
import { YourMainNavigator } from '../src/config/router'; //where your root navigator is
const store = configureStore();
export default class App extends Component<{}> {
render() {
return (
<Provider store={store}>
<YourMainNavigator />
</Provider>
);
}
}
You should know you no longer need the isSignedIn method in your auth.js
Your login method remains the same as outlined above in LoginScreen.
Now you can use redux to check the state of login like this:
import React, {Component} from 'react';
import {connect} from 'react-redux';
class MyComponent extends Component {
someFunction() {
if (this.props.loggedIn) {
//do something
}
}
}
const mapStateToProps = (state) => {
return {
loggedIn: state.loggedIn.userIsLoggedIn
};
}
export default connect(mapStateToProps)(MyComponent);
There should be a better way of using redux to manage login - better than what I outlined here. I think you can also use redux to manage your login state without using AsyncStorage. All you need to do is in your loginScreen, if the login functions returns a response.status == 'ok', you can dispatch an action to redux that logs the user in. In the example above, using asyncstorage you might only need to use redux to check if a user is logged in.
It is recommended that you use an abstraction on top of AsyncStorage instead of AsyncStorage directly for anything more than light usage since it operates globally. Redux-persist is that abstraction that goes on top of AsyncStorage. It provides a better way to store and retrieve more complex data (e.g. redux-persist has persistReducer(), persistStore()).
React native typescript implementation
storage.ts
import AsyncStorage from "#react-native-community/async-storage";
import { createStore, combineReducers } from "redux";
import { persistStore, persistReducer } from "redux-persist";
import exampleReducer from "./example.reducer";
const rootReducer = combineReducers({
example: exampleReducer,
});
const persistConfig = {
key: "root",
storage: AsyncStorage,
whitelist: ["example"],
};
// Middleware: Redux Persist Persisted Reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer);
// Middleware: Redux Persist Persister
let persistor = persistStore(store);
export { store, persistor };
App.tsx
import React from "react";
import { PersistGate } from "redux-persist/es/integration/react";
import { Provider } from "react-redux";
import RootNavigator from "./navigation/RootNavigator";
import { store, persistor } from "./store";
function App() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootNavigator />
</PersistGate>
</Provider>
);
}
export default App;

How to not overwrite dispatch with blank state

Background:
I am practicing the idea of React/Redux. I would want to follow the flow of data.
axios dispatches action -> reducer setState to props -> Component render()
The problem may be more than 1 point. Because I am new to Frontend world.
Please feel free to re-design my app(if needed)
Problem:
company does not render out because this.props.companies is blank. But axios does fetch the array from backend.
action/index.js
//First experiment action returns promise instance
export function fetchCompanies(token) {
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return {
type: FETCH_COMPANIES,
payload: instance
}
}
export function getCompanies(token){
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return instance
.then(data=> store.dispatch('GET_COMPANIES_SUCCESS', data));
}
company_reducers.js
import {FETCH_COMPANIES, GET_COMPANIES_ERROR, GET_COMPANIES_SUCCESS} from "../actions/const";
export default function (state = {}, action) {
switch (action.type) {
case GET_COMPANIES_SUCCESS:
return {
...state,
companies: action.payload
};
case GET_COMPANIES_ERROR:
return {
...state,
err_msg: action.payload.text
};
default:
return state;
}
}
reducers/index.js
import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import LoginReducer from './login_reducers';
import CompanyReducer from './company_reducers';
const rootReducer = combineReducers({
login: LoginReducer,
companies: CompanyReducer,
form: formReducer
});
export default rootReducer;
component/select_teams.js
import _ from 'lodash';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {fetchCompanies, getCompanies} from "../actions";
import {Link} from 'react-router-dom';
class SelectTeam extends Component {
constructor(props) {
super(props);
const token = localStorage.getItem('token');
this.state = {
token,
companies: null,
err_msg: null
}
}
componentWillMount() {
const tmp = this.props.getCompanies(this.state.token);
tmp.then(res => {
console.log(res)
})
.catch(err => {
console.log(err);
})
};
renderErrors() {
return (
<div>{this.state.err_msg}</div>
);
}
renderCompanies() {
return _.map(this.props.companies, company => {
return (
<li className="list-group-item" key={company.id}>
<Link to={`/${company.id}`}>
{company.name}
</Link>
</li>
)
});
}
render() {
if (this.props.companies === null) {
return (
<div>Loading...</div>
);
}
console.log(this.props);
return (
<div>
<h3>❤ Select Team ❤</h3>
{this.renderErrors()}
{this.renderCompanies()}
</div>
);
}
}
function mapStateToProps(state){
return {companies: state.companies}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
fetchCompanies: fetchCompanies,
getCompanies: getCompanies
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);
App.js
import React, {Component} from 'react';
import './App.css';
import SelectTeam from "./components/select_teams";
import reducers from './reducers/index';
import {Provider} from 'react-redux';
import promise from "redux-promise";
import {applyMiddleware, createStore} from 'redux';
import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
import LoginPage from './components/loginPage';
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const PrivateRoute = ({component: Component, isAuthorized, ...otherProps}) => (
<Route
{...otherProps}
render={props => (
isAuthorized() ? (<Component {...props} />) :
(
<Redirect to={
{
pathname: '/login',
state: {from: props.location},
}
}
/>
)
)}
/>
);
function PageNotFound() {
return (
<div>404 Page Not Found</div>
);
}
// TODO: I will add RESTful validation with backend later
function hasToken() {
const token = localStorage.getItem('token');
const isAuthenticated = !((token === undefined) | (token === null));
return isAuthenticated;
}
export const store = createStoreWithMiddleware(reducers);
class App extends Component {
//I will add security logic with last known location later.
//Get the features done first
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<PrivateRoute exact path="/select-teams" isAuthorized={hasToken} component={SelectTeam}/>
<Route path="/login" component={LoginPage}/>
<Route component={PageNotFound}/>
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;
You should dispatch an action with the data fetched from the server.
Actions are pure functions that return an object (the object has at minimum a TYPE field).
If you have any async operations, you may use Redux-Thunk, which is an action creator that returns a function, and call the api fetch within it.
Here is the actions snippet:
// imports..
export const fetchCompaniesSuccess = (data) => {
retyrn {
type: FETCH_COMPANIES_SUCCESS,
data
}
}
export const fetchCompanies = (token) => dispatch => {
// ...
axios(...).then(dispatch(data => fetchCompaniesSuccess(data)))
}
In your company_reducers.js,
// Company Reducer Function, State here represents only the companies part of the store
case FETCH_COMPANIES_SUCCESS: // should match the the type returned by the action
return [
...state,
...action.data
]
// other cases & default
MAKE SURE to add redux-thunk as a middleware in your createStore, read Redux-Thunk doc for instructions.
then in you component:
componentDidMount(){
this.props.fetchCompanies(this.state.token);
}
Once companies data is added to the redux store, your component will rerender and the companies array will be available in props
You don't need to have a duplicate companies array in the component state.
You may want to Watch Dan Abramov introduction to redux, it is a free course.
Seems like your dispatch syntax is wrong. The parameter should be an object with type and payload.
return instance
.then(data=> store.dispatch({
type: 'GET_COMPANIES_SUCCESS',
payload: data
}));

Categories

Resources