How do I load firebase data into react-redux asynchronously? - reactjs

I am currently trying to load my product data into redux, but so far I cant seem to pass the product information returned from firestore into the reducer.
Index.js -> load first 10 products from firestore soon after store was created.
store.dispatch(getAllProducts)
action/index.js
import shop from '../api/shop'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS
products
})
const getAllProducts = () => dispatch => {
shop.getProducts(products => {
dispatch(receiveProducts)
})
}
shop.js
import fetchProducts from './firebase/fetchProducts'
export default {
getProducts: (cb) => cb(fetchProducts())
}
fetchProducts.js
const fetchProducts = async() => {
const ProductList = await firebase_product.firestore()
.collection('store_products').limit(10)
ProductList.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
}).catch(function (error) {
console.log('Error getting Documents: ', error)
})
}
In product reducers
const byId = (state={}, action) => {
case RECEIVE_PRODUCTS:
console.log(action); <- this should be products, but it is now promise due to aysnc function return?
}
I can get the documents with no issues (tempDocs gets the first 10 documents without any issue.) but I am not able to pass the data back into my redux. If I were creating normal react app, I would add a loading state when retrieving the documents from firestore, do I need to do something similar in redux as well ?
Sorry if the code seems messy at the moment.

fetchProducts is an async function so you need to wait for its result before calling dispatch. There are a few ways you could do this, you could give fetchProducts access to dispatch via a hook or passing dispatch to fetchProducts directly.
I don't quite understand the purpose of shop.js but you also could await fetchProducts and then pass the result of that into dispatch.

A generalized routine I use to accomplish exactly this:
const ListenGenerator = (sliceName, tableName, filterArray) => {
return () => {
//returns a listener function
try {
const unsubscribe = ListenCollectionGroupQuery(
tableName,
filterArray,
(listenResults) => {
store.dispatch(
genericReduxAction(sliceName, tableName, listenResults)
);
},
(err) => {
console.log(
err + ` ListenGenerator listener ${sliceName} ${tableName} err`
);
}
);
//The unsubscribe function to be returned includes clearing
// Redux entry
const unsubscriber = () => {
//effectively a closure
unsubscribe();
store.dispatch(genericReduxAction(sliceName, tableName, null));
};
return unsubscriber;
} catch (err) {
console.log(
`failed:ListenGenerator ${sliceName} ${tableName} err: ${err}`
);
}
};
};
The ListenCollectionGroupQuery does what it sounds like; it takes a tableName, an array of filter/.where() conditions, and data/err callbacks.
The genericReduxAction pretty much just concatenates the sliceName and TableName to create an action type (my reducers de-construct action types similarly). The point is you can put the dispatch into the datacallback.
Beyond this, you simply treat Redux as Redux - subscribe, get, etc just as if the data were completely local.

Related

How to chain multiple dispatch actions one after the other in redux?

Im trying to chain multiple dispatch actions one for the other, by this order:
1.updateCart(dispatch,cartProducts ,loggedUser) - with redux-thunk.
after finishing fetching data, im dispatching actions by order:
2.dispatch(logoutReset()))
3.dispatch(logoutSuccess()))
4.then a refresh - window.location.reload())
but doesnt wait for dispatch to finish first before moving on so it keeps messing up.
i have tried many ways, with await or promises but didn't succeed.
i would like to learn from you guys, how to do it properly.
Component Navbar:
const handleLogout = async() => {
try{
await updateCart(dispatch,cartProducts ,loggedUser)
.then( ()=> dispatch(logoutReset()))
.then( ()=> dispatch(logoutSuccess()))
.then( ()=> window.location.reload());
}catch(err){
console.log(err)
}
};
Actions (updateCart) redux-thunk:
export const updateCart = async (dispatch, selectedProduct, loggedUser) => {
dispatch({ type: ActionTypes.UPDATE_CART });
try {
await userRequest.put(`carts/` + loggedUser.id, {
userId: loggedUser.id,
products: selectedProduct
})
} catch (error) {
console.log(error.response)
dispatch(returnErrors(error.response.data, error.response.status));
}
};
I have found a solution, posing if anyone will need it some day.
If you want to do something after dispatch finish, just put useEffect and the parameter that is updated after the dispatch, so after the dispatch and update you can do what you need to do.
very simple approach
const handleAddClick = () => {
dispatch(addToCart({selectedProduct:item,quantity}))
}
so after dispatch is finished cartProducts is updated.
useEffect(() => {
updateCart(dispatch,cartProducts ,loggedUser)
}, [cartProducts])

React and Redux toolkit - reject after promise

I'm working on a React Native app. I have a signup screen which has a button, onclick:
const handleClick = (country: string, number: string): void => {
dispatch(registerUser({ country, number }))
.then(function (response) {
console.log("here", response);
navigation.navigate(AuthRoutes.Confirm);
})
.catch(function (e) {
console.log('rejected', e);
});
};
The registerUser function:
export const registerUser = createAsyncThunk(
'user/register',
async ({ country, number }: loginDataType, { rejectWithValue }) => {
try {
const response = await bdzApi.post('/register', { country, number });
return response.data;
} catch (err) {
console.log(err);
return rejectWithValue(err.message);
}
},
);
I have one of my extraReducers that is indeed called, proving that it's effectively rejected.
.addCase(registerUser.rejected, (state, {meta,payload,error }) => {
state.loginState = 'denied';
console.log(`nope : ${JSON.stringify(payload)}`);
})
But the signup component gets processed normally, logging "here" and navigating to the Confirm screen. Why is that?
A thunk created with createAsyncThunk will always resolve but if you want to catch it in the function that dispatches the thunk you have to use unwrapResults.
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
import { unwrapResult } from '#reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(rejectedValueOrSerializedError => {})
}

Unsubscribe to firestore(...).onSnapshot() does not work when dispatch() is involved

Main goal: unsubscribe correctly all firestore-listeners before logging out the user, preventing leaks.
Libraries involved: react, react-native, redux, redux-thunk and react-native-firebase.
Problem: Unsubscribe to firestore(...).onSnapshot() does not work when dispatch() is involved.
I fetch data with onSnapshot and returns the unsubscribe function to the caller component which I call on user logout. Strangely, UNSUBSCRIBE only works when no dispath is made...
I have a component (component.js) that is connected to redux store and fetch constantly some user data like this:
componentDidMount() {
this.unsubscribe = this.props.userFetch(); // userFetch is an action creator in actions.js
}
In actions.js
import firestore from '#react-native-firebase/firestore';
import auth from '#react-native-firebase/auth';
export const userFetch = () => {
return dispatch => {
const unsubscribe = firestore()
.doc(`users/${auth().currentUser.uid}`)
.onSnapshot({
error: e => console.warn('ERROR IN FETCH: ', e),
next: SnapshotUser => {
console.log('User: ', SnapshotUser.data());
// Will dispatch action below
},
});
return unsubscribe;
};
};
Note that there is no DISPATCH for the moment in the previous action creator.
If I call unsubscribe in component.js, the firestore onSnapshot listener gets unsubscribed correctly, like this:
onLogoutPressed = () => {
this.unsubscribe(); // <-- HERE it works (for the moment...)
auth()
.signOut()
.then(() => {
console.log('user has been signout');
})
.catch(error => {
console.log('Error: ',error);
});
};
Now if I want to send my fetched data to the redux store with a dispatch, I add the dispatch like this in actions.js
export const userFetch = () => {
return dispatch => {
const unsubscribe = firestore()
.doc(`users/${auth().currentUser.uid}`)
.onSnapshot({
error: e => console.warn('ERROR IN FETCH: ', e),
next: SnapshotUser => {
console.log('User: ', SnapshotUser.data());
// Will dispatch action below
dispatch({ // <--------------------------------- HERE
type: 'USER_FETCH_SUCCESS',
payload: SnapshotUser.data(),
});
},
});
return unsubscribe;
};
};
But then suddenly in my component.js, the this.unsubscribe doesn't work anymore on logout.
I've found that guy doing the same but works for him on React: here.
The solution provided by this other guy is basically the same too.
It looks like the firestore-onsnapshot-listener is wrapped in some dispatch call due to redux-thunk and I cant understand how it behaves now.
Does someone has any solution?
Ok solved it with the help of #ioss on Reactiflux.
The componentDidMount was mounted twice for some weird reasons, creating multiple listeners thus unmounting one was not enough.
Solved it by adding another run on unsubscribe() in componentWillUnmount().

Async/await redux thunk not returning promise to action correctly

I have a thunk using Axios that's posting to an Express route using Sequelize.
The route is posting correctly (ie. data is getting added to the db) but the action inside of the React component isn't behaving as expected. Using async/await, I expect the action to wait until it completes the db post before continuing but that's not the case here. I'm getting undefined from the action.
The thunk hits the express route where I'm dispatching the action to update my redux store and returning the response:
const addedNewList = (newList) => ({type: ADD_NEW_LIST, newList})
export const addNewList = (name, userId) => async dispatch => {
try {
const { data } = await axios.post('/api/list/add', { name, userId })
dispatch(addedNewList(data))
return data
} catch (err) {
console.error(err)
}
}
Using debugger, I can confirm that return data is in fact returning the response from the server that I need. I can also confirm that the redux store is getting updated correctly.
But here, when I try and access that response data as result, I get undefined:
handleSubmit = async () => {
const result = await this.props.addNewList(this.state.name, this.props.userId)
// ** result is 'undefined' **
this.handleClose()
// pass off the results
}
If I add a setTimeout after I evoke the addNewList action, it works as expected. This suggests to me that maybe it's not returning a promise? But my understanding was that if you returned the response from the server in the thunk, it would do that.
For completeness, here is my route which I've also confirmed with debugger that data is being passed as expected:
const userAuth = function(req, res, next) {
if (req.isAuthenticated()) {
return next()
}
res.status(401).send('Unauthorized user')
}
router.post('/add', userAuth, async (req, res, next) => {
const { name, userId } = req.body
try {
const list = await List.create({ name, userId })
res.json(list)
} catch(err) { next(err) }
})
Why is the action returning undefined in the handleSubmit method?
Try returning the dispatch of addedNewList(data) instead:
export const addNewList = (name, userId) => async dispatch => {
try {
const { data } = await axios.post('/api/list/add', { name, userId })
return Promise.resolve(dispatch(addedNewList(data)));
} catch (err) {
console.error(err)
}
}
That being said, you could consider restructuring the component to instead utilize mapStateToProps to use values/result from the updated Redux store rather than explicitly awaiting the response and manually passing the value?
The response from Alexander got me on the right track so I'm sharing my solution in case it helps someone (as he suggested).
While I could have continued to try and solve this by wrapping the dispatch in a Promise, the better solution was to rethink how the component was structured.
In my situation, I wanted to get the ID for the newly created row in the database so that I could pass it into history.push.
handleSubmit = async () => {
const result = await this.props.addNewList(this.state.name, this.props.userId)
this.handleClose()
history.push(`/list/${result.id}`)
}
With result coming back undefined, the url was not updating correctly.
The better solution was to access the new data from the redux store where it was updated. This way I could be certain the history wouldn't get updated until the data was ready.
So my updated component now looked something like this where the history wouldn't update until a newId was available:
handleSubmit = () => {
this.props.addNewList(this.state.name, this.props.userId)
this.handleClose()
}
render(){
const { newId } = this.props
if (newId) {
history.push(`/list/${newId}`)
}
return (
....
)
}
}
const mapStateToProps = (state) => {
return {
newId: state.list.newId
}
}
Instead of putting this into render, I could probably also use a component lifecylcle method like componentWillReceiveProps or similar.

Using redux-saga and redial Server Side

redux-saga
redial
Right now, I am trying to get the initial state of my application server side through Redial.
Redial triggers a pure object action, and redux-saga listens/awaits for that action, and then launches the async request.
But the problem is, Redial has no promises to resolve to when redux-saga is completed because it is dispatching a pure object.
Component
const redial = {
fetch: ({ dispatch }) => dispatch({ type: actionTypes.FETCH_START }),
};
export default class PostList extends Component {
render() {
const { posts } = this.props;
return (
<div>
{posts.map(post => <ListItem key={post.id} post={post} />)}
</div>
);
}
}
PostList.propTypes = {
posts: PropTypes.array.isRequired,
};
export default provideHooks(redial)(connect(mapStateToProps)(PostList));
Saga
export function *fetch() {
try {
yield put({ type: actionTypes.FETCH_START });
const response = yield call(fakeData);
yield put({ type: actionTypes.FETCH_SUCCESS, data: response.data });
yield put({ type: actionTypes.FETCH_PENDING });
} catch (e) {
yield put({ type: actionTypes.FETCH_FAIL });
}
}
export default function *loadPost() {
yield * takeLatest(actionTypes.FETCH_START, fetch);
}
export default function *rootSaga() {
yield [
fork(loadPost),
];
}
Is there a way to connect redial to redux-saga ?
I think it can be done in this way:
firstly, you need to add store in locals. (codes are taken from redial README)
const locals = {
path: renderProps.location.pathname,
query: renderProps.location.query,
params: renderProps.params,
// Allow lifecycle hooks to dispatch Redux actions:
dispatch,
store
};
Then you can create a Promise manually like this:
const redial = {
fetch: ({ store, dispatch }) => {
return new Promise((resolve, reject) => {
const unsubscribe = store.subscribe(()=>{
if (store.getState()...) { // monitor store state changing by your saga
resolve(...) //you probably dont need any result since your container can read them from store directly
unsubscribe();
}
if (store.getState()....error) {
reject(...)
unsubscribe();
}
});
dispatch({ type: actionTypes.FETCH_START }),
}
}
};
Those codes are just for demonstration, don't use them in production without proper testing.
I think there might be a more elegant way to monitor saga execution results than checking redux store state over and over until the state matches those if(...) statements, maybe you can run saga with redux store and external listeners, then those redial hooks wont need to know about your store structure.
There is a rather elegant way of doing this. First of all you need to create a registry for your saga tasks (remember that running the middleware's .run method returns a task descriptor):
export default class SagaTaskRegistry {
constructor() {
this._taskPromises = [];
}
addTask(task) {
if (!this._taskPromises) {
this._taskPromises = [];
}
this._taskPromises.push(task.done);
}
getPromise() {
return new Promise((resolve) => {
const promises = this._taskPromises;
if (!promises) {
resolve();
return;
}
this._taskPromises = undefined;
Promise.all(promises).then(resolve).catch(resolve);
}).then(() => {
const promises = this._taskPromises;
if (promises) {
return this.getPromise();
}
return undefined;
});
}
}
When you add new tasks to the saga middleware using .run, you will then call registryInstance.add(taskDescriptor). The SagaTaskRegistry will grab the promise for that task and add it to an array.
By calling getPromise, you will receive a promise which will resolve when all added tasks are finished. It will never be rejected, as you most likely wouldn't want failed fetches to result in a rejection - you still want to render your application with the error state.
And this is how you can combine it with redial:
import createSagaMiddleware from 'redux-saga';
import { applyMiddleware, createStore } from 'redux';
import rootReducer from 'your/root/reducer';
import yourSaga from 'your/saga';
const sagaMiddleware = createSagaMiddleware();
const middleWare = [sagaMiddleware];
const createStoreWithMiddleware = applyMiddleware(...middleWare)(createStore);
const store = createStoreWithMiddleware(rootReducer);
const sagaTaskRegistry = new SagaTaskRegistry();
const sagaTask = sagaMiddleware.run(yourSaga);
sagaTaskRegistry.addTask(sagaTask);
match({ routes, history }, (error, redirectLocation, renderProps) => {
const locals = {
path: renderProps.location.pathname,
query: renderProps.location.query,
params: renderProps.params,
dispatch: store.dispatch,
};
trigger('fetch', components, locals);
// Dispatching `END` will force watcher-sagas to terminate,
// which is required for the task promises to resolve.
// Without this the server would never render anything.
// import this from the `redux-saga` package
store.dispatch(END);
// The `SagaTaskRegistry` keeps track of the promises we have to resolve
// before we can render
sagaTaskRegistry.getPromise().then(...)
});
A component can now be decorated with a simple hook:
const hooks = {
fetch: ({ dispatch }) => {
dispatch(yourAction());
},
};
From here on out you can just use sagas as usual. This should give you the ability to do what you are trying. You can further abstract this to allow for dynamic registration of sagas across code-split chunks and other things. The task registry already works for these use-cases by checking for newly registered tasks since the last call to getPromise before actually resolving the promise.

Resources