Using Flux for component states is an anti-pattern!

Reading Time: 5 minutes

Currently, I am refactoring a mid-size application which I started more than a year ago (additionally, to my daily workload). The project is a ReactJS application consisting of more than 120 components already. I started developing without Flux (mainly, due to the fact that I didn't understand the need for Flux initially). In the meanwhile, I gained a deeper knowledge of Flux and decided to go for it in that project. I chose nanoflux as a Flux implementation, my very own implementation. While I think that Flux is - once understood - a very simple concept I discovered that one challenge is to decide what shall be treated by Flux and what not - By far, not everything that is a state is worth being shifted towards Flux.

I developed nanoflux as kind of training to better understand Flux, but it turns into something more mature. At the moment of writing, it is has reached RC state, and the mentioned project is to prove its readyness for production.

The case

Recently, I came up with the idea to refactor a suggestion input field (that one where appear a list of suggested items as a dropdown while typing) and I concluded that applying Flux on that component brings no advantage at all. Even worse it scatters the component in parts that logically belong together. But let's go there step by step. Here's the component as it would be used within other components:

<SuggestionInput id="lojas" 
	restService={new GenericService("/loja")} 
	projection={this.projectionLoja} 
	title="Acesso Loja" 
	onSelected={this.handleLojaSelection} 
	placeholder="Digitar loja" />

At its core the component receives the a service, which fetches the suggestion entities from server, apply the mapping function passed as projection property, and returns a selected result to the callback function passed as onSelected property. So, everytime when the text in the internal input field changes the service is called (in fact, the call is debounced, and calls only each second). All logic and state is put and handled inside the component. Usually, it is said that states should be handled by Flux...but I will show the implications if we dumbly follow this recommendation.

Applying Flux

Ok. I started to apply Flux and began with the ActionProvider, where I put - following best practices - the Web API call to fetch the suggestions. Then I created the Store, where I manage the suggestion state. Here is how both Flux Components were implemented:

 

var Immutable = require('immutable');

_suggestions = {};
// the project uses requirejs -- think of it as module.exports
return {
	// get the suggestions for specific component ... wtf?
	getSuggestions : function(componentId){
		return _suggestions[componentId];
	},
	// called on action dispatch -- nanoflux uses a convention based auto-mapping
	onFetchSuggestions : function(data){
		_suggestions[data.componentId] = Immutable(data.suggestions);
		this.notify();
	}
}

// the action provider
var $config = require('app/config');
var GenericService = require('restservice/generic.service');
// the project uses requirejs -- think of it as module.exports
return {
	fetchSuggestions: function(componentId, url, searchTerm, projectionFunc){

		var suggestionService = new GenericService(url);
		suggestionService.getAll({
				textFilter: searchTerm,
				pagina: 1,
				numeroRegistros: $config.itemsPerSuggestion
			})
			.then(function (suggestions) {
				var projected = suggestions.map(function(suggestion){
					return projectionFunc(suggestion);
				});
				// fetchSuggestions maps automatically to onFetchSuggestion
				this.dispatch('fetchSuggestions', {
					componentId : componentId, // Hu, I need to manage the component instance!?
					suggestions : projected
				});
			}.bind(this))
	}
}
});

Even if it is not very much code, it is quite verbose. But applying Flux always leads to a small overhead (or considerable larger overhead, if you use Facebook's Flux implementation -- another reason why I created nanoflux). But that's not the point...I would like to draw attention to the fact that it is necessary to manage the state for each instance of my component. This makes me feel icky, because the components are exactly made for this...to isolate behavior per instance. The store is a singleton, so on notify all listening instances will be triggered, although it is entirely unnecessary. Why the hell I should break the implicit component behavior and reinvent (in a crappy way) the object management in the store!?

Here is an excerpt from the suggestion component using Flux, just to complete the Flux picture - don't use this code, it's not recommended!

onSuggestionsLoaded : function(){
	/// get *only* my suggestions, because this function is called for every mounted suggestion instance!!!
	this.setState({suggestions: this.componentStore.getSuggestions(this.state.componentId)});
},

getInitialState : function(){
	this.componentActions = NanoFlux.getActions('componentActions');
	this.componentStore = NanoFlux.getStore('componentStore');

	return {
		suggestions : [],
		value: this.props.defaultValue ,
		selectedIndex: 0,
		subscription: null,
		componentId : _.uniqueId('suggestion_') /// WTF, I need the id to manage it *outside* my component!!!
	}
},
componentWillMount : function(){
	this.state.subscription = this.componentStore.subscribe(this, this.onSuggestionsLoaded);
},
componentDidMount : function(){
	$event.addListener("clear-suggestion-" + this.props.id, this.reset);
},
componentWillUnmount : function(){
	$event.removeListener("clear-suggestion-" + this.props.id, this.reset);
	this.state.subscription.unsubscribe();
},

onChange : function(event){
	this.setState({value :event.target.value });

	var searchTerm = event.target.value.trim();
	if(searchTerm===''){
		this.setState({suggestions: []});
		return;
	}

	// trigger only every second
	_.debounce(function(){
		this.componentActions.fetchSuggestions(this.state.componentId, this.props.url,searchTerm, this.props.projection );
	}.bind(this), 1000)();
},

 

Conclusion

Using Flux for component states is an anti-PatternI came up with the conclusion, that some have to pay attention on which state should be managed by Flux and which not. In the demonstrated case the Flux application leads not only to code overhead, but also to performance overhead. Beside that, the Suggestion Input wouldn't be reusable without its Action Provider and Store. Applying Flux in such a case breaks everything that is resolved already by the component-oriented approach. I go that far to say, that everytime you coming up managing component instances (p.e. by passing instance identifier to the action)  in Flux, something is wrong, because you break a components isolated behavior. Flux is about application state, ...using Flux for component states is an anti-pattern!

 

Facebooktwittergoogle_plusredditpinterestlinkedin

4 comments

  1. I find that the lines may get muddled... Something that should reset if the page is reloaded should not go through redux/flux. This includes modals for a calendar picker, search boxes, like in tfa, etc.

    However, there are interactions, and states that should be part of the application state. A better example, even though it is per-control, would be form/control validation state... was a control focused, if so, then it should show a validated state, otherwise, it shouldn't until a submit is attempted.

    There are other controls that may very well go either way. It really depends on your need. I can see the desire to separate the control state from application state, but really, it should be a matter of the guiding parent's feature that controls this... the containing feature's form/screen should have its' own data state, and that state should guid the lookup through a determined service. Then you can still have structured components, but then separate the logic not to a generic control provider, but the parent becoming responsible.

    If you break apart by feature, and delegate responsibility of the control's data and events to a parent, then that parent can control these bits in a generic way that fits said parent.

    1. I am not sure if understand you correctly...the point is, that states have to be treated differently. As you said, "there are interactions, and states that should be part of the application state" - I think the difficulty is to figure out whether a state should be handled by Flux or not. But when you need to keep track of a components Id in Flux, then it is very likely that this state would better be handled inside the component (or its parent).

      1. I am not Michael, but I think I know what he alludes to.

        What I usually do, is to create a component with props only, delegating control of state to it's parent component. This component is essentially a dumb stateless view.
        The parent component can then either hold state locally (usually the first I make) or proxy the state handling onto a (global app state) flux-implementation.

        This means the first time you need state, you just use a parent component with local state. When you later find you need to share this state (or have it persisted through the route) you can then switch the parent component with a flux-based one.

        As a side-benefit, the dumb child component is usually pretty easy to do tests on.

        1. Yes, this is definitely the way to go.
          The Suggestion Input example is IMHO a case, where it's not favorable to use Flux.
          I think that Flux should not be used in cases, where it is necessary to identify component instances to get its state, i.e. using Ids.

          Thx, I appreciate your feedbacks.

Leave a Reply

Your email address will not be published. Required fields are marked *