Trigger Notification/Callback after reflux action execution - javascript

I recently used reflux in my project and here is a question puzzled me a lot.
As the reflux pattern, I call actions in my React components, and fetch remote data in my reflux store which are listening to the actions. And my components listen to the changing of data in store. For example get a list of items. So far it's fine.
But sometimes, I want a notification which can told me that the action has executed successfully.
For example, I have a UserStore, a UserActions and a LoginComponent which listen to UserStore. when users have input username and password and click a submit button, the LoginComponent call UserActions.login(), and I send login request in UserStore. When login succeed, UserStore gets user infos from the response.
In this time, I want to give an prompt in LoginComponent such as 'Login Success'. I have two ways to do it but I don't think either is good enough.
Give a flag in the data of UserStore then trigger changing. LoginComponent gets this flag when UserStore trigger a data change event, and then prompt. Because the UserStore would trigger data change not only in login success but also in some other situations like fetching user infos from cookies or sessionStorage, LoginComponent has to add an if-else for this login success flag, if it is login success then prompt.
I don't think it is a good pattern because that the flag is just for the prompt and NOT a real data like user's infos. And if I also want prompt after user change password, I will need another flag field.
Pass a promise(or a callback function) to the UserAction call, and resolve this promise after login succeed, then the LoginComponent can prompt in promise.then. It's seem better than the first one, but isn't it a little anti-pattern because the promise which been passed through actions to stores may broke the Unidirectional in reflux?
What I want to ask is: What's the common/appropriate way to solve this problem?
I'm not from an English area and not good at English expression. This is my first question in stackoverflow.com. I'm not very sure if I have described my question clearly. So if you have some advice to me about the question, please let me know and I will improve it to help others who care about this question. Thanks a lot!

You can include a parameter in the trigger.
export default class AppCtrl extends AppCtrlRender {
constructor() {
super();
this.state = getState();
}
componentDidMount = () => { this.unsubscribe = BasicStore.listen(this.storeDidChange); }
componentWillUnmount = () => { this.unsubscribe(); }
storeDidChange = (id) => {
switch (id) {
case 'data1': this.setState({Data1: BasicStore.getData1()}); break;
case 'data2': this.setState({Data2: BasicStore.getData2()}); break;
case 'data3': this.setState({Data3: BasicStore.getData3()}); break;
default: this.setState(getState());
}
}
}
import Reflux from 'reflux';
import Actions from '../actions/sa.Actions';
import AddonStore from './Addon.Store';
import MixinStoreObject from './Mixin.Store';
function _GotData(data) { this.data1 = data; BasicStore.trigger('data1'); }
let BasicStoreObject = {
init() { this.listenTo(AddonStore, this.onAddonTrigger); },
data1: {},
listenables: Actions,
mixins: [MixinStoreObject],
onGotData1: _GotData,
onAddonTrigger() { BasicStore.trigger('data2'); },
getData1() { return this.data1; },
getData2() { return AddonStore.data2; },
getData3() { return this.data3; }
}
const BasicStore = Reflux.createStore(BasicStoreObject);
export default BasicStore;

Related

Pattern for redirecting on unauthorized vuex actions

Navigation guards are perfect for redirecting unauthorized users to a login page, but what does one do to redirect unauthorized vuex actions to a login page?
I can do this easily enough in the vue method where I'm calling the action like so:
if (!this.isLoggedIn) {
this.$router.push({ name: 'login', query: { comeBack: true } })
$(`.modal`).modal('hide')
return
}
But then I'm inserting these 5 lines of code for every component method that requires authorization.
All the solutions I can think of sound hacky, so I'm wondering what the vuex way is:
In order to reject it at the vuex action level, I have to pass up the $router instance, and I'm still reusing the 5 lines for each action that requires auth.
I can handle this in a utility file, but then I'm handling $router instance in that file.
I can use a global vue mixin and call it (a) before making a call and then again (b) when getting a 401 back from the server.
All those seem odd. What vuex way am I missing here?
This sounds like a job for middleware. Unfortunately, Vuex doesn't have an official way to do middleware.
There is a subscribeAction() but that runs after the commit, so does not allow mods to the action. There is also a proposal Middleware processing between actions and mutation.
As I see it, we want middleware to be able to do two generic things
cancel the action
allow alternative actions to be called
The second is difficult to do without patching store.dispatch() or messing with the private property _actions after store has been created.
However, to guard an action as you describe, we only need to be able to cancel it.
Here is a poor-man's middleware for the modules pattern for Vuex store which I prefer.
store construction from modules
export const store = new Vuex.Store({
modules: {
config,
pages: applyMiddleware(pages),
measures,
user,
loadStatus,
search
}
})
applyMiddleware
const applyMiddleware = function(module) {
if(module.middlewares) {
Object.values(module.middlewares).forEach(middlewareFn => {
Object.keys(module.actions).forEach(actionName => {
const actionFn = module.actions[actionName]
module.actions[actionName] = addMiddleware(actionName, actionFn, middlewareFn)
});
})
}
return module;
}
addMiddleware
const addMiddleware = function(actionName, actionFn, middlewareFn) {
return function(context, payload) {
const resultFn = middlewareFn(actionFn)
if(resultFn) {
resultFn(context, payload)
}
}
}
defining middleware in the module
const actions = {
myAction: (context, payload) => {
...
context.commit('THE_ACTION', payload)
...
},
}
const middlewares = {
checkAuthMiddleware: (action) => {
return this.isLoggedIn
? action // if logged-in run this action
: null; // otherwise cancel it
}
}
export default {
state,
getters,
mutations,
actions,
middlewares
}
This implementation has module-specific middleware functions, but you could also define them globally and apply to as many modules as applicable.

dispacthing actions creator gets called twice using componentWillMount

I call action creator using componentWillMount method, then it gets called twice. look at below screenshot of the result.
this is my action creator :
export function fetchAdmin(){
return (dispatch)=>{
axios.get(`${Config.API_URL}/admin`,{
headers: { authorization: localStorage.getItem('token')}
})
.then(response => {
return dispatch({
type: FETCH_DATA,
payload: response.data
});
}
)
.catch(response => {
console.log(response);
});
}
}
and this is my reducer :
import { FETCH_DATA } from '../actions/types';
export function admin(state= {}, action){
switch(action.type){
case FETCH_DATA:
return {...state, lists: action.payload };
default:
return { state, lists: 'YEY!'}
}
}
and this is my container and the way i call my action creator using componentWillMount.
import React, { Component } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import * as actions from 'actions/admin_action';
class Admin extends Component{
componentWillMount(){
this.props.fetchAdmin()
}
renderData(){
var lists = this.props.lists;
console.log(lists);
}
render(){
return(
<div>
{this.renderData()}
</div>
)
}
const mapStateToProps = (state) => {
return { lists: state.admin.lists };
}
export default connect(mapStateToProps, actions)(Admin);
from the screenshot i assume that render() gets called twice. the first call goes to default action creator so that the result is "YEY!" and the second is the valid one.
can someone explain it to me why componentWillMount perform like this and actually in the first render i just wanna my array not "YEY!" how to solve this problem? Thank you.
There is no problem here! When your component render first time, it reached the rendering before the logic componentWillMount is finished, so:
First time rendering: Your component is rendered while the lists in global state is the default which equals YEY!.
Second time rendering: When data is fetched successfully, after that the reducer change in the lists in the global state with the fetched ones, after that your component detects the change due to mapStateToProps that got the lists from the global state.
Needless to say that React component is re-rendered on propsChange, that's why there is a method componentWillReceiveProps so that you can add some logic (if you need) on each time your props change.
This behavior allows you to handle waiting the data to be fetched by:
Add a loading spinner.
Get your layout rendered.
Get the other components in your page rendered.
Javascript is asynchronous. The HTTP request you are performing is asynchronous. This means that Javascript execution continues while the HTTP request is still pending.
So React already renders while the state is still 'Yey'. This is how it should be and also the beauty of Redux, actions are fire and forget, they are not blocking. If React would wait for the HTTP request to finish, user experience would be horrible!
If you don't want it to render 'Yey'. You should show a loader or something while the request is still pending.

Redux; accessing other parts of the state when using composed reducers via combineReducers

Update
Thanks to #Dominic Tobias and #gabdallah for spotting my embarrassing mistake.
The correct answer is of course;
so try checking action.payload.
The other comments regarding the switch statement and the action object we're referring to errors I made in my example, which I've since corrected.
Imagine I've combined the the following two reducers;
import { combineReducers } from 'redux'
import { routerStateReducer } from 'redux-router'
import entries from './entries'
export default combineReducers({
router: routerStateReducer,
entries
})
I would like to mutate the entries state based on another part of the global state, in this case; the router state provided by redux-router in order for example to implement pagination.
How could I do something like this?
// entries.js
import { ROUTER_DID_CHANGE } from 'redux-router/lib/constants'
const initialState = {}
function entries (state = initialState, action) {
switch (action.type) {
case ROUTER_DID_CHANGE:
// How can I access `state.router` here in order to do something like this;
if (routerState.location.pathname === '/entries') {
return {
...state,
page: routerState.location.query.page || state.page,
limit: routerState.location.query.limit || state.limit
}
}
return state
}
}
Some other approaches that come to mind;
connect the router state to the Entries route component, use the componentWillMount lifecycle method to check router state and call an action creator with the page and limit values mutating the entries state in turn. This would work; however I'm using some transition middleware to call a static fetchData method on the route component prior to mounting it, so the data get's fetched, then the pagination action creator would be called afterwards; not the desired behaviour.
listen to router actions somewhere else (i.e a dedicated router redux module), call an action creator on the entries store, but I'm not sure how well this fits with redux-router or how I would get access to the router part of the global store.
don't do this at all; simply query the router state in the static fetchData method
Other useful info;
The implementation in question is Universal App heavily inspired by react-redux-universal-hot-example
Relevant deps
react 0.14.2
redux 3.0.3
react-router 1.0.0-rc3
redux-router 1.0.0-beta3
How can I achieve this or similar behaviour? Am I even thinking about this the right way?
Back in the old days before things got simpler for a developer people would listen to the popstate event on the history object ;)
It looks like the required info is on the action?
history.listen((error, nextRouterState) => {
...
store.dispatch(routerDidChange(nextRouterState));
and the action:
export function routerDidChange(state) {
return {
type: ROUTER_DID_CHANGE,
payload: state
};
}
So try checking action.payload.
However your switch statement is using action instead of action.type so there's something fishy going on there.. You shouldn't need to do action = {} either - see http://redux.js.org/docs/basics/Reducers.html

React/Redux app actions not being dispatched properly

I'm trying to set up my first React app using Redux, but I'm currently having some issues with dispatching actions. I have the following files for my actions so far:
constants/AuthActionTypes.js:
export const AUTH_PENDING = 'AUTH_PENDING';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAIL = 'AUTH_FAIL';
actions/AuthActions.js:
import * as AuthActionTypes from '../constants/AuthActionTypes';
function authPending() {
return { type: AuthActionTypes.AUTH_PENDING };
}
function authSuccess(response) {
return { type: AuthActionTypes.AUTH_SUCCESS };
}
export function login(username, password) {
return dispatch => {
dispatch(authPending());
// nothing really happens yet
dispatch(authSuccess());
};
}
I've also got a login component containing an HTML form, and a login function that gets called when the form is submitted.
components/Login.js
...
login (e) {
e.preventDefault();
var username = ReactDOM.findDOMNode(this.refs.username).value;
var password = ReactDOM.findDOMNode(this.refs.password).value;
this.props.dispatch(AuthActions.login(username, password));
}
...
The function is triggered, but... something isn't right. I use redux-devtools, and it only dispatches a single action - surely, it should dispatch the actions from both authPending and authSuccess? In addition, the devtools pane doesn't display any action type, and I get the following warning in the console:
Warning: Failed propType: Invalid prop action of type function
supplied to LogMonitorEntry, expected object. Check the render
method of LogMonitor.
What am I doing wrong here? What have I missed? I've primarily looked at the examples at https://github.com/rackt/redux/tree/master/examples/real-world and https://github.com/yildizberkay/redux-example, and I can't see what I'm doing differently that makes my own app break.
... and a couple of minutes later, I've stumbled across the answer: I didn't have the redux-thunk middleware applied to my store. Applied that, and everything works as expected.
You can do w/o thunk.
change
this.props.dispatch(AuthActions.login(username, password));
to
this.props.dispatch(AuthActions.authPending());
this.props.dispatch(AuthActions.authSuccess());
Ofcourse, you have to import those two methods/action-creators.

Flux / Fluxible: Updating routes based on state change

What is the most concise way to trigger route changes based on a change to a state store, using Fluxible and react router?
An example component might take some user input and call an Action on a click event (shortened for brevity)
class NameInput extends React.Component {
constructor (props) {
super(props);
this.state = props.state;
this.handleClick = this.handleClick.bind(this);
}
handleClick (event) {
this.context.executeAction(setName, {name:'Some User Value'});
}
render () {
return (
<div>
<input type="button" value="Set Name" onClick={this.handleClick} />
</div>
);
}
}
export default Home;
The handleClick method executes an Action which can update a Store with our new value.
But what if I also want this to trigger a navigation after the Store is updated? I could add the router context type and directly call the transition method after executing the Action:
this.context.executeAction(setName, {name:'Some User Value'});
this.context.router.transitionTo('some-route');
But this assumes that the setName Action is synchronous. Is this conceptually safe, on the assumption that the new route will re-render once the Action is completed and the Store is updated?
Alternatively, should the original Component listen for Store Changes and start the route transition based on some assessment of the store state?
Using the Fluxible, connectToStores implementation, I can listen for discreet changes to Store state:
NameInput = connectToStores(NameInput, [SomeStore], function (stores, props) {
return {
name: stores.SomeStore.getState().name
}
});
How a could a Store listener of this type be used to initiate a Route change?
I've noticed in my own application that for these kinds of flows it's usually safer to let actions do all the hard work. Annoying thing here is that the router isn't accessible from actions.
So, this is what I'd do:
Create a meta-action that does both: setNameAndNavigate. As payload you use something like this:
{
name: 'Value',
destination: {to: 'some-route', params: []},
router: this.context.router
}
Then, in your action, do the navigating when the setName completes.
It's not ideal, especially the passing of the Router to the action. There's probably some way to attach the Router to the action context, but that's not as simple as I had hoped. This is probably a good starting point though.
Extra reading:
Why do everything in actions? It's risky to execute actions in components in response to store changes. Since Fluxible 0.4 you can no longer let actions dispatch inside another dispatch. This happens a lot faster than you think, for example, executing an action in response to a change in a store, without delaying it with setImmediate or setTimeout, will kill your application since store changes happen synchronously during a dispatch.
Inside actions however, you can easily execute actions and dispatches, and wait for them to complete before executing the next one.
The end result of working this way is that most of your application logic has moved to actions, and your components turn into simple views that set-and-forget actions only in response to user interactions (clicks/scrolling/hover/..., as long as it's not in response to a store change).
The Best way is to create a new action as #ambroos suggested,
setNameAndNavigate. For navigation though, use the navigateAction
https://github.com/yahoo/fluxible/blob/master/packages/fluxible-router/lib/navigateAction.js, you would only have to give the url as argument.
Something like this,
import async from 'async';
import setName from 'some/path/setName';
export default function setNameAndNavigate(context, params, done) {
async.waterfall([
callback => {
setName(context, params, callback);
},
(callback) => {
navigate(context, {
url: '/someNewUrl',
}, callback);
}
], done);
}
let your actions be the main workers as much as possible.

Categories