3

I'm talking about the mapStateToProps argument of connect.

One of my components has a property called rules which is an array of functions. In mapStateToProps I invoke those rules, passing in the current state. The rules either return an error message or true if there is no error. I put these error messages into a new prop so that the final component receives an array of error messages instead of an array of functions.

So far this has been working great, but now I want to add support for asynchronous rules. My plan right now is that whenever a rule returns a Promise instead of an error string, I will push a message like "Checking..." into the array of error messages instead, but once the promise is settled I need to "refresh" this array and insert the resolved message.

I figure if I can force an update then redux will have to call mapStateToProps again. In such a case, all the rules would be re-evaluated, but if I'm not careful I would receive a new Promise which would put right back to square one. However, if I memoize these rules then I can get back the same Promise instance as before, and then check if it's settled and then put that value into my error array instead.

So now the only problem is figuring out how to get a reference to the component instance within the context of mapStateToProps which only has access to the current state and props.

Is there any way I can force a re-render once my Promise has resolved?

Alternatively, if I could dispatch an action when it's resolved, I could work with that as well.

Community
  • 1
  • 1
mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • I *think* this is doable with `connectAdvanced`. With that I can get access to the state, props and dispatch function all at once, which should give me enough power to trigger a refresh when the promises resolve. – mpen Jan 27 '17 at 00:05

2 Answers2

2

You can't do that in mapState. The mapState function is intended to be totally synchronous. If you need to do something async, trigger that outside of mapState, store some values in state, and update them with the results to force mapState to re-run.

markerikson
  • 63,178
  • 10
  • 141
  • 157
  • I'm not sure how I would do this outside of `mapStateToProps`. I need both the state and the props of the component to evaluate these rules. I can get the state in my reducer, but then I'm missing the props. Even if I sent all the props along with every action I dispatch, it wouldn't help because then I could only revalidate the field that triggered the action, but I have contingent fields that may change validity based on the value of other fields. – mpen Jan 26 '17 at 22:15
  • You'll need some kind of middleware to do what you're asking to. Redux-saga https://github.com/redux-saga/redux-saga or Redux Thunk https://github.com/gaearon/redux-thunk are popular – AnilRedshift Jan 27 '17 at 00:14
  • @AnilRedshift I don't think either of those libs help with the problem I described. I don't have access to `dispatch` where I need it, so a middleware that handles dispatched actions doesn't help. – mpen Jan 27 '17 at 00:34
1

How about creating a stateful component? If you keep the results from your promises in the component's state, it will automatically re-render the component once the promises resolve.

import React, {Component} from 'react';
import {connect} from 'react-redux;
import Promise from 'promise';

class MyStatefulComponent extends Component {
    constructor(props) {
        this.state = {
            messages: [] // initialize empty, or with the synchronous rules
        };
    }

    componentWillMount() {
        const setState = this.setState;
        const rules = this.props.rules; // rules contains a list of either evaluated rules or promises
        Promise.all(rules).then(results => {
            // this is executed once all the promises in rules have resolved
            // setState will trigger a re-render of this component
            setState({
                messages: [] // construct your array of messages here from the results
            });
        })
    }

    render() {
        // render your list of messages
        return <ul>
            {this.state.messages.map(message => <li>{message}</li>)}
        </ul>;
    }
}

export default connect(
    (state, ownProps) => ({
        rules: ownProps.rules.map(rule => rule(state))
    })
)(MyStatefulComponent);
R3tep
  • 12,512
  • 10
  • 48
  • 75
forrert
  • 4,109
  • 1
  • 26
  • 38
  • I hadn't considered that! I'd have to work all that state into the HOC though; I'm building a wrapper for inputs and that's too much boilerplate to repeat. Also, I wouldn't be able to wrap stateless functional components anymore. But... I'll give this a whirl if my plan doesn't work out, thanks for the idea! – mpen Jan 27 '17 at 17:50