How to use firebase authentication with Redux Toolkit using onAuthStateChanged? - reactjs

I'm trying to implement Firebase Authentication via Redux Toolkit. But I think I'm missing something due to lack of knowledge.
My monitorAuthChange returns undefined.
I have two separate files - first list of firebase functions, second Redux Toolkit slice.
import {
createUserWithEmailAndPassword,
onAuthStateChanged,
} from "firebase/auth";
import { auth } from "./firebaseConfig";
export const createAccount = async (email, password) => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (error) {
console.log(error);
}
};
export const monitorAuthChange = () => {
onAuthStateChanged(auth, (user) => {
if (user) {
return true;
} else {
return false;
}
});
};
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { createAccount, monitorAuthChange } from "../../service/userServices";
export const createUser = createAsyncThunk(
"users/createUser",
async ({ username, password }) => {
await createAccount(username, password);
}
);
const initialState = { loginStatus: false };
const userSlice = createSlice({
name: "users",
initialState,
reducers: {},
extraReducers: {
[createUser.fulfilled]: (state, action) => {
const result = monitorAuthChange();
state.loginStatus = result;
},
[createUser.rejected]: (state, action) => {
state.loginStatus = false;
},
},
});
export const selectAllUsers = (state) => state.users;
export default userSlice.reducer;
Two things make me confused:
Thunk works - it creates account and I see it in Firebase. Do I need to track result of request in a different way?
If add console.log(user) inside monitorAuthChange it logs data depends if user was created or not. But still returns undefined.
Would appreciate any hint or advice or article to read to understand my mistake. Thanks in advance.

It seems you want to track user auth with onAuthStateChanged
You have plenty way to plug this callback to redux.
You cannot call monitorAuthChange inside the reducer as they must be pure.
Using global store
// users.slice.ts
const userSlice = createSlice({
name: "users",
initialState,
reducers: {
setLoginStatus: (state, action) {
state.loginStatus = action.payload;
}
},
extraReducers: {
[createUser.fulfilled]: (state, action) => {
state.loginStatus = true;
},
[createUser.rejected]: (state, action) => {
state.loginStatus = false;
},
},
});
// trackUserAuth.ts
onAuthStateChanged(auth, (user) => {
if (user) {
store.dispatch(setLoginStatus(true))
} else {
store.dispatch(setLoginStatus(true))
}
});
Using hooks
export const useAuth = () => {
const dispatch = useDispatch();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
dispatch(setLoginStatus(true))
} else {
dispatch(setLoginStatus(true))
}
});
return unsubscribe;
}, []);
}
Using thunks
export const checkAuthStatus = () => (dispatch) {
const unsubscribe = Firebase.auth().onAuthStateChanged(user => {
if (user) {
dispatch(setLoginStatus(true))
} else {
dispatch(setLoginStatus(true))
}
});
return unsubscribe;
}

Related

Redux Saga and Toolkit with Firebase - How to get the data back?

I am trying to implement Redux Toolkit and Redux Saga with Firebase. Currently I am stuck with the problem of fetching the data from the Firestore. I have configured the store and everything that is needed for the Redux Saga and Toolkit, now the only problem I have is when I try to fetch the data from the Firestore. I have made also the helper functions which can and already did help me, but just without the Redux involved.
This is what I mean by this:
store.js
import createSagaMiddleware from "redux-saga";
import { configureStore } from "#reduxjs/toolkit";
import allQuestionsReducer from "./allQuestionsState";
import allQuestionsSaga from "./redux-saga/allQuestionsSaga";
const saga = createSagaMiddleware();
const store = configureStore({
reducer: {
allQuestions: allQuestionsReducer,
},
middleware: [saga],
});
saga.run(allQuestionsSaga);
export { store };
AllQuestionsState.js
import { createSlice } from "#reduxjs/toolkit";
export const allQuestionSlice = createSlice({
name: "allQuestions",
initialState: {
allQuestions: [],
isLoading: false,
error: null,
},
reducers: {
getAllQuestionsFetch: (state, action) => {
state.isLoading = true;
},
getAllQuestionsSuccess: (state, action) => {
state.allQuestions = action.payload;
state.isLoading = false;
},
getAllQuestionsError: (state, action) => {
state.error = action.payload;
state.isLoading = false;
},
},
});
export const {
getAllQuestionsFetch,
getAllQuestionsSuccess,
getAllQuestionsError,
} = allQuestionSlice.actions;
export default allQuestionSlice.reducer;
AllQuestionsSaga.js
import { call, put, takeEvery } from "redux-saga/effects";
import { getQuestions } from "../../utils/firebase-functions/firebase-functions";
import { getAllQuestionsSuccess } from "../allQuestionsState";
function* workGetAllQuestions() {
const response = yield new Promise(getQuestions);
yield put(getAllQuestionsSuccess(response));
}
function* allQuestionsSaga() {
yield takeEvery("allQuestions/getAllQuestionsFetch", workGetAllQuestions);
}
export default allQuestionsSaga;
Helper Function
export const getQuestions = async (formData) => {
let error;
try {
const success = await onSnapshot(collection(firebaseDatabase, "questions"));
return success.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
} catch (error) {
error = error.message;
return error;
}
};
Where the action is dispatched
const dispatch = useDispatch();
const data = useSelector(
(state) => state.allQuestions.allQuestions
);
useEffect(() => {
dispatch(getAllQuestionsFetch());
}, []);
console.log(data);
Does anyone has any clue on how to fix this. I am getting the [] even though I have data in Firestore.

Passing multiple params in Redux Toolkit's createAsyncThunk

I have the following code where I want to send POST request in with a data object and a history function. How can I pass the history param in the action creator? Thanks
login action creator
import { createAsyncThunk } from '#reduxjs/toolkit';
import { client } from '../client';
import {User} from './userLoginSlice';
export const userLogin = createAsyncThunk('user/userLogin' , async (data: User, history: string, thunkAPI) => {
try {
const res = await client.post<User>('/User/Credentials/Login', data);
if(res.status === 200) history.push('/dashboard');
return res.data;
} catch (error) {
return thunkAPI.rejectWithValue('Sorry! Something went wrong ):') ;
}
});
login slice
import { createSlice } from '#reduxjs/toolkit';
import { userLogin } from './userLoginThunk';
export interface User{
email: string;
passowrd: string;
rememberMe: boolean;
}
const initialState = {
user: {} as User,
loading: false,
error: ''
};
export const userLoginSlice = createSlice({
name: 'user',
initialState: initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(userLogin.pending, (state) => {
state.loading = true;
state.error = '';
});
builder.addCase(userLogin.fulfilled, (state, { payload }) => {
state.loading = false;
state.user = payload;
});
builder.addCase(userLogin.rejected, (state, { payload }) => {
state.loading = false;
state.error = String(payload);
});
}
});
export const userLoginReducer = userLoginSlice.reducer;
Using the action creator in a component
import { Form, Input, Checkbox } from 'antd';
import { useHistory } from 'react-router-dom';
import { ConfirmBtn } from '../small-components/ActionBtns';
import { useAppDispatch } from '../../custom-hooks/reduxCustomHooks';
import {userLogin } from 'src/redux/user-login/userLoginThunk';
import {User} from '../../redux/user-login/userLoginSlice';
import '../../sass/light-theme/user-login.scss';
export const UserLogin = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const onFinish = (values: User) => {
dispatch(userLogin(values, history));
};
return (
<Form></Form>
);
};
It's throwing the error which says that the userLogin expects 1 arguments but got 2. What am I doing wrong?
A thunk only accepts one parameter, so you would have to provide an object containing both your values and the history object.
In your case, you could also use the the unwrap() function (https://stackoverflow.com/a/67876542/3170628) so you don't have to provide the history object to your thunk:
const onFinish = async (values: User) => {
try {
await dispatch(userLogin(values)).unwrap();
history.push('/dashboard');
} catch (error) {
...
}
};

onAuthStateChanged return undefined

i can log value of onAuthStateChanged but when i return it, extraReducers seem did not get it.
function check state of auth
import { createAsyncThunk, createSlice} from "#reduxjs/toolkit";
import {
onAuthStateChanged,
signOut
} from "firebase/auth";
export const checkUserSignIn = createAsyncThunk(
"auth/checkUSerSignIn",
async () => {
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
console.log(user)
return true
} else {
return false
}
});
}
);
where i get return value
const authSlice = createSlice({
name: "auth",
initialState: {
auth: {
isLoading: false,
isAuthenticate: false,
user: null,
},
},
reducers: {},
extraReducers: (builder) => {
//Check User SignIn
builder
.addCase(checkUserSignIn.pending, (state, action) => {
state.auth.isLoading = true;
console.log(`CheckUserSignIn Pending: ${action.payload}`);
})
.addCase(checkUserSignIn.fulfilled, (state, action) => {
state.auth.isLoading = false;
action.payload
? (state.auth.isAuthenticate = true)
: (state.auth.isAuthenticate = false);
console.log(`CheckUserSignIn Fulfilled: ${action.payload}`);
})
.addCase(checkUserSignIn.rejected, (state, action) => {
console.log(`CheckUserSignIn Rejected: ${action.error.message}`);
});
},
});
action.payload of fulfilled case always return undefined. how can i fix it?
have a nice day, everyone!
onAuthStateChanged is an asynchronous call, but it doesn't return a promise itself. Even if it did, you're not returning anything from the top-level code in the checkUserSignIn function.
This is probably closer to what you need/want:
export const checkUserSignIn = createAsyncThunk(
"auth/checkUSerSignIn",
async () => {
return new Promise((resolve, reject) {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, (user) => {
unsubscribe();
if (user) {
resolve(true);
} else {
resolve(false);
}
});
});
}
);

React-redux- Fetching data from API

I am working on a project and I need to fetch data from backend or from an API. I tried fetch the data but nothing appears. I think I am doing something wrong in the container. I am a beginner in react-redux, I don't know what I am doing wrong.
I've already read all the posts but nothing seems to works.
my reducer:
const initialState={
articles: [],
};
const rootReducer = (state = initialState, action) => {
const { type, payload }=action;
switch(type) {
case SRETRIEVE_ARTICLE:{
return {
...state,
articles:payload,
};
}
default: return state;
}
}
export default rootReducer;
This is what I have right now in container:
import Articles from 'components/Articles';
import { fetchArticles } from '../../pages/index';
const mapStateToProps = (state) => ({
articles:state.articles
})
const ConnectedArticles = connect(
mapStateToProps,
{fetchArticles}
)(Articles)
export default ConnectedArticles;
pages.js
axios.get('API').then((response) => {
const { data } = response;
dispatch({ type: RETRIEVE_ARTICLES, payload: data });
});
};
const Index = () => {
const articles= useSelector((state) => state.articles);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchArticles);
}, []);
return <>{articles && articles.map((article) => <Article key={article.id} name={article.name} />)}</>;
};
Index.getInitialProps = async () => ({
authRequired: true,
label: 'Dashboard',
});
export default Index;
Also I defined the action type: export const SET_UNOPENED_REWARD = 'SET_UNOPENED_REWARD';
and action const unopenedRewards = (payload) => ({ type: SET_UNOPENED_REWARD, payload });
One very nice way to do data fetching with redux is to use redux toolkit's createAsyncThunk and createSlice functions.
// src/features/articles/articlesSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const fetchArticles = createAsyncThunk("articles/get", async () => {
// Here you can use axios with your own api
const response = await fetch("https://rickandmortyapi.com/api/character");
const json = await response.json();
return json.results;
});
export const slice = createSlice({
name: "articles",
initialState: {
loading: false,
data: []
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticles.pending, (state) => {
state.loading = true;
});
builder.addCase(fetchArticles.fulfilled, (state, action) => {
state.data = action.payload;
state.loading = false;
});
builder.addCase(fetchArticles.rejected, (state) => {
state.loading = false;
});
}
});
export default slice.reducer;
// src/features/articles/Articles.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchArticles } from "./articlesSlice";
export const Articles = () => {
const articles = useSelector((state) => state.articles.data);
const loading = useSelector((state) => state.articles.loading);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchArticles());
}, []);
return (
<>
{loading && "...loading"}
{articles.map((article) => <Article key={article.id} {...article} />)}
</>
);
};
you should use async and await
let response = await axios.get('https://run.mocky.io/v3/5c045896-3d18-4c71-a4e5-5ed32fbbe2de')
if(response.status==200){
dispatch({ type: RETRIEVE_ARTICLES, payload: data });
}

Adding function in Slice for redux

Please help me how I can introduce new function like getOrdersByCustomer in ordersSlice. I have provided full code of ordersSlice below. Please tell me what is extraReducers and how it works.
import { createSlice, createAsyncThunk, createEntityAdapter } from '#reduxjs/toolkit';
import axios from 'axios';
export const getOrders = createAsyncThunk('eCommerceApp/orders/getOrders', async () => {
const response = await axios.get('/api/e-commerce-app/orders');
const data = await response.data;
return data;
});
export const removeOrders = createAsyncThunk(
'eCommerceApp/orders/removeOrders',
async (orderIds, { dispatch, getState }) => {
await axios.post('/api/e-commerce-app/remove-orders', { orderIds });
return orderIds;
}
);
const ordersAdapter = createEntityAdapter({});
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
const ordersSlice = createSlice({
name: 'eCommerceApp/orders',
initialState: ordersAdapter.getInitialState({
searchText: ''
}),
reducers: {
setOrdersSearchText: {
reducer: (state, action) => {
state.searchText = action.payload;
},
prepare: event => ({ payload: event.target.value || '' })
}
},
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload)
}
});
export const { setOrdersSearchText } = ordersSlice.actions;
export default ordersSlice.reducer;
In Addition
Also can you please tell me what I will do with this following code for my custom function getOrdersByCustomer.
export const { selectAll: selectOrders, selectById: selectOrderById } = ordersAdapter.getSelectors(
state => state.eCommerceApp.orders
);
because, in my component I have used like
const orders = useSelector(selectOrders);
You can introduce new (async) functions as you already have (I used the customerId as part of the url -> you could access it through the params in your backend):
export const getOrdersByCustomer = createAsyncThunk('eCommerceApp/orders/getOrdersByCustomer', async (customerId) => {
const response = await axios.get(`/api/e-commerce-app/orders/${customerId}`);
const data = await response.data;
return data;
});
Then you can handle the response in your extraReducer:
extraReducers: {
[getOrders.fulfilled]: ordersAdapter.setAll,
[removeOrders.fulfilled]: (state, action) => ordersAdapter.removeMany(state, action.payload),
[getOrdersByCustomer.fulfilled]: (state, action) =>
// set your state to action.payload
}
The extraReducers handle actions like async thunks. The createAsyncThunk function return 3 possible states (along with other things): pending, rejected or fulfilled. In your case you only handle the fulfilled response. You could also set your state with the other two options (in your case [getOrdersByCustomer.pending] or [getOrdersByCustomer.rejected]

Resources