I am trying to access auth state from my redux store in private route. I can get default state but cannot get updated state from store. I have stored my current user data and token in store. I can access these value from all other components except my private route component.
I have provided some code below
AdminRoute.js
import React from "react";
import { useSelector } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const AdminRoute = ({ component: Component, ...rest }) => {
const authState = useSelector((state) => state.authState);
console.log(authState.currentUser);
return (
<Route
{...rest}
render={(props) =>
authState.currentUser?.id ? (
<Component {...props} />
) : (
<Redirect to="/auth" />
)
}
/>
);
};
export default AdminRoute;
Store.js
import { configureStore } from "#reduxjs/toolkit";
import { uiReducer } from "../slices/uiSlice";
import { authReducer } from "../features/auth/authSlice";
import { userReducer } from "../features/user/userSlice";
export default configureStore({
reducer: {
uiState: uiReducer,
authState: authReducer,
userState: userReducer,
},
});
authSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { apiUrl } from "../../api/apiConfig";
import Axios from "axios";
import { tokenConfig } from "../../utils/tokenConfig";
export const userLogin = createAsyncThunk(
"auth/userLogin",
async (userData, { rejectWithValue, dispatch }) => {
const { email, password } = userData;
try {
const { data } = await Axios.post(`${apiUrl}/auth/login`, {
email,
password,
});
if (data) {
localStorage.setItem("token", data.token);
return data;
}
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data?.msg);
}
}
);
export const getCurrentUser = createAsyncThunk(
"auth/getCurrentUser",
async (_, { rejectWithValue, getState }) => {
try {
const { data } = await Axios.get(
`${apiUrl}/user/cUser`,
tokenConfig(getState)
);
if (data) {
return data;
}
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data?.msg);
}
}
);
const authSlice = createSlice({
name: "auth",
initialState: {
token: localStorage.getItem("token"),
isLoggedIn: false,
currentUser: null,
},
extraReducers: {
[userLogin.pending]: (state, action) => {
state.status = "loading";
},
[userLogin.fulfilled]: (state, action) => {
state.status = "success";
state.isLoggedIn = true;
state.token = action.payload.token;
},
[userLogin.rejected]: (state, action) => {
state.status = "failed";
state.error = action.payload;
},
[getCurrentUser.pending]: (state, action) => {
state.status = "loading";
},
[getCurrentUser.fulfilled]: (state, action) => {
state.status = "success";
state.currentUser = action.payload;
state.isLoggedIn = true;
},
[getCurrentUser.rejected]: (state, action) => {
state.status = "failed";
state.error = action.payload;
},
},
});
const { actions: authActions, reducer: authReducer } = authSlice;
export { authActions, authReducer };
It seems like your protected route should be rerendered when the redux state updates. Perhaps you could remove some of the complexity.
import React from "react";
import { useSelector } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const AdminRoute = (props) => {
const authState = useSelector((state) => state.authState);
return authState.currentUser?.id ? (
<Route {...props} />
) : (
<Redirect to="/auth" />
);
};
And just ensure you render the AdminRoute like any other regular Route component.
Ex:
<AdminRoute path="/protected" component={PrivateComponent} />
<AdminRoute
path="/protected"
render={props => <PrivateComponent {...props} />}
/>
<AdminRoute path="/protected">
<PrivateComponent />
</AdminRoute>
Related
I'm working on a react project. I have my own API to fetch information. I'm using the useEffect hook to fetch profile information from API. My problem is when page mounts for the first time i can fetch the data with no problem but if i refresh the page it doesn't work at all. I know i have to give a second parameter to useEffect. I tried to put profile as the second argument even dispatched the getCurrentProfile function but when i do that it constantly fires off fetch request. I would be glad if anyone can help me with that. Thanks.
Here is my Profile component:
export const Profile = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentProfile());
}, [])
const profileReducer = useSelector((state) => state.profile);
const authReducer = useSelector((state) => state.auth);
const { profile, error, loading } = profileReducer;
const { user } = authReducer;
console.log("loading", loading)
console.log("profile", profile)
return loading && profile === null ? (
<div >
<Spinner />
</div>
) :
Here is my Profile action:
export const getCurrentProfile = () => async dispatch => {
try {
const res = await axios.get("/api/profile/me");
console.log(res);
dispatch({
type: "GET_PROFILE",
payload: res.data.data
})
} catch (err) {
dispatch({
type: "PROFILE_ERROR",
payload: { msg: err.response.statusText, status: err.response.status }
})
}
}
Here is my profile reducer:
export default (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case "GET_PROFILE":
return {
...state,
profile: payload,
loading: false
}
case "PROFILE_ERROR":
return {
...state,
error: payload,
profile: null
}
case "CLEAR_PROFILE":
return {
...state,
profile: null,
loading: false
}
default:
return state;
}
}
You might want to try adding conditional logic within the useEffect so you only trigger the dispatch if you don't already have a profile.
import "./styles.css";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useCallback } from "react";
import { getCurrentProfile } from "./action";
export const Profile = () => {
const dispatch = useDispatch();
const profileReducer = useSelector((state) => state.profile);
const authReducer = useSelector((state) => state.auth);
const { profile, error, loading } = profileReducer;
// read more about this here: https://stackoverflow.com/questions/58624200/react-hook-useeffect-has-a-missing-dependency-dispatch
const stableDispatch = useCallback(dispatch, []);
useEffect(() => {
if (!profile) {
stableDispatch(getCurrentProfile());
}
}, [profile, stableDispatch]);
const { user } = authReducer;
console.log("loading", loading);
console.log("profile", profile);
return loading && profile === null ? <div>Spinner</div> : "Actual Profile";
};
export default Profile;
Also, it doesn't seem like you're currently doing anything with the loading piece of state–at least from what you've shared here. You might want to dispatch an action indicating that you're loading before you start the fetch and then it will be set to false when you get the response.
Check out this codesandbox for reference: https://codesandbox.io/s/focused-kilby-gd2nr?file=/src/App.js
Reducers:
const initialState = {
profile: null,
loading: false
};
export const profile = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case "LOADING_PROFILE":
return {
...state,
loading: true
};
case "GET_PROFILE":
return {
...state,
profile: payload,
loading: false
};
case "PROFILE_ERROR":
return {
...state,
error: payload,
profile: null
};
case "CLEAR_PROFILE":
return {
...state,
profile: null,
loading: false
};
default:
return state;
}
};
export const auth = (state = {}, action) => {
return state;
};
Action Creator:
import axios from "axios";
export const getCurrentProfile = () => async (dispatch) => {
try {
dispatch({ type: "LOADING_PROFILE" });
const res = await axios.get("https://jsonplaceholder.typicode.com/users/1");
console.log(res);
dispatch({
type: "GET_PROFILE",
payload: res.data.data
});
} catch (err) {
dispatch({
type: "PROFILE_ERROR",
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, combineReducers, applyMiddleware } from "redux";
import { profile, auth } from "./reducers";
import App from "./App";
import thunk from "redux-thunk";
const store = createStore(
combineReducers({
profile,
auth
}),
applyMiddleware(thunk)
);
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
rootElement
);
Well i solved it by dispatching 'getCurrentProfile' not 'getCurrentProfile()' turns out using it like a function causes continuously firing off.
const profileReducer = useSelector((state) => state.profile);
const authReducer = useSelector((state) => state.auth);
const { profile, error, loading } = profileReducer;
const dispatch = useDispatch();
useEffect(() => {
if (!profile) {
console.log("It worked")
dispatch(getCurrentProfile());
}
}, [dispatch(getCurrentProfile)])
I try to create a Redux and I have a problem when I try to do dispatch is not working.
Action file, userActions.js:
export const setId = () => {
console.log("Enter to set id func");
return {
type: 'SET_ID'
}
}
Reducer file, userReducer.js:
const INITIAL_STATE = {
id: "",
email: "",
name: "",
};
const userReducer = (state = INITIAL_STATE, action) => {
console.log('Enter to userReducer');
switch (action.type) {
case "SET_ID": {
// console.log(action.payload);
}
default: {
return state;
}
}
}
export default userReducer;
combineReducers file:
import userReducer from "./userReducer";
import { combineReducers } from "redux";
const allReducers = combineReducers({
userReducer: userReducer
})
export default allReducers;
App.js file:
import React from 'react';
import Routes from "./Routes";
import { createStore } from "redux";
import allReducer from "./app/reducers";
import { Provider } from "react-redux";
const store = createStore(
allReducer
);
const App = () => {
return (
<Provider store={store}>
<Routes />
</Provider>
);
};
export default App;
In login screen file, I have button when I click on him call to dispatch to "setId" action.
Here some of my code from Login.js:
import { useDispatch } from 'react-redux';
import { setId } from '../actions/userActions';
handleLoginResult = (error, user) => {
console.log('Enter to handleLoginResult');
if (error !== "") {
this.setState({ generalError: error });
} else {
const dispatch = useDispatch();
console.log('uid: ', user.user.uid);
dispatch(setId());
alert("Login!");
}
}
What is the problem and why is not enter to setId action?
You can try with like this
const userReducer = (state = INITIAL_STATE, action) =>dispatch =>{
console.log('Enter to userReducer');
switch (action.type) {
case "SET_ID": {
// console.log(action.payload);
}
default: {
return state;
}
}
}
I didn't quite understand your question, but I'll give you an example of an action of mine
export const register_user = ({ name, email, password, password_confirmation }) => {
return dispatch => {
dispatch(
{
type: CREATE_USER
}
)
let url = "/users"
Axios.post(`${SERVER}${url}`, {
"user": {
"name": name,
"email": email,
"password": password,
"password_confirmation": password_confirmation
}
})
.then(() => {
Alert.alert('Registrado com sucesso!')
registerUserSuccess(dispatch)
})
.catch((err) => {
registerUserError(err, dispatch)
})
}
}
const registerUserSuccess = (dispatch) => {
dispatch(
{
type: CREATE_USER_SUCCESS
}
)
this.props.navigation.navigate('Login')
}
const registerUserError = (err, dispatch) => {
dispatch(
{
type: CREATE_USER_ERROR
}
)
Alert.alert('Algo deu errado, verifique suas credenciais.')
}
The type is exported from my reducer.
And the register_user constant is imported and used on my register screen.
Hooks cannot be used inside a function. They need to declared directly inside the functional component.
Also useDispatch hook cannot be used inside a class component, you must use connect for a class component.
Assuming you have a class component, judging by how you use this.setState, you would write your code like
class Login extends React.Component {
...
handleLoginResult = (error, user) => {
console.log('Enter to handleLoginResult');
if (error !== "") {
this.setState({ generalError: error });
} else {
const dispatch = this.props;
console.log('uid: ', user.user.uid);
dispatch(setId());
alert("Login!");
}
}
...
}
export default connect()(Login)
If however you were to write login as a functional component, you would write it like
const Login = (props) => {
const dispatch = useDispatch();
const [state, setState] = useState({});
...
const handleLoginResult = (error, user) => {
console.log('Enter to handleLoginResult');
if (error !== "") {
setState({ generalError: error });
} else {
console.log('uid: ', user.user.uid);
dispatch(setId());
}
}
...
}
I am getting a dispatch is not defined error from 'shopfront' code. I believe it is because i'm not passing the properties down to the next level but I'm not sure if that is correct or not. I want to be able to pass the dispatch function through to the product.actions code correctly.
I have tried to narrow down the problem as much as possible by removing unnecessary code. I have a user reducer that is working correctly but I don't know why this product reducer isn't
// products.reducer
const initialState = {
products: null,
error: null
};
const ProductReducer = (state = initialState, action) => {
let newState = null;
switch(action.type){
case "GET_ALL_PRODUCTS": newState = {
...state,
products: action.products
};
return newState;
case "GET_ALL_PRODUCTS_FAIL": newState = {
...state,
error: action.error
};
return newState;
default: return state;
}
};
export default ProductReducer;
// index
import React from "react";
import ReactDOM from "react-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import { register } from "./serviceWorker";
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import UserReducer from "./store/reducers/users.reducers";
import ProductReducer from "./store/reducers/products.reducer";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const appReducer = combineReducers({
usersRed: UserReducer,
productsRed: ProductReducer
});
const logger = (store) => {
return next => {
return action => {
console.log("Middleware dispatching ");
console.log(action);
const result = next(action);
console.log("Middleware next state ");
console.log(store.getState());
return result;
};
};
};
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const appStore = createStore(appReducer, composeEnhancers(applyMiddleware(logger, thunk)));
const app = (
<BrowserRouter>
<Provider store={appStore}>
<App />
</Provider>
</BrowserRouter>
);
ReactDOM.render(app, document.getElementById("root"));
register();
// shopfront
import React, { Component } from "react";
import { Container, Row, Col, InputGroup, InputGroupAddon, Button } from "reactstrap";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { Alert } from "reactstrap";
import * as actionMethods from "../../store/actions/index.actions";
import Product from "../../components/Product/Product";
class Shopfront extends Component {
state = {
onAlert: false,
internalError: null
};
componentDidMount() {
this.props.loadAllProducts(5);
console.log("component_did_mount_run")
}
render() {
let ProductsList = <h1>No Products Yet!</h1>;
if (this.props.products !== null) {
ProductsList = this.props.products.map(Product => {
return <Product
key={Product.id}
title={Product.name}
excerpt={Product.description}
medialink={Product.permalink}
ProductId={Product.id}
/>;
});
}
return (
<Container>
{ProductsList}
</Container>
);
}
};
const mapStateToProps = (state) => {
return {
products: state.productsRed.products,
error: state.productsRed.error
};
};
const mapDispatchToProps = (dispatch) => {
return {
loadAllProducts: (perpage) => { dispatch(actionMethods.loadAllProducts(perpage)) }
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Shopfront));
// index.actions
export {
loadAllProducts
} from "./product.actions";
// product.actions
import wcapi from "../../axios-wp";
export const loadAllProducts = (perpage) => {
wcapi.get("products", {
per_page: perpage,
})
.then((response) => {
// Successful request
let productsRes = response.data;
dispatch({ type: "GET_ALL_PRODUCTS", products: productsRes });
})
.catch((err) => {
// Invalid request, for 4xx and 5xx statuses
dispatch({ type: "GET_ALL_PRODUCTS_FAIL", error: err });
})
}
Thank you for your help!
it is because you are not returning dispatch from your loadAllProducts action
/ product.actions
import wcapi from "../../axios-wp";
export const loadAllProducts = (perpage) => (dispatch) => { //make this change
wcapi.get("products", {
per_page: perpage,
})
.then((response) => {
// Successful request
let productsRes = response.data;
dispatch({ type: "GET_ALL_PRODUCTS", products: productsRes });
})
.catch((err) => {
// Invalid request, for 4xx and 5xx statuses
dispatch({ type: "GET_ALL_PRODUCTS_FAIL", error: err });
})
}
I have ever faced such this issue before. Then I used return dispatch => {} like this:
export const loadAllProducts = perpage => {
return dispatch => {
wcapi.get("products", {
per_page: perpage,
})
.then((response) => {
// Successful request
let productsRes = response.data;
dispatch({ type: "GET_ALL_PRODUCTS", products: productsRes });
})
.catch((err) => {
// Invalid request, for 4xx and 5xx statuses
dispatch({ type: "GET_ALL_PRODUCTS_FAIL", error: err });
})
}
}
I create modal for login when user submit the button , dispatch auth for send email and pass and history and after the response was okay it should dispatch idToken and isAuthenticated and in reducer should update isAuthenticated to true and store idToken but when i use it in my component i see just default state but in redxu devtools i see the state updated
action.js
.then((response) => {
if (response.status == 200) {
history.push("/dashboard");
return dispatch({
type: AUTH_SUCCESS,
payload: {
idToken: response.data.idToken,
isAuthenticated: true
}
});
}
})
.catch((error) => {
console.log(error.response.data.error.message);
// dispatch(authFail(error.response.data.error));
});
reducer.js
export default (state = initialState, action) => {
switch (action.type) {
case AUTH_SUCCESS: {
return {
...state,
idToken: action.payload.idToken,
isAuthenticated: traction.payload.isAuthenticated
};
}
case AUTH_FAIL:
return {
...state,
errorCode: action.code,
errorMessage: action.message
};
default:
return state;
}
};
privateRoute.js
const PrivateRoute = ({ component: Component, ...rest }) => {
const authenticated = useSelector((state) =>console.log('state',state.auth));
///state not updated ????
return (
<Route
{...rest}
render={(props) =>
authenticated ? <Component {...props} /> : <Redirect to="/login" />
}
/>
);
};
export default PrivateRoute;
store.js
import { compose, createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "../services/reducer";
export default (initialState) => {
// initialState = JSON.parse(window.localStorage.getItem('state')) || initialState;
const middleware = [thunk];
let enhancer;
enhancer = compose(
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
const store = createStore(rootReducer, initialState, enhancer);
// store.subscribe(() => {
// const state = store.getState();
// const token = {
// token: state.auth
// };
// window.localStorage.setItem("token", JSON.stringify(token));
// });
return store;
};
combinereducer.js
import { combineReducers } from 'redux';
import authReducer from './auth/reducer';
export default combineReducers({
auth:authReducer
})
app
const Root = ({ children, initialState = {} }) => (
<Provider store={store(initialState)}>{children}</Provider>
);
state in app doesn't update but in redux devtools show that my state is updated
I have a component that should navigate when a user is authenticated:
componentDidUpdate(prevProps, prevState) {
if (this.props.authenticated) {
this.props.navigation.navigate('Main')
}
}
When I dispatch authLogin it should cause a rerender, which handles the navigation:
export const authLogin = (username, password) => {
return dispatch => {
dispatch(authStart());
axios.post(`http://10.0.2.2:8000/api/v1/rest-auth/login/`, {
username: username,
password: password
})
.then(response => {
var token = response.data.key;
try {
AsyncStorage.setItem('token', token);
} catch (err) {
console.log(err)
}
dispatch(authSuccess(token));
})
.catch(err => {
dispatch(authFail());
console.log(err);
})
}
}
Here is my reducer:
export default function (state = initialState, action) {
switch (action.type) {
case "AUTH_START": {
return {
...state,
authenticating: true,
}
}
case "AUTH_SUCCESS": {
return {
...state,
authenticating: false,
authenticated: true,
token: action.token,
}
}
case "AUTH_FAIL": {
return {
...state,
authenticating: false,
authenticated: false,
}
}
case "AUTH_LOGOUT": {
return {
...state,
authenticating: false,
authenticated: false,
token: null,
}
}
default:
return state
}
}
and action creators:
export const authStart = () => ({type: "AUTH_START"})
export const authSuccess = token => ({type: "AUTH_SUCCESS", token})
export const authFail = () => ({type: "AUTH_FAIL"})
My console is logging that Redux actions are dispatched and that the state is changing, but no rerendering is happening. Here's the whole component:
import React, { Component } from 'react';
import { View, StyleSheet } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import LoginForm from '../components/LoginForm';
import { authLogin } from '../actions/authActions';
export class LoginScreen extends Component {
handlePress = async (username, password) => {
await this.props.authLogin(username, password);
}
componentDidUpdate(prevProps, prevState) {
if (this.props.authenticated) {
this.props.navigation.navigate('Main')
}
}
render() {
return (
<View style={styles.loginForm}>
<LoginForm handlePress={this.handlePress} {...this.props} />
</View>
);
}
}
const mapState = state => {
return {
authenticated: state.auth.authenticated
}
};
const mapDispatch = dispatch => {
return bindActionCreators({
authLogin,
}, dispatch)
};
export default connect(mapState, mapDispatch)(LoginScreen);
LoginScreen.propTypes = {
authLogin: PropTypes.func.isRequired,
authenticated: PropTypes.bool.isRequired,
};
const styles = StyleSheet.create({
loginForm: {
justifyContent: 'center',
alignItems: 'center',
flex: 1
}
});
and here's my store:
import { combineReducers } from 'redux';
import { createStore, applyMiddleware } from 'redux';
import { logger } from 'redux-logger';
import thunk from 'redux-thunk';
import auth from './auth'
const reducer = combineReducers({auth})
const enhancer = applyMiddleware(thunk, logger)
const store = createStore(reducer, enhancer)
export default store
The store is connected in the Provider in App.js.
I added another reducer, which fixed it instantly. Apparently redux didn't like that combineReducers() only had one argument.
i.e. change
const reducer = combineReducers({auth})
to
const reducer = combineReducers({auth, otherReducer})
Why not put the authentication check and navigation statement inside the handlePress().
handlePress = async (username, password) => {
await this.props.authLogin(username, password);
if (this.props.authenticated) {
this.props.navigation.navigate('Main')
}
}
After the authLogin() dispatches the action and state is updated, you can check the authentication status and navigate the user.
Hope this helps!