Flux/React Complex Reusable Component
Asked Answered
C

1

12

I want to do something like this

var App = React.createClass({
    render: function() {
        return (
            <CountryAutoComplete />
        )
    }
});

Different app

var App2 = React.createClass({
    render: function() {
        return (
            <CountryAutoComplete />
        )
    }
});

Here is a simple Autocomplete (Not the entire file)

var AutoComplete = React.createClass({
    componentDidMount: function() {
        $(this.getDOMNode()).typeahead(this.props);
    },
    render: function() {
        return (
            <input type="text" class="typeahead" onChange={this.props.onChange} />
        );
    }
});

The CountryAutoComplete would be something like this to be self contained.

var CountryAutoComplete = React.createClass({
    search: function(country, process) {
        // Make an ajax call and return the data. No other components needed
        $.ajax({
            url: '/country' + '?search=' + country
        }).then(process);
    },
    render: function() {
        return (
            <AutoComplete onChange={this.props.onChange} source={this.search} />
        );
    }
});

Based on the Flux docs, it looks like anything with an API call needs to go through the

actions -> API -> Dispatcher -> stores -> component

That makes the CountryAutoComplete tied to a specific app because the actions, Dispatcher and stores are specific to the App. What is the best way to make this component reusable across apps?

Cracy answered 7/11, 2014 at 2:24 Comment(0)
L
10

You shouldn't be making any ajax calls in the autocomplete component (since you said you want to make it reusable). I usually put all the data request calls/api usage into a separate module that uses promises to prevent the multiple requests

So the idea then is to just have your autocomplete component get the options/data from a parent component. That parent component can get the data from store initially, and listen for any change events in that store, and update its state if needed. Pass that this.state.options (or whatever state you're using for the options) as a prop to AutoComplete. When a user types something, emit an action with the query. That action in turns should call the API and Dispatcher, updates the store, and emits a change event for the store. Your parent component will update its state respectively, and that will flow to the AutoComplete component as prop.

So something like this:

var App = React.createClass({
    getInitialState: function() {
        return {
            // default results/data?
            data : Store.getResults('')
        };
    },
    storeChangeListener: function(newData) {
        this.setState({
            data: newData
        });
    },
    componentDidMount: function() {
        this.listenTo(Store, this.storeChangeListener);
    },
    onChange: function(query) {
        // on search change
        Actions.getResults(query);
    },
    render: function() {
        return (
            <AutoComplete data={this.state.data} onChange={this.onChange} />
        );
    }
});

And in store, something like this:

var countryAPI = require('./countryAPI')
var Store = {
    getResults: function(query) {
        // check cache if any? otherwise make call
        if(this.cache[query]) {
            return this.cache[query];
        } else {
            countryAPI.search(query).then(this.update);
        }
    },
    update: function(data) {
        AppDispatcher.dispatch({
            type: "DATA_FROM_SERVER",
            payload: {id: query, data: result}
        })
    },
    handleDataFromServer: function(action) {
        //store into cache/store
        this.cache[action.payload.id] = action.payload.result;
        this.emit("change"); // re-render app on whoever is listening to this store
    }
}

and your api for example

var countryAPI = {
    search: function(query) {
        // check to make sure this promise isnt called before
        if(!this.allPromises[query]) {
            this.allPromises[query] = $.ajax({
                url: '/country' + '?search=' + country
            })
        }
        return this.allPromises[query];
    }
}

To sum it up, the actual API implementation imo should be separated from flux actions, they only should be concerned with Web-API specific stuff and just let flux actions/stores handle the responses separately as data flows:

Component --> Listens to Store
          --> Calls Load Action --> Show Pending State/Optimistic Updates --> Dispatcher --> Store --> changeEvent (Component will be listening and be updated)
          --> countryAPI.load() 
onLoadSuccess --> Dispatcher --> Store --> changeEvent --> Component
onLoadError   --> Dispatcher --> Store --> changeEvent --> Component
Lycanthropy answered 8/11, 2014 at 0:29 Comment(4)
Thanks for such a detailed answer. It was very helpful. Are components not suppose to keep state or call APIs directly? This seems like a lot of boiler plate to use this component each time. The dispatcher is specific to the App.Cracy
well react is meant to be a view, hence any ajax requests (api calls) in my opinion should be separated from the react components. it might be a lot of boiler plate, but it keeps the component reusable and isolated. Here's a more written-out article about the data flow of this: code-experience.com/…Lycanthropy
Because the autocomplete relies on ajax, and ajax belongs in a separate component/dispatcher, any component that relies on ajax cannot truly be an independent, reusable component? What would be wrong with having ajax contained in the autocomplete component, and having the component emit events that the application's dispatcher and store can listen to?Unhitch
What about maintaining focus on the relevant HTML element?Persaud

© 2022 - 2024 — McMap. All rights reserved.