I am implementing authentication functionality in my app and when I try to save auth token which I get from my backend to reducer state it does nothing... I am new to this so there may be some dumb error.
This is my store.js file:
import React from 'react';
export const initialState = { access_token: null };
export const reducer = (state, action) => {
switch (action.type) {
case "SET_TOKEN":
console.log(action.data) // this does return the token which means data is passed correctly
return { access_token: action.data };
case "REMOVE_TOKEN":
return { access_token: null };
default:
return initialState;
}
};
export const Context = React.createContext();
This is my root component file AppRouter.js:
function AppRouter() {
const [store, dispatch] = useReducer(reducer, initialState);
const access_token = store.access_token;
console.log(access_token);
const AuthenticatedRoute = GuardedRoute(access_token);
return (
<Context.Provider value={{store, dispatch}}>
<Router>
<Switch>
<Route exact path="/" component={HomeScreen}/>
<Route exact path="/register" component={RegisterScreen}/>
<Route exact path="/login" component={LoginScreen}/>
<AuthenticatedRoute component={DashboardScreen} exact path={"/dashboard"}/>
</Switch>
</Router>
</Context.Provider>
)
}
So to me all this looks fine, and then this is the _login function in which I send the dispatch() to save the token(EDIT: this is everything between start of component function and return():
const [afterSuccessRegister, setAfterSuccessRegister] = useState(false);
const [emailInput, setEmailInput] = useState("");
const [passwordInput, setPasswordInput] = useState("");
const [loginErrorMessage, setLoginErrorMessage] = useState("");
const [createdUserEmail, setCreatedUserEmail] = useState("");
const { store, dispatch } = useContext(Context);
const _login = () => {
axios.post(`${ROOT_API}/v1/users/login`, {
"user": {
"email": emailInput,
"password": passwordInput
}
}, {}).then(res => {
console.log(res.data);
dispatch({type: 'SET_TOKEN', data: res.data.meta.access_token});
}).catch(err => {
console.log(err.response);
setLoginErrorMessage(err.response.data.message)
})
};
const _handleEmailChange = (e) => {
setEmailInput(e.target.value);
};
const _handlePasswordChange = (e) => {
setPasswordInput(e.target.value);
};
useEffect(() => {
if(typeof props.location.state !== "undefined") {
if (typeof props.location.state.success_register === 'undefined' || props.location.state.success_register === null || props.location.state.success_register === false) {
console.log("login");
} else {
setAfterSuccessRegister(true);
setCreatedUserEmail(props.location.state.created_user_email);
delete props.location.state;
}
}
}, [props.location.state]);
I really don't know why is it not saving it even though data is passed correctly. I tried adding console.log(store.access_token) after my login request has finished to see if it was saved, but it returns null.
Thanks!
Related
I'm fairly new to redux toolkit so I'm still having a few issues with it!
I'm stuck on this. I couldn't find what to do, how to follow a method. so i need your help.
My only request is to redirect the user to the home page when he logs in successfully, but I couldn't find what to check, where and how. I will share the code pages that I think will help you.
import { createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
import { TokenService } from "./token.service";
export const AuthService = {};
AuthService.userLogin = createAsyncThunk(
"userSlice/userLogin",
async ({ username, password }) => {
console.log(username, password);
try {
const response = await axios.post(
"https://localhost:7163/api/Login",
{ username, password },
{
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: typeof username === "string" ? username.toString() : "",
password: typeof password === "string" ? password.toString() : "",
}),
}
);
if (response.data.status) {
const tokenResponse = { accessToken: response?.data?.data?.token };
TokenService.setToken(tokenResponse);
} else {
console.log("false false false");
return response.data.status;
}
return response.data;
} catch (error) {
console.error(error);
}
}
);
import { createSlice } from "#reduxjs/toolkit";
import { AuthService } from "../../services/auth.service";
const userToken = localStorage.getItem("userToken")
? localStorage.getItem("userToken")
: null;
const initialState = {
userToken,
isLoading: false,
hasError: false,
userinfo: null,
};
const userSlice = createSlice({
name: "userSlice",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(AuthService.userLogin.pending, (state, action) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(AuthService.userLogin.fulfilled, (state, action) => {
state.userToken = action.payload;
state.isLoading = false;
state.hasError = false;
})
.addCase(AuthService.userLogin.rejected, (state, action) => {
state.isLoading = false;
state.hasError = true;
});
},
});
export const selectUserToken = (state) => state.userSlice.userToken;
export const selectLoadingState = (state) => state.userSlice.isLoading;
export const selectErrorState = (state) => state.userSlice.hasError;
export default userSlice.reducer;
export default function SignIn() {
const navigate = useNavigate();
const error = useSelector(selectErrorState);
console.log(error);
const [username, setName] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const data = {
username: username,
password: password,
};
const handleNameChange = (event) => {
setName(event.target.value);
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(username, password);
const response = dispatch(AuthService.userLogin(data))
.unwrap()
.then(() => {
if (response.success) {
navigate("/");
}
});
console.log("RESPONSE", response);
console.log(error);
};
//index.js
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="" element={<Home />} />
<Route path="events" element={<Events />} />
</Routes>
</BrowserRouter>
</Provider>
</React.StrictMode>
);
log
my result from API:
Does it make more sense to use the status property here?
{data: {…}, status: true, exception: null, code: 200, message: null}
code: 200
data: {token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfb…yMzl9.1P25An_PHA9n4dyQ9JRKOjwPWWtQShWgW9In-gqS7Ek'}
exception: null
message: null
status: true
[[Prototype]]: Object
Even if I enter wrong information, the promise always seems to be fulfilled.
I think I need to use token in this process. The user will see his own information. I can get the token somehow, but I couldn't use it after I put it in localstorage.
While on the login page, it struggled with the data returned from the login meta, but I couldn't. I want to know what is the most logical method in such a situation. Thank you in advance for your answers.
Building an app with a Django backend and React front end. When I attempt to login I receive a refresh and access token from Django. I would like to store that token in my localstorage and redirect my users to a static profile page if they have received a valid access token.
I am getting the following errors on my console:
Uncaught TypeError: Cannot read properties of undefined (reading 'getState')
at Provider.js:20:1
at mountMemo (react-dom.development.js:15442:1)
The above error occurred in the <Provider> component:
in Provider (at src/index.js:21)
in Router (at src/index.js:20)
Consider adding an error boundary to your tree to customize error handling behavior.
This is my login.js
const Login = () => {
const [username, setUserName] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = e => {
e.preventDefault()
try {
const response = AxiosInstance.post('/api/token/',{
username: username,
password: password
});
console.log('from api/token we get this:')
console.log(response)
AxiosInstance.defaults.headers['Authorization'] = "JWT " + response.access;
localStorage.setItem('access_token', response.access);
localStorage.setItem('refresh_token', response.refresh);
console.log('JWT response.access to refresh: ')
return response;
} catch (error) {
throw error;
}
}
Login.propTypes = {
login: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps, {
login
}) (withRouter(Login));
//export default Login;
LoginActions.js:
export const login = (userData, redirectTo) => dispatch => {
axios
.post("/api/token/", userData)
.then(response => {
const { auth_token } = response.data;
setAxiosAuthToken(auth_token);
dispatch(setToken(auth_token));
dispatch(getCurrentUser(redirectTo));
})
.catch(error => {
dispatch(unsetCurrentUser());
toastOnError(error);
});
};
export const getCurrentUser = redirectTo => dispatch => {
axios
.get("/users/")
.then(response => {
const user = {
username: response.data.username,
email: response.data.email
};
dispatch(setCurrentUser(user, redirectTo));
})
.catch(error => {
dispatch(unsetCurrentUser());
toastOnError(error);
});
};
export const setCurrentUser = (user, redirectTo) => dispatch => {
localStorage.setItem("user", JSON.stringify(user));
dispatch({
type: SET_CURRENT_USER,
payload: user
});
console.log("set user" + redirectTo);
if (redirectTo !== "") {
dispatch(push(redirectTo));
}
};
and Reducer.js
const createRootReducer = history =>
combineReducers({
router: connectRouter(history),
createUser: signupReducer,
auth: loginReducer
});
export default createRootReducer;
My Root.js:
export default ({ children, initialState = {} }) => {
const history = createBrowserHistory();
const middleware = [thunk, routerMiddleware(history)];
const store = createStore(
rootReducer(history),
initialState,
applyMiddleware(...middleware)
);
// check localStorage
if (!isEmpty(localStorage.getItem("token"))) {
store.dispatch(setToken(localStorage.getItem("token")));
}
if (!isEmpty(localStorage.getItem("user"))) {
const user = JSON.parse(localStorage.getItem("user"));
store.dispatch(setCurrentUser(user, ""));
}
return (
<Provider store={store}>
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</Provider>
);
};
Utils.js:
export const setAxiosAuthToken = token => {
if (typeof token !== "undefined" && token) {
// Apply for every request
axios.defaults.headers.common["Authorization"] = "Token " + token;
} else {
// Delete auth header
delete axios.defaults.headers.common["Authorization"];
}
};
export const toastOnError = error => {
if (error.response) {
// known error
toast.error(JSON.stringify(error.response.data));
} else if (error.message) {
toast.error(JSON.stringify(error.message));
} else {
toast.error(JSON.stringify(error));
}
};
export const isEmpty = value =>
value === undefined ||
value === null ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0);
and finally my index.js:
const history = createBrowserHistory();
ReactDOM.render(
<Router history={history}>
<Provider>
<App />
</Provider>
</Router>,
document.getElementById('root')
);
serviceWorker.unregister();
Any ideas as to where I might be going wrong?
You did not pass store to the Provider:
ReactDOM.render(
<Router history={history}>
// Make sure you import store correct
<Provider store={store} >
<App />
</Provider>
</Router>,
document.getElementById('root')
What would be the best practice for private routing? Maybe I'm doing something wrong, but when user logged in I already am redirect to the /login page
And my second question: Which of these versions is better or you have even better idea?
Code:
Auth
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: false,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isUserLoggedIn = false
})
},
})
export const me = createAsyncThunk('auth/me', async () => {
try {
const user = await userService.getUserData()
return { userData: user.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
return message
}
})
CASE 1:
App
function App() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(me())
}, [])
const auth = useSelector((state) => state.auth)
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes isAuthenticated={auth.isUserLoggedIn} />
</BrowserRouter>
</div>
)
}
export default App
Routes
export const AppRoutes = ({ isAuthenticated }) => (
<Routes>
<Route
path='/login'
element={<Login />}
/>
<Route
path='/dashboard'
element={
<PrivateRoute isAuthenticated={isAuthenticated}>
<Stats />
</PrivateRoute>
}
/>
...
PrivateRoute
export const PrivateRoute = ({ children, isAuthenticated }) => {
return isAuthenticated ? children : <Navigate to='/login' />
}
CASE 2:
App
function App() {
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</div>
)
}
export default App
Routes
export const AppRoutes = () => (
<Routes>
<Route
path='/login'
element={<Login />}
/>
<Route
path='/dashboard'
element={
<PrivateRoute>
<Stats />
</PrivateRoute>
}
/>
...
PrivateRoute
export const PrivateRoute = ({ children }) => {
const dispatch = useDispatch()
const { isUserLoggedIn } = useSelector((state) => state.auth)
useEffect(() => {
if (isUserLoggedIn === null) {
dispatch(me())
}
}, [])
return isUserLoggedIn ? children : <Navigate to='/login' />
}
CASE 1 or CASE 2 is better to approach or maybe you have a better idea?
For these 2 ideas, it redirects very quickly to /login when I go to /dashboard
What I want to achieve is good practice, quick verification, and waiting until we receive a positive response from the backend that the user is authenticated
Team, what do you suggest?
EDIT
New version:
Redux
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: true,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isLoading = false
state.isUserLoggedIn = false
})
},
})
export const me = createAsyncThunk('auth/me', async ({}, thunkAPI) => {
try {
const user = await userService.getUserData()
return { userData: user.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
thunkAPI.rejectWithValue(message)
return message
}
})
PrivateRoute
export const PrivateRoute = ({ children }) => {
const dispatch = useDispatch()
const { isLoading, isUserLoggedIn } = useSelector((state) => state.auth)
if (isLoading) return null
return isUserLoggedIn ? (
children
) : (
<Navigate
to='/login'
replace
/>
)
}
App
function App() {
const dispatch = useDispatch()
const { isUserLoggedIn } = useSelector((state) => state.auth)
useEffect(() => {
if (isUserLoggedIn === null) {
dispatch(me())
}
}, [])
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</div>
)
}
export default App
Store
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import authReducer from '../features/auth/authSlice'
export const store = configureStore({
reducer: {
auth: authReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }),
})
Index.js
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
But when I reload the page I go to /login don't stay at the same page like /dashboard how could I cope with that?
Either approach is fine. The issue you are seeing is based on the initial redux state value being used for auth check and redirection prior to the effect running to set the auth state. You will want to hold off on redirecting until the authentication status is determined. Use the state to rendering null or some loading indicator until a user is verified authenticated.
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: true, // <-- assume initially loading state from mouting
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isUserLoggedIn = false
})
},
});
Between the two implementations IMO the second is preferred as it leads to less component coupling. I suggest a mix of the two. Check for a user in App and dispatch the action to set the auth state, and check the auth state in the private route.
function App() {
const dispatch = useDispatch();
const { isUserLoggedIn } = useSelector((state) => state.auth);
useEffect(() => {
if (isUserLoggedIn === null) {
dispatch(me());
}
}, []);
return (
<div data-theme={theme}>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</div>
);
}
...
export const PrivateRoute = ({ children }) => {
const dispatch = useDispatch()
const { isLoading, isUserLoggedIn } = useSelector((state) => state.auth)
if (isLoading) return null; // <-- or loading spinner, etc...
return isUserLoggedIn ? children : <Navigate to='/login' replace />
}
The same can be achieved with the first version, you'd just need to pass the isLoading state along with the isUserLoggedIn state as props to the PrivateRoute.
I found a problem! Ha ha!
Seems stupid && simple but it started working ->
Redux
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
isUserLoggedIn: null,
isLoading: true,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(me.pending, (state) => {
state.isLoading = true
})
.addCase(me.fulfilled, (state, action) => {
state.user = action.payload.userData
state.isUserLoggedIn = true
state.isLoading = false
})
.addCase(me.rejected, (state) => {
state.isLoading = false
state.isUserLoggedIn = false
})
},
})
export const me = createAsyncThunk('auth/me', async (_, thunkAPI) => { // <- {} replaced to _ and this is it!
try {
const user = await userService.getUserData()
return { userData: user.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
thunkAPI.rejectWithValue(message)
return message
}
})
I built a custom hook to get a user from firebase:
export function useAuth() {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return currentUser;
}
In my components I use it as:
const currentUser = useAuth();
useEffect(() => {
const localScore = JSON.parse(localStorage.getItem("score"));
setPush(!(localScore[currentUser.name] === tasks.length));
// ^ TypeError: Cannot read properties of undefined (reading 'name')
}, []);
I think I get the error, because when the components gets rendered the first time, there is no currentUser. But I load the component after the user logged in (what sets the user), thus I am confused.
I think I need to await the user somehow, but how?
From the comments:
I moved my onAuthStateChanged to a context, but this is checking the login state only once and is not updating:
const AppContext = createContext(null);
export function AppWrapper({ children }) {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return (
<AppContext.Provider value={currentUser}>{children}</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
function MyApp({ Component, pageProps }) {
const currentUser = useAppContext();
return (
<>
<AppWrapper>
{Component.secure && !currentUser ? (
<Login />
) : (
<>
<Component {...pageProps} />
</>
)}
</AppWrapper>
</>
);
}
I would like to rewrite this life cycle method into a hook but it does'nt work as expected.
when the componentdidmounted, if the user id exists in the local storage,the user is connected and his name is displayed in the navbar. And when he disconnects and reconnects his name is displayed in the navbar.
So i am trying to convert this class Component with hooks, when the username changes nothing is displayed in the navbar so i have to refresh the page and that way his name is displayed
The real problem is the componentDidUpdate
how can i get and compare the prevProps with hooks
The class Component
const mapStateToProps = state => ({
...state.authReducer
}
);
const mapDispatchToProps = {
userSetId,
userProfilFetch,
userLogout
};
class App extends React.Component {
componentDidMount() {
const userId = window.localStorage.getItem("userId");
const {userSetId} = this.props;
if (userId) {
userSetId(userId)
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
const {userId, userProfilFetch, userData} = this.props; //from redux store
if(prevProps.userId !== userId && userId !== null && userData === null){
userProfilFetch(userId);
}
}
render() {
return (
<div>
<Router>
<Routes/>
</Router>
</div>
);
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);
With hooks
const App = (props) => {
const dispatch = useDispatch();
const userData = useSelector(state => state.authReducer[props.userData]);
const userId = window.localStorage.getItem("userId");
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfilFetch(userId))
}
}, [userData, userId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
export default App;
How to get the previous props or state?
Basically create a custom hook to cache a value:
const usePrevious = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Usage:
const App = (props) => {
const dispatch = useDispatch();
const userData = useSelector(state => state.authReducer[props.userData]);
const userId = window.localStorage.getItem("userId");
// get previous id and cache current id
const prevUserId = usePrevious(userId);
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfileFetch(userId))
}
// do comparison with previous and current id value
if (prevUserId !== userId) {
dispatch(userProfileFetch(userId));
}
}, [userData, userId, prevUserId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
FYI: You may want to refactor the code a bit to do the fetch from local storage in an effect hook that runs only on mount. If I understand your app flow correctly it would look something like this:
const App = (props) => {
const dispatch = useDispatch();
const { userId } = useSelector(state => state.authReducer[props.userData]);
useEffect(() => {
const userId = window.localStorage.getItem("userId");
userId && dispatch(userSetId(userId));
}, []);
// get previous id and cache current id
const prevUserId = usePrevious(userId);
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfileFetch(userId))
}
// do comparison with previous and current id value
if (prevUserId !== userId) {
dispatch(userProfileFetch(userId));
}
}, [userId, prevUserId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
now i resolve it, i made this
const App = props => {
const userId = window.localStorage.getItem("userId");
const dispatch = useDispatch();
const userData = useSelector(state=> state.authReducer[props.userData]);
const isAuthenticated = useSelector(state=> state.authReducer.isAuthenticated);
useEffect(()=> {
if(userId){
dispatch(userSetId(userId))
dispatch(userProfilFetch(userId))
}
}, [userId])
return(
<div>
<Router>
<Routes/>
</Router>
</div>
)
};