Flux/Alt data dependency, how to handle elegantly and idiomatically
Asked Answered
P

3

9

I'm using alt as my flux implementation for a project and am having trouble wrapping my head around the best way to handle loading stores for two related entities. I'm using sources feature along with registerAsync for to handle my async/api calls and bind them to my views using AltContainer.

I have two entities related one to one by the conversationId. Both are loaded via an api call:

enter image description here

Once my job store is loaded with data I want to fill a conversation store.

I use a source to load the job store:

module.exports = {
    fetchJobs() {
        return {
            remote() {
                return axios.get('api/platform/jobs');

            },....

Seems like a job for the waitFor() method, but it seems for use when the contents of one store require a transformation or merging with the contents of another. I need to fetch the contents of one data store based on the contents of another.

In general terms I need to:

  • Call a third party API and load a list of entities into a store.
  • When that data arrives I need to use attribute from each of the above to call another API and load that data into another store.

My naive solution is to reference the conversation actions from the job store and to dispatch an event when the data arrives. Something like this:

var jobActions = require('../actions/Jobs');
var conversationActions = require('../actions/Conversations');

class JobStore {
    constructor() {
        this.bindListeners({
            handlefullUpdate: actions.success
        });...

    }

    handlefullUpdate(jobs) {
        this.jobs = jobs;
        conversationActions.fetch.defer(jobs);
    }
}

Of course, doing this, violates the dictum that stores shouldn't dispatch events, and so I must use defer to dispatch an action in the middle of a dispatch. It makes sense to me, since it seems in going down this path I'm reintroducing all sorts of side effects in my code; losing the beauty of the "functional pipelines" that I should be seeing with flux.

Also, my job store has to hold a reference to any dependent entities so it can dispatch the appropriate action. Here I have only one, but I could imagine many. In terms of the dependencies between entities this seems totally backwards.

A couple of alternatives come to mind:

I can call the api/platform/jobs endpoint in the source/action where I fetch all conversations, just to get the id. The original approach is more efficient, but this seems truer to the spirit of flux in that I lose all the cross-talk.

I could also have single action/source that fetches both, returning {jobs:{}, conversations: in the action} (orchestrating the dependency there using promises) and use this populate both stores. But this approach seems unnecessarily complicated to me (I feel like I shouldn't have to do it!).

But am I missing another way? It seems strange that such a common use case would break the elegance of the flux paradim and/or forces me to jump through so many hoops.

@dougajmcdonald posed a similar question here, but maybe it was phrased too generally, and didn't get any traction:

Printmaker answered 11/2, 2016 at 16:21 Comment(0)
P
0

I've been looking at @gravityplanx's answer, but am not sure how much it improves the situation.

To reiterate and amplify: I really, really like the alt pattern of loading my stores from a source. Every component that needs a store(s) looks something like so:

export default class extends React.Component {
    componentDidMount() {
        CurrentUser.fetch();
        Jobs.fetch(); 
    };
    render() {    
        return(
          <AltContainer stores={{user:CurrentUser, jobs:Jobs}}>
               <Body/>
          </AltContainer>)
    }

}

The intent is understandable at a glance. And I get a clean separation of concerns, making it easier to test my actions/stores, and sources.

But the beautiful pattern breaks down in the common use case from my question, and I wind up having to create some fairly complicated plumbing in my actions and stores to orchestrate the ajax calls for the conversations. The other answer seems to just shift this complexity elsewhere. I want it gone.

What I wound up doing was to completely separate the stores (the first solution suggested in the question). I make an extra ajax call to fetch the conversationId off the job and another to fetch the conversations in the JobConversation source. Something like this:

  axios.get(window.taxFyleApi + 'api/platform/active-jobs-pick/layerConversation?jobId=' + jobId)
            .then((res)=> {
                let job = res.data[0];  //Returns an array with only one item
                let qBuilder = window.layer.QueryBuilder.messages().forConversation(job.conversationId)...

I preserve the nice way of using AltContainer with my components and lose all the orchestration plumbing. Right now, I think the resultant clarity is worth the extra back end call.

I also realize how I'd LIKE it to work (in terms of notation), and will ask @goatslacker about, or may take a shot at doing it myself. I'd like to be able to specify the dependency in the exportAsync() method on a store. I'd love to be able to say:

class JobConvesationStore {
    constructor() {
        this.exportAsync(source, JobStore);
    }

The async method JobConversationStore would not be called until the JobStore had its data. The intent is easy to read and no complex action choreography would be needed.

Printmaker answered 20/2, 2016 at 15:59 Comment(0)
W
1

This definitely seems like a flaw in the Flux design, and you're right, it is a very common use case. I've been researching this issue (and a few similar ones) for a few days now to better implement Flux in my own system, and while there are definitely options out there, most have drawbacks.

The most popular solution seems to be using Containers, such as AltContainer, to abstract away the task of data requesting from apis and loading from stores into a single component, absent of ui-logic. This Container would be responsible for seeing the information from the stores and determining if additional actions are required to make them complete. For example;

static getPropsFromStores() {
    var data = SomeStore.getState();
    if (/*test if data is incomplete */){
        SomeAction.fetchAdditionalData();
    }
    return data;
}

It makes sense. We have the logic and component in our Component layer, which is where Flux says it belongs.

Until you consider the possibility of having two mounted containers asking for the same information (and thus duplicating any initialization fetches, i.e. conversations in your model), and the problem only gets worse if you add a third (or fourth, or fifth) container accessing those same stores.

The canned response to that from the Container crowd is "Just don't have more than one!", which is great advice... if your requirements are ok with that.

So here's my altered solution, around the same concept; Include one Master Container/Component around your entire application (or even beside it). Unlike traditional containers, this Master Container/Component will not pass store properties to its children. Instead, it is solely responsible for data integrity and completion.

Here's a sample implementation;

class JobStore {
    constructor() {
        this.bindListeners({
            handleJobUpdate: jobActions.success
        });

    },

    handleJobUpdate(jobs) {
        this.jobs = jobs;            
    }
}

class ConversationStore {
    constructor() {
        this.bindListeners({
            handleJobUpdate: jobActions.success,
            handleConversationUpdate: conversationActions.success
        });

    },

    handleJobUpdate(jobs) {
       this.conversations = {};
       this.tmpFetchInfo = jobs.conversation_id;
       this.isReady = false;

    },

    handleConversationUpdate(conversations) {
       this.conversations = conversations;
       this.tmpFetchInfo = '';
       this.isReady = true;
    }

}

class MasterDataContainer {

    static getPropsFromStores() {
        var jobData = JobStore.getState();
        var conversationData = ConversationStore.getState();

        if (!conversationData.isReady){
            ConversationAction.fetchConversations(conversationData.tmpFetchInfo);
        }  
    },

    render: function(){
        return <div></div>;
    }
}
Waldo answered 19/2, 2016 at 4:22 Comment(1)
I'll look over your code when my head is clearer. Thanks.Printmaker
P
0

The best solution I've come with with so far (and the one that the examples seem to follow) is to add a "jobsFetched" action to my jobs actions and to dispatch it when the data arrives.

var jobActions = require('../actions/Jobs');
var ConversationActions = require('../actions/Conversations');

class JobStore {
    constructor() {
        this.bindListeners({
            handlefullUpdate: jobActions.success...
        });...

    }

    handlefullUpdate(jobs) {
        this.jobs = jobs;
        ConversationActions.jobsFetched.defer(jobs);
    }
}


class ConversationStore {
    constructor() {
        this.bindListeners({
            handleJobUpdate: jobActions.jobsFetched...
        });...

    }

    handleJobUpdate(jobs) {

        /*Now kick off some other action like "fetchConversations" 
          or do the ajax call right here?*/

    }

}

This eliminates the problem of the jobs store having to hold a reference to all its dependent objects, But I still have to call an action from inside my store, and I have to introduce jobsFetched action which sets up the ConversationStore to fetch it's data. So it seems like I can't use a source for my conversations.

Can anyone do better?

Printmaker answered 14/2, 2016 at 22:38 Comment(0)
P
0

I've been looking at @gravityplanx's answer, but am not sure how much it improves the situation.

To reiterate and amplify: I really, really like the alt pattern of loading my stores from a source. Every component that needs a store(s) looks something like so:

export default class extends React.Component {
    componentDidMount() {
        CurrentUser.fetch();
        Jobs.fetch(); 
    };
    render() {    
        return(
          <AltContainer stores={{user:CurrentUser, jobs:Jobs}}>
               <Body/>
          </AltContainer>)
    }

}

The intent is understandable at a glance. And I get a clean separation of concerns, making it easier to test my actions/stores, and sources.

But the beautiful pattern breaks down in the common use case from my question, and I wind up having to create some fairly complicated plumbing in my actions and stores to orchestrate the ajax calls for the conversations. The other answer seems to just shift this complexity elsewhere. I want it gone.

What I wound up doing was to completely separate the stores (the first solution suggested in the question). I make an extra ajax call to fetch the conversationId off the job and another to fetch the conversations in the JobConversation source. Something like this:

  axios.get(window.taxFyleApi + 'api/platform/active-jobs-pick/layerConversation?jobId=' + jobId)
            .then((res)=> {
                let job = res.data[0];  //Returns an array with only one item
                let qBuilder = window.layer.QueryBuilder.messages().forConversation(job.conversationId)...

I preserve the nice way of using AltContainer with my components and lose all the orchestration plumbing. Right now, I think the resultant clarity is worth the extra back end call.

I also realize how I'd LIKE it to work (in terms of notation), and will ask @goatslacker about, or may take a shot at doing it myself. I'd like to be able to specify the dependency in the exportAsync() method on a store. I'd love to be able to say:

class JobConvesationStore {
    constructor() {
        this.exportAsync(source, JobStore);
    }

The async method JobConversationStore would not be called until the JobStore had its data. The intent is easy to read and no complex action choreography would be needed.

Printmaker answered 20/2, 2016 at 15:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.