I am learning Redux global state store and I created a simple app, when click the "Select" button, the selected item will be added to the SavedCourses list. Then I want to display the SavedCourses list. But right now my code does not display the list. Is there something wrong with my reducer? My sandbox link is at https://ky801.csb.app/#/
It's actually your mapDispatchToProps function which is the problem. addCourse is supposed to take an argument which is the class_nbr. But your this.props.addCourse function ignores its arguments and just calls dispatch(addCourse()).
const mapDispatchToProps = (dispatch) => {
return {
addCourse: () => dispatch(addCourse())
};
};
Since you aren't actually passing the course info, your savedCourses is ending up like [undefined, undefined, undefined].
You could change your mapDispatchToProps so that it takes an argument:
const mapDispatchToProps = (dispatch) => {
return {
addCourse: (class_nbr) => dispatch(addCourse(class_nbr))
};
};
But when all you are doing is dispatching the function with the same arguments then it is not necessary to define mapDispatchToProps as a function. You can just use an object of action creators:
export default connect(mapStateToProps, {addCourse})(Courses);
Redux has gotten a lot easier nowadays with the react-redux hooks and redux-toolkit. You can simplify your code a lot using those.
Related
Using React-Redux
I have a select list that when the user chooses one of the options, a item is created and placed in the database (if it matters, the reason its a select box is that there are multiple variations of the same item and what variation is important).
This is working correctly.
My problem is that I am not sure how I can get the id of the new item out of the redux store.
And just for chuckles, the prior developer set all this up with sagas. So I am still coming up to speed on how it all works together.
So when the select box is checked, the function checkFunction is called that calls the function createItem in the saga file. These functions are below:
in Repositories.jsx
checkFunction = (data) => {
const {createItem} = this.props;
// data holds the info that we need to send to the action
const created = createItem(data);
// once data comes back, redirect the user ot the details of the created item
// need id of created item
// navigateTo(`/item-details/${created.id}`);
}
in Repositories.saga.js
export function* createItem(action) {
try {
const {payload: newItem} = action;
// call api to create item
const created = yield call(requestAPI.post, ITEMS_URL, newItem);
// send message that its been done
yield put(actions.repositories.createItem.ok(created));
// send message to refresh item list
yield put(actions.inventories.fetchItems.start());
} catch (e) {
yield put(actions.repositories.createItem.fail(e));
}
}
I don't understand how to return the id of the created item once its created. I feel like I am missing something basic here. Can anyone point me in the right direction?
Actually getting data from saga back to react component is not trivial. There are multiple approaches to do what you need although each has its downside.
1. Call navigateTo in the saga.
export function* createItem(action) {
...
const created = yield call(requestAPI.post, ITEMS_URL, newItem);
yield call(navigateTo, `/item-details/${created.id}`)
}
This would be my recommended solution if you can get the navigateTo function into the saga. Navigation is a side effect and sagas are there to deal with side effects. Make sure to use the call effect, changing the url by directly calling the function can lead to some issues.
2. Store the latest created item id in redux store
In your reducer, when action actions.repositories.createItem.ok(created) is dispatched, store the created item info and then send the latest created item to the component. Finally you can use componentDidUpdate or useEffect to call navigateTo when the prop changes.
render() {
const created = Redux.useSelector(state => state.created);
useEffect(() => navigateTo(`/item-details/${created.id}`), [created])
...
}
This has the disadvantage that the component will rerender becuase of the changed created value.
3. Send callback in the createItem action
You can put a function into your action and then call it from the saga and essentially using it as a callback.
Component:
checkFunction = (data) => {
const {createItem} = this.props;
// data holds the info that we need to send to the action
const created = createItem(data, (created) => {
navigateTo(`/item-details/${created.id}`);
});
}
Saga:
export function* createItem(action) {
...
const created = yield call(requestAPI.post, ITEMS_URL, newItem);
action.callback(created)
...
}
The problem with this approach is that functions are not serializable and so you ideally should avoid them in your actions. Also, technically, there could be multiple sagas handling the same action and then it gets kind of confusing who should call the callback.
When Formik runs validation, all my fields are validated.
One of my operations is really expensive (DB lookup).
I'd like to not run this validation if it hasn't changed. How can I achieve this?
I'm using withFormik Higher order component.
One option I considered was to track the current value and the last validated value. If different, then run the validation. Maybe that could be const [lastValidated, setLastValidated] = useState(); in the underlying component. But I'm not sure how to access state on my underlying comopnent.
const Guest = (props) => {
// maybe lastValidated here?
};
const formikEnhancer = withFormik({
validate: (values, props) =>
new Promise((resolve, reject) => {
const errors = {};
// Runs validation on all fields
}
});
export default formikEnhancer(Guest);
You could consider using field-level validation in this case. It could give you more flexibility in checking for changes in this specific input so you can bail before the expensive operation is run.
When using next-redux-wrapper how do I start a long asynchronous task so that it only runs on the client? I don’t want to use await on the server side since it would delay the initial page load. I’d rather set a loading flag as the task starts and show a loading indicator until it completes.
Let’s say my async operation looks like this:
function async takesLong(store) {
store.dispatch({type: “LOADING”, payload: true});
const result = await longOperation();
store.dispatch({type: “SETDATA”}, payload: data);
return store.dispatch({type: “LOADING”, payload: false});
}
I can call this in my Next page’s getInitialProps function like this:
MyPage.getInitialProps = async ({ store, isServer }) => {
const loader = takesLong(store)
if (isServer) await loader; // <-- will delay client response
return {
someprop: "some value"
};
};
This works well if the page loads on the client side. The operation starts, and my page can display a loading-spinner until the operation completes. But when started on the server side I have a long delay before the page displays at all. I’ve tried a number of approaches but can’t find one that works properly:
Starting the process on the server and not using await renders the page without the results being written to the store, so it has only set “loading” to true in the store and it never gets the data.
Passing store in my props doesn’t work - it ends up being an empty object ({ }) in the client.
Trying to run it inside my component doesn’t seem to work for a few reasons:
a) I don’t have the store object accessible in the React Component (only in getInitialProps which won’t get called on the client).
b) Even if I use actions instead of store.dispatch in the Component, when can I call it safely? I can’t do it during render since it will change the Redux state, and componentWillReceiveProps won’t get called before the first client-side render
Is there a well defined pattern for deferring a long operation to the client-side when using Next?
Do your long async task on componentDidMount, it will run only on client side.
React in SSR not runs componentDidMount lifecycle hook.
Using bound actions during componentDidMount works. Thanks to #eenagy for the suggestion. Doing things in this order seems to do what is needed:
import { bindActionCreators } from "redux";
import { setLoading, setError, setData } from "../actions";
componentDidMount() {
if (!this.props.haveData && !this.props.loading && !this.props.error) {
this.props.setLoading(true);
loadSomeData() // <-- this takes a while to complete
.then( data => {
this.props.setData(data);
this.props.setLoading(false);
})
.catch( err => {
this.props.setError(err);
this.props.setLoading(false);
});
}
}
render() {
if (this.props.loading) return (<Loading/>);
return (/*regular page*/);
}
export const mapDispatchToProps = dispatch => {
return bindActionCreators({ setLoading, setError, setData }, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(Component);
This way if the initial data is not already loaded (say by another page) it will get kicked off when the
component mounts and run asynchronously until the operation completes and
calls the actions in redux to cause the page to re-render.
I am writing action creator in react app. where in when i do some api call i need to show the Progress Loader on screen. So, my action creator looks like this.
export const fetchData = (actionType, param) => (dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
return fetchDataRequest(actionType, param) // Here is Fetch APi Call
.then(responseData => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER));
dispatch(Action(recd(actionType), { data: responseData, receivedAt: Date.now() }));
}).catch((error) => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
};
When i write this piece of code its working as expected, i am dispatching the action as dispatch(fetchData(data)) from component and i am able to show the loader in my Parent Component. What i understand is fetch is returning me the promise. Once the fetch gets completed then i am hiding the loader which is working as expected.
Now, There is scenario where in i need to do some validation where in i don't have to make any api call but all the validation are performed locally.
Here also i want to do the same thing like i need to show loader in my parent component as well when all the validation are done i need to hide the loader.
I have written the same piece of code even actions are getting called but my render function is not getting called.
My Code Looks like:
// This my action creator which will actually do the validation
export const validateAndSaveData = () => {
return ((dispatch, getState) => {
return new Promise((resolve, reject) => {
let saveRecommendDetailsFlag = true;
// here i am dispacthing some action and storing data in my store
saveRecommendDetailsFlag = canSaveData(getState());
if (saveRecommendDetailsFlag) {
resolve('SUCCESS');
} else {
reject('ERROR');
}
});
});
};
And there is one more action creator which i am calling it from from UI Component which will first initiate the show loader action and then perform validation and based on the result of validation i have to hide the loader.
export const saveData = () => {
return ((dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
return dispatch(validateAndSaveData())
.then(() => {
// Here i m dispatching an action to do some more processing.
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
})
.catch(() => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
});
};
Everything is working fine but my loader are not coming on the screen. i am not able to figure it out where am i doing wrong.
Can anyone suggest something how can i solve this issue?
I got some workaround using setTimeout func but i don't think that is right approach.
export const saveData = () => {
return ((dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
setTimeout(()=>return dispatch(validateAndSaveData())
.then(() => {
// Here i m dispatching an action to do some more processing.
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
})
.catch(() => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
},10);
});
};
Your code looks reasonable, my suspicion is that your validateAndSaveData promise finishes so quickly that there is no visible loader on the screen.
In that case, a timeout is totally reasonable. However, in order to do it properly, I would keep a state on if the loading screen is visible + if it's been shown long enough. You can then remove the loading screen once it is both up for long enough, and the actual event expires.
I'm not sure which action package you're using, so I can't post exact code, but the pseudocode would look something like this:
const delay = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds));
let loadingCounter = 0;
const showLoadingScreen = () => (dispatch) => {
const counter = loadingCounter;
loadingCounter++;
delay(5).then(() => {
if (getStore().loadingScreen.counter === counter) {
dispatch(Action(ActionConstants.PROGRESS_LOADER_DELAY_ELAPSED))
}
})
return dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER, counter))
}
Basically, you would keep track of 3 pieces of state for the loader:
{
counter: 0,
taskCompleted: false,
canHide: false,
}
Counter is saved so that you can disambiguate what happens if you get SHOW_PROGRESS_LOADER while an existing SHOW_PROGRESS_LOADER is in progress.
taskCompleted keeps a record of whether the thing you're waiting on is done, and canHide keeps track if the loader has been visible on the screen long enough.
When you dispatch PROGRESS_LOADER_DELAY_ELAPSED it sets canHide to true, and when you dispatch HIDE_PROGRESS_LOADER it sets taskCompleted to true. (although you may want to rename the latter action). When both canHide and taskCompleted are set to true, only then can the loader go away.
This is a pretty common UI pattern - Try to complete a task quickly. If it takes more than a short amount of time, then throw up a loading dialog. However, the loading dialog is guaranteed to stay up a minimum amount of time to prevent flickering. So the more advanced version of this kind of pattern would be to add another state which doesn't show the progress loader at all unless the call takes more than Y milliseconds.
Hope this makes sense, leave a comment if not :-)
I have an SPA that is loading some global/shared data (let's call this APP_LOAD_OK) and page-specific data (DASHBOARD_LOAD_OK) from the server. I want to show a loading animation until both APP_LOAD_OK and DASHBOARD_LOAD_OK are dispatched.
Now I have a problem with expressing this in RxJS. What I need is to trigger an action after each DASHBOARD_LOAD_OK, as long as there had been at least one APP_LOAD_OK. Something like this:
action$
.ofType(DASHBOARD_LOAD_OK)
.waitUntil(action$.ofType(APP_LOAD_OK).first())
.mapTo(...)
Does anybody know, how I can express it in valid RxJS?
You can use withLatestFrom since it will wait until both sources emit at least once before emitting. If you use the DASHBOARD_LOAD_OK as the primary source:
action$.ofType(DASHBOARD_LOAD_OK)
.withLatestFrom(action$.ofType(APP_LOAD_OK) /*Optionally*/.take(1))
.mapTo(/*...*/);
This allows you to keep emitting in the case that DASHBOARD_LOAD_OK fires more than once.
I wanted to avoid implementing a new operator, because I thought my RxJS knowledge was not good enough for that, but it turned out to be easier than I thought. I am keeping this open in case somebody has a nicer solution. Below you can find the code.
Observable.prototype.waitUntil = function(trigger) {
const source = this;
let buffer = [];
let completed = false;
return Observable.create(observer => {
trigger.subscribe(
undefined,
undefined,
() => {
buffer.forEach(data => observer.next(data));
buffer = undefined;
completed = true;
});
source.subscribe(
data => {
if (completed) {
observer.next(data);
} else {
buffer.push(data);
}
},
observer.error.bind(observer),
observer.complete.bind(observer)
);
});
};
If you want to receive every DASHBOARD_LOAD_OK after the first APP_LOAD_OK You can simply use skipUntil:
action$ .ofType(DASHBOARD_LOAD_OK)
.skipUntil(action$.ofType(APP_LOAD_OK).Take(1))
.mapTo(...)
This would only start emitting DASHBOARD_LOAD_OK actions after the first APP_LOAD_OK, all actions before are ignored.