Add Loading Indicator for React-Redux app with Promise Middleware - reactjs

I am new to react redux. I want to add a loading text when the user pressed the search button and dismiss the text when data comes back or the action completed.
In a normal async function, I can just toggle the isLoading flag before and after the call back.
However, in my app, I am dispatching an action that returns a 'type' and 'payload' that is a promise. The middleware redux-promise then 'automatically' converts that promise payload and send it to the reducer.
In other words, the middleware does the .then action for the promise behind the scene and gives my reducer the correct data.
In this case, I am not sure how I can add a loading text to my view because as soon as I call this.props.getIdByName(this.state.value), I do not know when the data comes back.
The logical place for me would be inside the reducer since that is when the data comes back. However, I belive that would be a bad way because reducers should not perform such task?
Inside my component, I have the following function for my submit
handleSubmit(e) {
e.preventDefault();
this.props.getIdByName(this.state.value);
}
Inside my actions/index.js file, I have the following action generator
export function getIdByName(name) {
const FIELD = '/characters'
let encodedName = encodeURIComponent(name.trim());
let searchUrl = ROOT_URL + FIELD + '?ts=' + TS + '&apikey=' + PUBLIC_KEY + '&hash=' + HASH + '&name=' + encodedName;
const request = axios.get(searchUrl)
return {
type: GET_ID_BY_NAME,
payload: request
}
}
Inside my reducers/reducers.jsx
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case GET_ID_BY_NAME:
console.log(action.payload.data.data.results[0].id); <-- here the data comes back correctly because reduer called the promise and gave back the data for me
return {...state, id: action.payload.data.data.results[0].id};
default:
return state;
}
}
And in my main index.js file, I have the store created with the following middleware
const createStoreWithMiddleware = applyMiddleware(
promise,
thunk
)(createStore);

When you dispatch an action with a Promise as its payload while using redux-promise, the redux-promise middleware will catch the action, wait until the Promise resolves or rejects, and then redispatch the action, now with the result of the Promise as its payload. This means that you still get to handle your action, and also that as soon as you handle it you know it's done. So you're right in that the reducer is the right place to handle this.
However, you are also right in that reducers should not have side-effects. They should only build new state. The answer, then, is to make it possible to show and hide the loading indicator by changing your app's state in Redux :)
We can add a flag called isLoading to our Redux store:
const reducers = {
isLoading(state = false, action) {
switch (action.type) {
case 'START_LOADING':
return true;
case 'GET_ID_BY_NAME':
return false;
default:
return state;
}
},
// ... add the reducers for the rest of your state here
}
export default combineReducers(reducers);
Whenever you are going to call getIdByName, dispatch an action with type 'START_LOADING' before calling it. When getIdByName gets redispatched by redux-promise, you'll know the loading is done and the isLoading flag will be set back to false.
Now we have a flag that indicates whether we are loading data in our Redux store. Using that flag, you can conditionally render your loading indicator. :)

I am sure Pedro's answer should get you started but I recently did the very exact loader/pre-loader in my app.
The simplest way to do it is.
Create a new key in the state object in one of your reducers and call it showLoader: false.
In the same container as before(the one with the button), create a mapStateToProps and get that state property showLoader.
Inside the container that holds the button you are trying to trigger the loader with, add an onClick event which calls an action, say displayLoader. In the same function also set the this.props.showLoader to true
Create a displayLoader action write something like this:
function displayLoader() {
return {
type: DISPLAY_LOADER,
payload: {}
}
}
In the reducers, catch this action and set the showLoader back to false when your desired payload is received.
Hope it helps!

Related

Redux dispatch in useEffect causes numerous redux errors

Edit - Fixed The problem was absolute garbage redux code as it was my first time using it. I studied Redux and rewrote my code and it works fine. It wasn't working because there was way too much garbage code working at once.
I'm using nextJS and when visiting a shared URL such as /username/p/postID I want to display the initial post modal/popover.
async function presentInitialPost(postID: string) {
const postSnap = await db.collection("posts").doc(postID).get();
const postData = postSnap.data();
if(postData){
dispatch(showModalPostView(postData as Post));
}
}
useEffect(() => {
if(initialPostID){
presentInitialPost(initialPostID)
}
}, [initialPostID])
Errors (there ae numerous of each):
"You may not unsubscribe from a store listener while the reducer is executing."
"You may not call store.getState() while the reducer is executing."
I use the same dispatch throughout my app just fine - and if I call presentInitialPost on a button click instead - it works completely fine.
I've tried delays and debugging where the error is coming from but I haven't figured it out at all, any help is appreciated, thank you.
Redux code:
showModalPostView(state, action) {
state.showModalPostViewFunction(action.payload);
},
setShowModalPostViewFunction(state, action) {
state.showModalPostViewFunction = action.payload;
return state;
},
showModalPostViewFunction: (post: Post) => {},
showModalPostViewFunction comes from my overlay wrapper component
showModalPostView = (post: Post) => {
console.log(this.state)
this.setState({
showModalPostView: true,
modalPostViewPost: post,
});
};
When the modal post view is shown here, it contains multiple useSelectors which cause the errors - but only when i present the modal post view in useEffect - it works just fine throughout the app when dispatched through a click action.
In modal post view various selectors throw the errors:
const likedPosts = useSelector((state: ReduxRootState) => state.likedPosts);
It appears you are not using redux the way it was intended to be used. What you pass to dispatch() is supposed to be an action. A reducer receives that action and returns new state, and then then new state is sent to your subscriber (the React component).
You are instead calling a function showModalPostView which is calling some setState function in the parent component and is returning who knows what. That return value is being passed as an argument dispatch, which kicks off a reducer. However, that setState is likely causing your child component to unsubscribe from the store so it can re-render.
It like you aren't actually dispatching an action; you're just calling a function, so you shouldn't be using dispatch at all.
async function presentInitialPost(postID: string) {
const postSnap = await db.collection("posts").doc(postID).get();
const postData = postSnap.data();
if(postData){
showModalPostView(postData as Post);
}
}

how action contacted the reducer to change the state of the object

I am sorry it might be a silly question but i want to know i have redux store first let me explain the data flow there
The data is getting store at a global level state which i have in my store.js which have been declare in my productReducer.js There i define a switch statement and there i alter the state of the product
productReducer.js
my code here
import {
PRODUCT_LIST_SUCCESS,
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_FAIL,
CLEAR_ERROR
} from '../constants/productConst'
export const productReducer = (state ={product:[]},action)=>{
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return{
laoding: true,
product:[],
}
case PRODUCT_LIST_SUCCESS:
return{
laoding: false,
product:action.payload.products,
productsCount:action.payload.productCount,
}
case PRODUCT_LIST_FAIL:
return{
laoding: false,
error:action.payload,
}
case CLEAR_ERROR:
return{
...state,
error:null
}
default:
return {
...state,
}
}
}
i have action productAction.js
import axios from 'axios'
import {
PRODUCT_LIST_SUCCESS,
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_FAIL,
CLEAR_ERROR
} from '../constants/productConst'
export const getProduct = () => async (dispatch) =>{
console.log("Been executed at the action")
try {
dispatch({type:PRODUCT_LIST_REQUEST})
const {data} = await axios.get("api/v1/products")
dispatch({
type:PRODUCT_LIST_SUCCESS,
payload:data,
})
} catch (error) {
dispatch({
type:PRODUCT_LIST_FAIL,
payload:error.response.data.message,
})
}
}
export const clearError =() =>async (dispatch)=>{
dispatch({type:CLEAR_ERROR})
}
Let me Sum up my question when i neeed to update the state from the frontend i call the action but there is no way the action and the reducer connected together how the state of the product is altered in my case
To answer your question "how do actions contact the reducer to change the state of the object?":
The overall redux store setup enables this, especially how you registered your productReducer.
Let's go through the typical flow to illustrate how everything is connected:
Somewhere in a React component, a user-interaction (e.g. button click) or something automatic dispatches the async getProduct() action. This is possible because getProduct is either a prop of the component (Redux' connect API) or you're using the useDispatch hook.
The store setup knows that PRODUCT_LIST_SUCCESS is handled by your productReducer. We go through the switch statement and now state.product.product holds an array of products (careful with the naming there btw, plural vs. singular).
Any React component interested in state.product.product is now notified that the state has changed. They're getting notified because either the connect API (mapStateToProps) or useSelector connects the state with the mounted React component. The products can now be (re-)rendered, or clicked on, etc..
Action
An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application.
Reducer
You can think of a reducer as an event listener which handles events based on the received action (event) type.
By using dispatch() you are dispatching the event and then comes the following in the reducer logic:
Check to see if the reducer cares about this action
If so, make a copy of the state, update the copy with new values, and return it
Otherwise, return the existing state unchanged
If you are interested about more then please have a look into official redux documentation, there is really everything you will need.

Redux saga - error binding - appropriate way to bind error to local state

We have a ReactJS app that uses redux-saga.
In redux-saga we perform all the data grabbing (http webrequests).
Moreover: We use React hooks.
Scenario:
We have a button that dispatches an action to redux. Redux-saga handles this action and fires a webrequest.
Now, this webrequest fails (for instance server is down).
What we want to do now is to change the text of the button.
So in fact, we want to get back to the "scope" where the action has been dispatched.
Question:
What is the appropriate way to get back from the redux-saga to the button ?
I have found this:
https://github.com/ricardocanelas/redux-saga-promise-example
Is this appropriate to use with hooks in year 2021 ?
That example you posted sounds needlessly convoluted. Error handling with redux-sagas can be relatively straightforward. We'll consider 3 parts of the application - the UI, the store, and the actions/saga chain.
The UI:
const SomeUIThing = () => {
const callError = useSelector(state => state.somewhere.error);
const dispatch = useDispatch();
return (
<button onClick={
dispatch({
type: "API_CALL_REQUEST"
})
}>
{callError ? 'There was an error' : 'Click Me!'}
</button>
)
}
You can see that the UI is reading from the store. When clicked, it will dispatch and API_CALL_REQUEST action to the store. If there is an error logged in the store, it will conditionally render the button of the text, which is what it sounds like you wanted.
The Store
You'll need some actions and reducers to be able to create or clear an error in the store. So the initial store might look like this:
const initialState = {
somewhere: {
error: undefined,
data: undefined
}
}
function reducer(state = initialState, action){
switch(action.type){
case "API_CALL_SUCCESS":
return {
...state,
somewhere: {
...state.somewhere,
data: action.payload
}
}
case "API_CALL_FAILURE":
return {
...state,
somewhere: {
...state.somewhere,
error: action.payload
}
}
case "CLEAR":
return {
...state,
somewhere: initialState.somewhere
}
default:
return state;
}
}
Now your reducer and your store are equipped to handle some basic api call responses, for both failures and successes. Now you let the sagas handle the api call logic flow
Sagas
function* handleApiCall(){
try {
// Make your api call:
const response = yield call(fetch, "your/api/route");
// If it succeeds, put the response data in the store
yield put({ type: "API_CALL_SUCCESS", payload: response });
} catch (e) {
// If there are any failures, put them in the store as an error
yield put({ type: "API_CALL_ERROR", payload: e })
}
}
function* watchHandleApiCall(){
yield takeEvery("API_CALL_REQUEST", handleApiCall)
}
This last section is where the api call is handled. When you click the button in the UI, watchHandleApiCall listens for the API_CALL_REQUEST that is dispatched from the button click. It then fires the handleApiCall saga, which makes the call. If the call succeeds, a success action is fired off the to store. If it fails, or if there are any errors, an error action is fired off to the store. The UI can then read any error values from the store and react accordingly.
So as you see, with a try/catch block, handling errors within sagas can be pretty straightforward, so long as you've set up your store to hold errors.

How to set the data in Redux when I am fetching something?

Good evening,
I am making weather app with react and redux. I have my reducer with a object of my data info with empty string etc:
const initState = {
city: '',
temp: '',
wind: ''
}
And for example in React I am gonna fetch all the data and what should i do now?
I should have dispatch action (FILL DATA - for example) and then i should make a [useState] inside my component for temporary place for my data. Then fill the data in my temporary place and then useDispatch to update redux store?
Actually, there are a few ways to do this, such as:
Use a redux middleware like redux-saga or redux-thunk, which I recommend.
In the request action, make the asynchronous call and then dispatch the success action to update the state data.
Call the API from the component and call the state redux store update action to update the global state. NOT RECOMMENDED.
Apparently, you're trying to use the third way, but it's not recommended because it beats the purpose of abstract redux and making the data scattered all around. A bad practice.
A middleware example would be very long, so I'll try to explain the second way briefly.
In your request action, do something like this:
...
axios.get(url).then(res => {
dispatch({ type: 'REQUEST_SUCCESS', data: res.data });
});
In your reducer:
...
switch (action.type) {
case: 'REQUEST_SUCCESS';
return { ...state, ...action.data };
}
There is a library called redux-thunk that will help you with this. It allows actions to be asynchronous, so you can actually dispatch the action, fetch all the data INSIDE the action and then dispatch an action to fill your state. After configuring it, your action would look something like this:
const getWeatherData = () => { // Declare it like any other action
return async (dispatch) => { //Then return a promise
const response = await getWeatherAPI(); // fetch from the backend
return dispatch({ // And then dispatch another action to populate your reducer
type: "FILL_DATA",
payload: response
})
}
}
This action would be dispatched from your code just like any other action, but considering it returns a promise you can actually await for it to finish and handle that any way you want.

ReactJS / Redux call to dispatch action in other reducer

How to dispatch action in other reducer.
For example i create reducer LogsReducer with switch case ADD_LOG and REMOVE_LOG. Code:
const addLog = (text) => ({type: 'ADD_LOG', text});
const removeLog = (id) => ({type: 'REMOVE_LOG', id});
const logsReducer = (state = {}, action) => {
switch (action.type) {
case 'ADD_LOG':
// todo
case 'REMOVE_LOG':
// todo
default:
return state;
}
};
And i add logsReducer to combineReducers.
To dispatch action i call this inside mapDispatchToProps.
dispatch(addLog('sample log'));
dispatch(removeLog(2);
And here question. How to allow to dispatch message from outside of logs reducer. For example from fetch response in other reducer eg. contactReducer?
How to allow to dispatch message from outside of logs reducer. For example from fetch response in other reducer eg. contactReducer?
Usually presence of asynchronous API calls or action dispatching queue meant that presentation/domain logic is consists of process, so process-manager is needed for resolve that task.
General solution is redux-saga, which combines middleware and live process manager. With using saga, components just notify about logical happened action, and then saga performs API calls, produce new actions and even do Optimistic updates.
Also, with redux+saga approach your web-application automatically become full-stack Event-Sourced application: by writing front-end code you will get also isomorphic back-end code (If use some tool like that https://github.com/reimagined/resolve )
See practices with using saga Infinite loop with redux-saga and how to setstate after saga async request

Resources