Redux middleware is half working but not fully cancelling action - javascript

I have some simple middleware that is kind of working but also not working
basically I have a list of users and I am trying to delete one. and then sync it up with firebase. all is well.
I'm adding some middleware so that when a user deletes one it asks if you are sure? (just using a simple alert for now). if you click cancel, it doesn't delete. if you click ok it does
so far, that is working but because of my action creators it still carrying on and deleting the user. here is some code:
// click to delete user
<button
onClick={() =>
this.props.deleteUserFromStoreThenUpdateFirebase(user)
}
>
calls this method
I think something funky is going on here, basically it shouldn't call the deletedUserFromFirebase method if I hit cancel
export const deleteUserFromStoreThenUpdateFirebase = user => {
return (dispatch, getState) => {
return dispatch(deleteUser(user)).then(() => {
return deleteUserFromFirebase(user);
})
};
};
export const deleteUser = user => {
return async dispatch => {
dispatch({ type: DELETE_USER, user: user, reqConfirm: true });
};
};
middleware:
const confirmMiddleware = store => next => action => {
if(action.reqConfirm){
if(confirm('are you sure?')){
next(action)
}
}
else {
next(action)
}
}

I also think that confirmation actions should be [only] role of the UI.
Redux store can (should?) be treated like API - request (action) > response (changed state). Are you sending request to an API and waiting to confirmation message? It would be at least strange.
But I see even more than some sense in this idea. Centralised confirmation element can have a similiar role to toasts/snackbar messages.
Problem #1: There is a strict separation between UI and store logic - you can't use middleware to show dialog. But you can change state which can used in UI to show confirmation dialog.
Problem #2: There is no simple if/then/return chain of actions, you have to buffer an action and run it after confirmation (receiving 'confirm' action) or drop it on cancel. Yes, it can be done as middleware.
IDEA:
Action dispatched as requiring confirmation is saved in buffer. You can then change action type to CONFIRM_SHOW - state will be changed, props passed, modal dialog can be shown.
On CONFIRM_OK run buffered action, next steps will be common with CONFIRM_CANCEL: clear buffer and hide modal. It could be even one value - buffer can be a modal flag (empty - hide, defined - show) dervied in mapStateToProps.
To be fully usable there should be an option to pass custom confirmation message along with reqConfirm.

Related

How to work with multiple tabs using react-idle-timer

I am having a react Application, where I need to display a modal with two buttons (Logout and Continue Session) when the user goes idle, It was actually working fine with the IdleTimer component from the react-idle-timer.
But If I open the Application in multiple tabs, and click on Continue session in one tab, the other tabs are not receiving that, The sample code that I have used is,
<IdleTimer
timeout={1000}
onIdle={// Opens a modal with two buttons Logout and Continue Session}
></IdleTimer>
Is it possible to use the crossTab prop? but I am not sure how to implement them.
How can I achieve my requirement to work with multiple tabs so that if I click on continue session all the tabs need to close the modal.
The version I am using is - react-idle-timer (4.6.4)
Could someone please help with this to achieve my requirement? Thanks in advance!!
You can use the Cross Tab Messaging feature from the documentation:
// Action dispatcher (redux)
const dispatch = useDispatch()
// Message handler
const onMessage = data => {
switch (data.action) {
case 'LOGOUT_USER':
dispatch(logoutAction())
break
// More actions
default:
// no op
}
}
// IdleTimer instance
const { message } = useIdleTimer({ onMessage })
// Logout button click
const onLogoutClick = () => {
// Tell all tabs to log the user out.
// Passing true as a second parameter will
// also emit the event in this tab.
message({ action: 'LOGOUT_USER' }, true)
}
See the documentation for v5, here.

How to close MatDialogBox or any div when API is successful in NGXS state?

I have started learning state management using NGXS. So far everything is fine but have few questions regarding some scenarios like -
If a Mat Dialog box is open (or any div - here I've both the scenarios in my project) and from inside it an API is called, how can I close the dialog only if API returns success?
Suppose a user logs out, how can I reset the states to default values?
For the first case below is my code for the state, action & dispatcher:
abc.action.ts
export class AddExamCategory {
static readonly type = '[ExamCategory] Add';
constructor(public payload: ExamCategory) {}
}
abc.state.ts
export interface ExamCategoryStateModel {
examCategoryList: ExamCategory[];
}
#State<ExamCategoryStateModel>({
name: 'examCategory',
defaults: {
examCategoryList: []
}
})
#Injectable()
export class ExamCategoryState {
constructor(private _adminService: AdminService) {}
#Action(AddExamCategory)
addExamCategory({ getState, patchState }: StateContext<ExamCategoryStateModel>, { payload }: AddExamCategory) {
return this._adminService.createExamCategory(payload).pipe(tap(response => {
patchState({ examCategoryList: [] }); // Want to close the modal/div after this part. If API returns ERROR do not close.
}));
}
}
abc.component.ts
this.store.dispatch(new AddAdminUser({ ...this.adminRegistrationForm.value, password: this.password.value }))
.subscribe(response => {
this.store.dispatch(new GetAdminUsers());
this.dialogRef.close(true)
});
Currently it's like this but it closes no matter what's the status of API.
For the second case, in the service where I have written the logic for logout() I have written like this: this.store.reset({}). Though it's resetting the state but not with the default values in it. I have multiple states to reset on this single logout method.
How to work on these scenarios?
You can add extra property on your state to track the requesting state of your application ('requesting','idle') [you can create extra states as needed to track 'success' and 'error' response from the server]
when dispatch GetAdminUsers set the value of the newely added state to requesting and at GetAdminUsersComplete set the value to idle
subscribe to a selector that's read the state on your ngOnInit and call dialogRef.clse(true) inside of it. like following:
this.store
.pipe(
select(selectors.selectRequestState),
skip(1) //only start tracking after request created
)
.subscribe(result => {
if (result == 'idle')
this.dialogRef.close()
});
example: https://stackblitz.com/edit/angular-ivy-4ofc3q?file=src/app/app.component.html
Reset State
I don't think there is a simple way to reset the state with the store. You need to move through all your state features and implement reset action that set the state to initial state.
the simplest solution is to refresh the browser page after user logout using location.reload();
if you keep the store inside localstorage you need to remove it first then do the reload
the easiest way to accomplish that is with an effect, after you dispatch your action you should be able to trigger other action like "UsserAddedSuccessfully" and read it after that close that modal.
read this question for more detail.

React JS Redux load data after login

i'm using react js and redux and did an authentication with jwt.
I have a navbar with a dropdown. This dropdown is only visible for authenticated users. The dropdown items are dynamicly and should be loaded from a server after an user logged in.
But i have no idea where i should trigger the api call to load the dropdown items.
When i click on "login" an authentication action will be triggerd. In this action i do the authentication and return a boolean to a reducer.
Then the state "authenticated" switched to "true" and he dropdown will be shown.
Now the dropdown items should be displayed to the user.
But when i should load them?
My thinking was to put the "load dropdown items" logic into a reducer so i can wait for the "authenticated" action. But the logic should be implemented in the action, should it?
Do you have any ideas what i can do?
In redux once you have authenticated the user, you can use a thunk to trigger another api call to fetch the information for the user and dispatch it corresponding action for it.
You can read more about the need and use thunks here
export function loginUser() {
return async (dispatch, getState) => {
let response = await login(credentials)
//action creator to update boolean that user is logged in
dispatch(loginSuccessful(response)) ;
//action creator to trigger api to fetch drop down data
dispatch(loadDropdownItems(response));
}
}
I think you should handle this from backend when user enters his credentials. If the user is authenticated send 'true' response with dropdown items of authenticated users.
From frontend user can handle this in action using redux thunk. Redux thunk is a middleware that helps you to return function.
In componentDidMount, call this.props.actions.authAsync()
function authAction() {
return {
type: 'AUTH_SUCCESS'
};
}
function authAsync() {
return dispatch => {
// api call
// based on response show dropdown items
if(response.data.authenticated){
// return dropdown items from response
}
dispatch(authAction());
};
}

ReactJS: How to handle component state when it's based on AJAX?

Summary: New to ReactJS and I'm trying to figure out the best way to update a component when it's state depends on a remote API (i.e. keep component state in sync with remote database via AJAX API).
Example Use Case: Think of a product inventory where clicking a button adds a product to your cart and decrements the inventory by 1. Every time the user clicks it initiates an AJAX request and then upon completion of the request, the component re-renders with the new product inventory by calling setState().
Problem: I've ran into an issue where because both setState() and the AJAX request are asynchronous, the component becomes out of the sync with the server. For example if you click really quickly you can initiate more than one AJAX request for a single product ID because the component's state has not yet updated to reflect that the product ID is no longer in inventory. I have a simple example below to illustrate the concept:
Inadequate Solution: This could be handled on the server side by sending an error back if the client request a product that is no longer in inventory, however I'm really looking for the best way to handle this common scenario in ReactJS on the client side and to make sure I'm understanding the best way to handle component state.
Component extends React.Component {
constructor(props) {
super(props);
this.state = {
clicksLeft: 0,
};
}
componentDidMount() {
//getClicksLeft is async and takes a callback, think axios/superagent
getClicksLeft((response) => {
this.setState(response);
});
}
btnClicked = () => {
//This may appear redundant/useless but
//imagine sending an element in a list and then requesting the updated
//list back
const data = {clicks: this.state.clicksLeft--};
decrementClicksLeft(data, () => {
getClicksLeft((response) => {
this.setState(response);
});
}
}
render() {
<button onClick={this.btnClicked}>Click me {this.state.clicksLeft} times</button>
}
}
Is there any reason to have to call getClicksLeft when the button is clicked? You have already called it when the component is mounted and then anytime the button is clicked you just decrement that number by one.
btnClicked = () => {
if (this.state.clicksLeft > 0) {
decrementClicksLeft();
this.setState({clicksLeft: this.state.clicksLeft - 1});
}
}
This would work if there is only one user trying to buy stuff at a time. Otherwise you could also check the amount left before making the purchase.
btnClicked = () => {
getClicksLeft((response) => {
if (response > 0) {
decrementClicksLeft();
this.setState({clicksLeft: this.state.clicksLeft - 1});
}
});
}
This way if there are no clicks left, nothing happens.
The most basic solution would be to disable the button while you wait for the response to come back:
(I've also made your code simpler.)
Component extends React.Component {
constructor(props) {
super(props);
// Initial state
this.state = {
clicksLeft: 0, // No clicks are availabe
makeRequest: false, // We are not asking to make a request to the server
pendingTransaction: false, // There is no current request out to the server
};
}
componentDidMount() {
// Initial load completed, so go get the number of clicks
this._getClicksRemaining();
}
// Called whenever props/state change
// NOT called for the initial render
componentWillUpdate(nextProps, nextState) {
// If there is no existing request out to the server, AND if the next
// state is asking us to make a request (as set in _handleButtonClick)
// then go make the request
if (!this.state.pendingTransaction && nextState.makeRequest) {
const data = {
clicks: this.state.clicksLeft--,
};
// decrementClicksLeft is async
decrementClicksLeft(data, () => this._getClicksRemaining());
// First fire off the async decrementClicksLeft request above, then
// tell the component that there is a pending request out, and that it
// is not allowed to try and make new requests
// NOTE this is the one part of the code that is vulnerable to your
// initial problem, where in theory a user could click the button
// again before this setState completes. However, if your user is able
// to do that, then they are probably using a script and you shouldn't
// worry about them. setState/render is very fast, so this should be
// more than enough protection against human clicking
this.setState({
makeRequest: false,
pendingTransaction: true,
});
}
}
_getClicksRemaining() {
// getClicksLeft is async
getClicksLeft((response) => {
// Here we are inside of the callback from getClicksLeft, so we
// know that it has completed. So, reset our flags to show that
// there is no request still pending
const newState = Object.assign(
{
pendingTransaction: false,
},
response,
);
this.setState(newState);
});
}
// The button was clicked
_handleButtonClick = () => {
if (!this.state.pendingTransaction) {
// If there isn't a request out to the server currently, it's safe to
// make a new one. Setting state here will cause `componentWillUpdate`
// to get called
this.setState({
makeRequest: true,
});
}
}
render() {
// Disable the button if:
// * there are no clicks left
// * there is a pending request out to the server
const buttonDisabled = ((this.state.clicksLeft === 0) || this.state.pendingTransaction);
return (
<button
disabled={buttonDisabled}
onClick={this._handleButtonClick}
>
Click me {this.state.clicksLeft} times
</button>
);
}
}
After spending some time with react-redux, redux-thunk and redux-pack I decided to go with something simpler: react-refetch. I didn't really need the complexities of redux as I am only doing post and get operations on lists. I also need some simple side effects like when I do a post, I need to update multiple lists (which is achieved through andThen() in react-refetch).
This solution has much less boiler plate and works great for small projects. The core reason to choose this project over react-redux can be summarized in this quote from heroku's blog entry:
Looking around for alternatives, Redux was the Flux-like library du jour, and it did seem very promising. We loved how the React Redux bindings used pure functions to select state from the store and higher-order functions to inject and bind that state and actions into otherwise stateless components. We started to move down the path of standardizing on Redux, but there was something that felt wrong about loading and reducing data into the global store only to select it back out again. This pattern makes a lot of sense when an application is actually maintaining client-side state that needs to be shared between components or cached in the browser, but when components are just loading data from a server and rendering it, it can be overkill.
1: https://github.com/heroku/react-refetch
2: https://engineering.heroku.com/blogs/2015-12-16-react-refetch/

React confirm modal and redux middleware

I'm new to React and Redux also. I want to remove item from the list so I dispatch an action deleteSelectedItems then I use redux middleware to catch it and show confirm. That looks like below:
Action:
export const deleteSelectedItems = () => {
return {
type: ActionTypes.ITEM.DELETE_SELECTED,
payload: {
confirm: {
message: 'Are you sure you want to delete these selected items?'
}
}
}
};
Middleware:
const confirmMiddleware = store => next => action => {
if (action.payload.confirm) {
if (confirm(action.payload.confirm.message)) {
next(action);
}
} else {
next(action);
}
};
Everything works well. Now, I don't want to use confirm() to show confirm dialog, I want to use my own ConfirmDialog component instead.
I found #Dan Abramov solution, that is great. But I am confused how to integrate those together. I want to use confirmMiddleware to dispatch an action that show modal but I don't know how to handle when user click ok or cancel on modal. How can I do that?
I managed to independently re-invent the modal management technique that Dan describes in that issue, and then pushed it a bit farther. I did a writeup of my approach at Implement a confirm modal using React & Redux.Quoting myself:
I have a central component that is responsible for displaying all currently open dialogs, in the proper layout order (ie, I can have "DialogB" on top of "DialogA", etc). The component that wants to trigger showing the dialog runs an action creator that dispatches a "SHOW_DIALOG" action, with the name of the dialog component in the payload, and arbitrary additional data attached to the action. That data is added to the store, and the dialog managing component will pass that extra data to the dialog component as props when it's rendered.
I've created some generic "picker" dialogs, like ColorPicker and IconPicker. These dialogs know nothing about the rest of my app. They can take some optional bits of data in their props (such as the initially selected color value), and are also looking for a special prop with a name like "onColorSelected". The component that requested the dialog can include the entirety of another action as part of the payload, and when the dialog has its "OK" button clicked on, that new action will be dispatched along with the "return value" of the dialog.
So, in general, my suggestion is to include a plain action object that gets passed along to the dialog, and the dialog can dispatch a copy of the action when it is closed.
Redux middleware isn't really the right place for UI, it only really works in your existing implementation because window.confirm has magical powers and can stop the thread of execution.
Instead I would recommend dispatching a separate action to open the confirm dialog, e.g. CONFIRM_DELETE_ITEMS which toggles a flag to indicate the dialog should be displayed then dispatch the DELETE_ITEMS action when the dialog confirm button has been clicked.
e.g.
function Root({ dispatch, confirmDeleteItems }) {
return (
<div>
{confirmDeleteItems ? (
<ConfirmDialog onConfirm={() => dispatch(deleteItems())} onDeny={() = dispatch(hideModal())} />
) : null}
<button onClick={() => dispatch(confirmDeleteItems())}>Delete</button>
</div>
)
}

Categories