Realize a confirm component with react, redux and promise - reactjs

I try to build a generic confirm component with redux and native promise. I read Dan Abramovs solution here: How can I display a modal dialog in Redux that performs asynchronous actions? but i am looking for a more generic appoach.
Basically i want to do this:
confirm({
type: 'warning',
title: 'Are you sure?',
description: 'Would you like to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort'
})
.then(() => {
// do something after promise is resolved
})
The confirm method basically opens the modal and returns a promise. Inside the promise i subscribe my redux store, listen for state changes and resolve or reject the promise:
export const confirm = function(settings) {
// first dispatch openConfirmModal with given props
store.dispatch(
openConfirmModal({
...settings
})
);
// return a promise that subscribes to redux store
// see: http://redux.js.org/docs/api/Store.html#subscribe
// on stateChanges check for resolved/rejected
// if resolved or rejected:
// - dispatch closeConfirmModal
// - resolve or reject the promise
// - unsubscribe to store
return new Promise((resolve, reject) => {
function handleStateChange() {
let newState = store.getState();
if (newState.confirmModal.resolved) {
store.dispatch(closeConfirmModal());
resolve();
unsubscribe();
}
if (newState.confirmModal.rejected) {
store.dispatch(closeConfirmModal());
reject();
unsubscribe();
}
}
let unsubscribe = store.subscribe(handleStateChange);
})
}
My confirm component is connected to redux store and is included once in some kind of layout component - so it is useable on all routes in the app:
class ConfirmModal extends Component {
constructor(props) {
super(props)
}
confirm() {
this.props.dispatch(resolveConfirmModal());
}
abort() {
this.props.dispatch(rejectConfirmModal());
}
render() {
// my modal window
}
}
export default connect(
state => ({
confirmModal: state.confirmModal
})
)(ConfirmModal);
Reducer/Action looks like this:
export const openConfirmModal = (settings) => {
return {
type: 'OPEN_CONFIRM_MODAL',
settings
};
};
export const resolveConfirmModal = () => {
return {
type: 'RESOLVE_CONFIRM_MODAL'
};
};
export const rejectConfirmModal = () => {
return {
type: 'REJECT_CONFIRM_MODAL'
};
};
export const closeConfirmModal = () => {
return {
type: 'CLOSE_CONFIRM_MODAL'
};
};
const initialState = {
open: false,
type: 'info',
title: 'Are you sure?',
description: 'Are you sure you want to do this action?',
confirmLabel: 'Yes',
abortLabel: 'Abort',
};
export const ConfirmModalReducer = (state = initialState, action) => {
switch (action.type) {
case 'OPEN_CONFIRM_MODAL':
return { ...action.settings, open: true };
case 'RESOLVE_CONFIRM_MODAL':
return { ...state, resolved: true };
case 'REJECT_CONFIRM_MODAL':
return { ...state, rejected: true };
case 'CLOSE_CONFIRM_MODAL':
return initialState;
default:
return state;
}
};
The redux part is working. My confirm window can be open/closed and renders depending on my options. But how i can define a promise in my confirm method that can be resolved in my component? How i get everything connected?
Found a working Solution!
Found a solution that is pretty much what i was looking for:
The modal properties are driven by my Redux state
The modal component is included once AND it lives inside my
applicition not as a different rendered app like
here:http://blog.arkency.com/2015/04/beautiful-confirm-window-with-react/
The confirm method returns a native promise that is
resolved/rejected driven by Redux state
What do you think?

Well, you can do it, but it won't be pretty. Basically, you need a map of outstanding promises next to your confirm():
var outstandingModals = {}
const confirm = function(settings) {
return new Promise((resolve, reject) => {
let id = uuid.v4();
outstandingModals = resolve;
store.dispatch(
openConfirmModal({
...settings,
confirmationId: id,
})
);
}
and then later:
case 'CLOSE_CONFIRM_MODAL':
let resolve = outstandingModals[state.confirmationId];
if (resolve) {
resolve();
delete outstandingModals[state.confirmationId];
}
return initialState;
Like I said - ugly. I don't think you can do much better than that using promises.
But you can do better by NOT using promises. What I would do is simply render a Confirm component whenever necessary, say:
render() {
return <div>
... My stuff ...
{confirmationNecessary && <Confirm text='Are you sure?' onAction={this.thenConfirmed}/>}
</div>
}
confirmationNecessary can come from this.state or from the store.

I wrote a blog post that discusses one possible approach for managing "generic" modals like this: Posts on PacktPub: "Generic Redux Modals" and "Building Better Bundles".
The basic idea is that the code that requested the modal can include a pre-written action as a prop, and the modal can dispatch that action once it's closed.
There's also an interesting-looking library at redux-promising-modals that appears to implement modal result promises through middleware.

possible so :)
export const MsgBox = {
okCancel : s =>
new Promise((ok, cancel) =>
confirm(s) ? ok() : cancel() ),
......etc

Related

react -redux component does not re-render after state change

I have been trying and trying by my component wont re-render itself . Below is my reducer code and I have tried everything to not mutate the state. In my component code ,inside render method, I have a log statement console.log("Check Here"); I know the component does not re-render because this log works first time the component renders but after reducer changes the state the log statement is not called . In logs I can clearly see that prev state and next state are different by just that one SearchType that I am changing. Please help!!
const initState = {
searchType: ""
};
const techniqueReducer = (state = initState, action) => {
switch (action.type) {
case actionTypeConstants.GET_SEARCH:
{
return { ...state, searchType: "new string" };
}
default: {
return state;
}
}
};
export default myReducer;
My component code is below
import React, { Component } from "react";
import { connect } from "react-redux";
import * as tDispatchers from "../actions/Actions";
const mapStateToProps = state => {
  return {
  
    searchType: state.searchType
  };
};
class SearchCollection extends Component {
  Search= () => {
    this.props.dispatch(tDispatchers.getSearch(document.getElementById("txtSearch").value));
  }
 
  render() {
console.log("Check Here")
    return (
      <div class="container-fluid">
        <div>
          <input
            type="text"
            id="txtSearch"
            class="form-control"
            placeholder="Enter Search Keywords Here..."
          />
        </div>
        <div>
  <button
            className="btn btn-light btn-sm m-1"
            onClick={this.Search}
          >
            Search
          </button>
         
        </div>
  
      </div>
    );
  }
}
export default connect(mapStateToProps)(SearchCollection);
GetSearch looks like below
I plan to pass payload to reducer eventually but currently I am not
import * as actionTypeConstants from "../action_type_constants";
import axios from "axios";
export function getSearch(searchtext) {
return dispatchFunction => {
axios
.get("<api call>"+searchtext)
.then(response => {
dispatchFunction({
type: actionTypeConstants.GET_SEARCH,
payload: response.data.data
});
})
};
}
ActionTypeConstant
export const GET_SEARCH = "GET_SEARCH";
I suppose you are using redux-thunk to work with async actions. But you don't return an async function from getSearch. I believe it should be
export function getSearch(searchtext) {
return dispatchFunction => {
return axios
.get("<api call>"+searchtext)
.then(response => {
dispatchFunction({
type: actionTypeConstants.GET_SEARCH,
payload: response.data.data
});
})
};
}
or
export function getSearch(searchtext) {
return async dispatchFunction => {
const response = await axios
.get("<api call>"+searchtext);
dispatchFunction({
type: actionTypeConstants.GET_SEARCH,
payload: response.data.data
});
};
}
You are not updating searchType value, which is hardcoded to string new string. Try setting the new state from the action, for example:
return { ...state, searchType: action.payload};
Or check this, https://jsfiddle.net/xt3sqoc6/1/ and open your dev tools to see the rerenders.
You can use componentDidUpdate(prevProps, prevState). It is invoked immediately after updating occurs & you can compare the current props to previous props. Using that you can re-render your component by changing state
componentDidUpdate(prevProps) {
if (this.props.SearchType !== prevProps.SearchType) {
//Do whatever needs to happen!
}
}
You may call setState() immediately in componentDidUpdate but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop.
Hope this helps you. Feel free for doubts.

Instantiate a new Howler Object in Action Creators and dispatch actions on events

So I'm trying to build a React.js + Redux audio player.
I'm using the Howler.js Library to process audio from an API.
I'm dispatching actions from the player control buttons and from other component such as a track list component.
When the user clicks on a track in the lis, I'm dispatching a track object to the Player Reducer. I instantiate a new Howl Object here which works but there is some issues here :
Because Howler is fetching the data from an API, this is an async task and it's an anti pattern. I know that, but here I'm not triggering events from Howler so it works, but it's a bit buggy.
I can't use event triggers because it would be async and it's a bad pratice to dispatch actions from the Reducer.
What I would like :
Instanciate a new Howler Object in my Action Creator.
Use the event triggers, e.g : onload() or onloaderror(), to dispatch an other action to tell the player that Howler is ready to play a song or if there was an error.
The code :
Action Creator
export const PLAYER_INITIALIZE = 'PLAYER_INITIALIZE'
export const initialize = () => {
return {
type: PLAYER_INITIALIZE,
}
}
export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
export const setTrack = (trackId) => {
return {
type: PLAYER_SET_TRACK,
}
}
export const PLAYER_PLAY_TRACK = 'PLAYER_PLAY_TRACK'
export const playTrack = () => {
return {
type: PLAYER_PLAY_TRACK,
}
}
Reducer
function PlayerReducer(state = initialPlayerState, action) {
switch (action.type) {
case PlayerActions.PLAYER_SET_TRACK:
if (state.audioObj.state() != 'unloaded')
state.audioObj.unload();
return {
...state,
audioObj: new Howl({
src: API.API_STREAM_TRACK + action.trackId,
html5: true,
preload: true,
onload: () => {
console.log("Track loaded succesfully.");
},
onloaderror: (id, err) => {
console.error("Load Error : " + err);
}
}),
trackMetadata: action.trackMetadata
};
case PlayerActions.PLAYER_PLAY_TRACK:
state.audioObj.play();
return {
...state,
isPlaying: true,
};
[...]
My idea is to instanciate Howler like this :
new Howler({
[ ... some options ],
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS
});
},
onloaderror: () => {
dispatch({
type: PLAYER_LOAD_ERROR
});
}
});
How can I edit this code to create a new Howl Object correctly in the Action Creator and pass it to the player on each track that I want to listen?
Can you give me an example ?
Thanks.
You can add redux-thunk to your app and then be able to dispatch multiple actions from a single action creator. After adding redux-thunk, your action could look like the following:
export const setTrack = (trackId) => {
(dispatch, getState) => {
const { audioObj } = getState();
if (audioObj != null) {
audioObj.unload();
}
dispatch({
audioObj: new Howler({
[ ... some options ],
onload: () => {
dispatch({
type: PLAYER_LOAD_SUCCESS
});
},
onloaderror: () => {
dispatch({
type: PLAYER_LOAD_ERROR
});
}
}),
type: PLAYER_SET_TRACK
});
}
you can move your other async and Howler work into action creators this way and make your reducer only concerned with storing information.

Custom Websocket in Redux Architecture

The more I read about this subject it seems like going down a rabbit hole. This is a new Trading application which receives realtime data through web sockets which is based on a request-response paradigm. There are three separate SPA's in which apart from initial load, every user action triggers a call to the dataStore with a new MDXQuery. So in turn I would need to make fresh subscriptions on a componentDidMount() as well as in the respective ActionCreators.I would like to streamline the code to avoid duplicate code and redundancy.
The below code helps establish a new subscription channel to streams the response through web-socket.(Unlike, most sockets.io code where it comes with a designated open,close,send)
this.subscription = bus.channel(PATH, { mode: bus.wsModes.PULL }).createListener(this.onResponse.bind(this));
this.subscription.subscribe(MDXQuery);
If I read the REDUX documentation as to where should I place the web socket code? It mentions to create a custom middleware.
LINK: https://redux.js.org/faq/codestructure#where-should-websockets-and-other-persistent-connections-live
But I am not very sure how could I go about using this custom web socket code framing my own middleware or doing at the component level would help to mimic this strategy.
const createMySocketMiddleware = (url) => {
return storeAPI => {
let socket = createMyWebsocket(url);
socket.on("message", (message) => {
storeAPI.dispatch({
type : "SOCKET_MESSAGE_RECEIVED",
payload : message
});
});
return next => action => {
if(action.type == "SEND_WEBSOCKET_MESSAGE") {
socket.send(action.payload);
return;
}
return next(action);
}
}
}
Any design inputs would really help!!
I wrote that FAQ entry and example.
If I understand your question, you're asking about how to dynamically create additional subscriptions at runtime?
Since a Redux middleware can see every dispatched action that is passed through the middleware pipeline, you can dispatch actions that are only intended as commands for a middleware to do something. Now, I'm not sure what an MDXQuery is, and it's also not clear what you're wanting to do with the messages received from these subscriptions. For sake of the example, I'll assume that you want to either dispatch Redux actions whenever a subscription message is received, or potentially do some custom logic with them.
You can write a custom middleware that listens for actions like "CREATE_SUBSCRIPTION" and "CLOSE_SUBSCRIPTION", and potentially accepts a callback function to run when a message is received.
Here's what that might look like:
// Add this to the store during setup
const subscriptionMiddleware = (storeAPI) => {
let nextSubscriptionId = 0;
const subscriptions = {};
const bus = createBusSomehow();
return (next) => (action) => {
switch(action.type) {
case "CREATE_SUBSCRIPTION" : {
const {callback} = action;
const subscriptionId = nextSubscriptionId;
nextSubscriptionId++;
const subscription = bus.channel(PATH, { mode: bus.wsModes.PULL })
.createListener((...args) => {
callback(dispatch, getState, ...args);
});
subscriptions[subscriptionId] = subscription;
return subscriptionId;
}
case "CLOSE_SUBSCRIPTION" : {
const {subscriptionId} = action;
const subscription = subscriptions[subscriptionId];
if(subscription) {
subscription.close();
delete subscriptions[subscriptionId];
}
return;
}
}
}
}
// Use over in your components file
function createSubscription(callback) {
return {type : "CREATE_SUBSCRIPTION", callback };
}
function closeSubscription(subscriptionId) {
return {type : "CLOSE_SUBSCRIPTION", subscriptionId};
}
// and in your component:
const actionCreators = {createSubscription, closeSubscription};
class MyComponent extends React.Component {
componentDidMount() {
this.subscriptionId = this.props.createSubscription(this.onMessageReceived);
}
componentWillUnmount() {
this.props.closeSubscription(this.subscriptionId);
}
}
export default connect(null, actionCreators)(MyComponent);
I tried out your solution for my own problem which involves creating a socket instance only when a user is logged and here is how my code looks:
const socketMiddleWare = url => store => {
const socket = new SockJS(url, [], {
sessionId: () => custom_token_id
});
return next => action => {
switch (action.type) {
case types.USER_LOGGED_IN:
{
socket.onopen = e => {
console.log("Connection", e.type);
store.dispatch({
type: types.TOGGLE_SOCK_OPENING
});
if (e.type === "open") {
store.dispatch({
type: types.TOGGLE_SOCK_OPENED
});
createSession(custom_token_id, store);
const data = {
type: "GET_ACTIVE_SESSIONS",
JWT_TOKEN: Cookies.get("agentClientToken")
};
store.dispatch({
type: types.GET_ACTIVE_SESSIONS,
payload: data
});
}
};
socket.onclose = () => {
console.log("Connection closed");
store.dispatch({
type: types.POLL_ACTIVE_SESSIONS_STOP
});
// store.dispatch({ type: TOGGLE_SOCK_OPEN, payload: false });
};
socket.onmessage = e => {
console.log(e)
};
if (
action.type === types.SEND_SOCKET_MESSAGE
) {
socket.send(JSON.stringify(action.payload));
return;
} else if (action.type === types.USER_LOGGED_OUT) {
socket.close();
}
next(action);
}
default:
next(action);
break;
}
};
};
It doesn't work though but could you point me in the right direction. Thanks.

Promise.catch in redux middleware being invoked for unrelated reducer

I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.
Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.

Redux state change in componentWillMount is not be recognized in componentDidMount?

Need to load my main component and, in case a localstorage with the pair value "logged: true" exists redirect to "/app" using react-router.
I am using react-redux and this is my code:
class Main extends Component {
componentWillMount(){
// Return true in redux state if localstorage is found
this.props.checkLogStatus();
}
componentDidMount(){
// redirect in case redux state returns logged = true
if(this.props.logStatus.logged){
hashHistory.push('/app');
}
}
render() {
return (
<App centered={true} className="_main">
{this.props.children}
</App>
);
}
}
My redux action:
checkLogStatus() {
// check if user is logged and set it to state
return {
type: LOGIN_STATUS,
payload: window.localStorage.sugarlockLogged === "true"
};
}
But when the component gets to the componentDidMount stage, my redux state has still not been updated.
Y manage to get this to work by using:
componentWillReceiveProps(nextProps){
if (nextProps.logStatus.logged && nextProps.logStatus.logged !== this.props.logStatus.logged){
hashHistory.push('/app');
}
}
But I am not sure it is the most elegant solution.
Thanks in advance!
Using componentWillReceiveProps is the way to go here since your logStatus object is being passed in as a prop that is being changed.
There is a more elegant way to this using the Redux-thunk middleware which allows you to dispatch a function (which receives dispatch as an argument instead of the object actions. You can then wrap that function in a promise and use it in componentWillMount.
In your actions file:
updateReduxStore(data) {
return {
type: LOGIN_STATUS,
payload: data.logInCheck
};
}
validateLocalStorage() {
...
}
checkLogStatus() {
return function(dispatch) {
return new Promise((resolve, reject) => {
validateLocalStorage().then((data) => {
if (JSON.parse(data).length > 0) {
dispatch(updateReduxStore(data));
resolve('valid login');
} else {
reject('invalid login');
}
});
});
};
}
Then in your component:
componentWillMount() {
this.props.checkLogStatus()
.then((message) => {
console.log(message); //valid login
hashHistory.push('/app');
})
.catch((err) => {
console.log(err); //invalid login
});
}
Redux-thunk middleware is made for such use cases.

Resources