Edit on Github

API: React Components #

React components need to be able to access the state of the application that is held within stores and also be able to execute actions that the stores can react to. Since we are not using singletons, we need to provide access to the current request's ComponentContext.

Component Context #

The component context receives limited access to the FluxibleContext so that it can't dispatch directly. It contains the following methods:

  • executeAction(action, payload)
  • getStore(storeConstructor)

It's important to note that executeAction does not allow passing a callback from the component. This enforces that the actions are fire-and-forget and that state changes should only be handled through the Flux flow. You may however provide an app level componentActionErrorHandler function when instantiating Fluxible. This allows you to handle errors (at a high level) spawning from components firing actions.

Providing the Context #

To make the component context available to all your components, you must wrap your top level component in a FluxibleProvider component. FluxibleProvider takes the ComponentContext as prop and will make it available to all children components down the tree:

// App.jsx

import Fluxible from from 'fluxible';
import { FluxibleProvider } from 'fluxible-addons-react';

const fluxibleApp = new Fluxible();

const context = fluxibleApp.createContext();

const componentContext = context.getComponentContext();

const App = () => {
  return (
    <FluxibleProvider context={componentContext}>
      <MyComponent />
    </FluxibleProvider>
  );
};

Another possibility would be to wrap the MyComponent from example above with the higher-order component provideContext:

// MyComponent.jsx

import { provideContext } from 'fluxible-addons-react';

const MyComponent = () => {
    return // ...
};

export default provideContext(MyComponent);

Then, in App.jsx:

// App.jsx

const App = () => {
  return <MyComponent context={componentContext} />;
};

One last possibility, would be to use FluxibleComponent. FluxibleComponent is just a React component that will wrap its children with FluxibleProvider (very similar as the first example from this section):

const App = () => {
  return (
    <FluxibleComponent context={componentContext}>
      <MyComponent />
    </FluxibleComponent>
  );
};

The only difference is that it will also inject the context as prop in MyComponent. FluxibleComponent can considered a legacy API from the times where setting up react context would require setting context types and so on. It has been kept for compatibility reasons with older applications.

Accessing Stores #

It is of course important that your component can access your store state. You also need to make sure that any changes to the store are received by the component so that it can re-render itself. A component that listens to a store for changes without any helpers would look similar to this:

import { FluxibleComponentContext } from 'fluxible-addons-react';
import FooStore from '../stores/FooStore';

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = this.getStoreState();
    }
    getStoreState () {
        return {
            foo: this.context.getStore(FooStore).getFoo()
        }
    }
    componentDidMount () {
        this.context.getStore(FooStore).addChangeListener(this._onStoreChange);
    }
    componentWillUnmount () {
        this.context.getStore(FooStore).removeChangeListener(this._onStoreChange);
    }
    _onStoreChange () {
        this.setState(this.getStoreState());
    }
    render () {...}
}

MyComponent.contextType = FluxibleComponentContext;

To eliminate some of this boilerplate and eliminate potential developer error (for instance forgetting componentWillUnmount), Fluxible provides the following helpers to connect your components to your stores:

Executing Actions #

Executing actions from a component is as simple as requiring the action you want to execute and calling executeAction on the context:

import { FluxibleComponentContext } from 'fluxible-addons-react';
import fooAction from '../actions/fooAction';

class MyComponent extends React.Component {
    onClick () {
        this.context.executeAction(fooAction, { /*payload*/ });
    },
    render () {
        return <button onClick={this.onClick}>Click me</button>;
    }
}

MyComponent.contextType = FluxibleComponentContext;

Testing #

When testing your components, you can use our MockComponentContext library and pass an instance to your component to record the methods that the component calls on the context.

When executeAction is called, it will push an object to the executeActionCalls array. Each object contains an action and payload key.

getStore calls will be proxied to a dispatcher instance, which you can register stores to upon instantiation:

createMockComponentContext({ stores: [MockStore] });

Usage #

Here is an example component test that uses React.TestUtils to render the component into jsdom to test the store integration.

import {createMockComponentContext} from 'fluxible/utils';
import assert from 'assert';
import jsdom from 'jsdom';
import mockery from 'mockery';

// Real store, overridden with MockStore in test
import {BaseStore} from 'fluxible/addons';

class FooStore extends BaseStore {
    // ...
}
FooStore.storeName = 'FooStore';

// Action fired from component, could be overridden using Mockery library
let myAction = function (actionContext, payload, done) {
    var foo = actionContext.getStore(FooStore).getFoo() + payload;
    actionContext.dispatch('FOO', foo);
    done();
};

// the mock FooStore
class MockFooStore extends BaseStore {
    constructor (dispatcher) {
        super(dispatcher);
        this.foo = 'foo';
    }
    handleFoo (payload) {
        this.foo = payload;
        this.emitChange();
    }
    getFoo () {
        return this.foo;
    }
}
MockFooStore.storeName = 'FooStore'; // Matches FooStore.storeName
MockFooStore.handlers = {
    'FOO': 'handleFoo'
};

describe('TestComponent', function () {
    var componentContext;
    var React;
    var ReactTestUtils;
    var provideContext;
    var connectToStores;
    var TestComponent;

    beforeEach(function (done) {
        mockery.enable({
            useCleanCache: true,
            warnOnUnregistered: false
        });
        componentContext = createMockComponentContext({
            stores: [MockFooStore]
        });
        jsdom.env('<html><body></body></html>', [], function (err, window) {
            global.window = window;
            global.document = window.document;
            global.navigator = window.navigator;

            // React must be required after window is set
            React = require('react');
            ReactDOM = require('react-dom');
            ReactTestUtils = require('react-addons-test-utils');
            provideContext = require('fluxible-addons-react/provideContext');
            connectToStores = require('fluxible-addons-react/connectToStores');

            // The component being tested
            TestComponent = class TestComponent extends React.Component {
                render() {
                    return (
                      <button onClick={() => this.context.executeAction(myAction, 'bar')}>
                        {this.props.foo}
                      </button>
                    );
                }
            };
            TestComponent.contextType = FluxibleComponentContext;
            // Wrap with context provider and store connector
            TestComponent = provideContext(connectToStores(TestComponent, [FooStore], function (context, props) {
                return {
                    foo: context.getStore(FooStore).getFoo()
                };
            }));
            done();
        });
    });

    afterEach(function () {
        delete global.window;
        delete global.document;
        delete global.navigator;
        mockery.disable();
    });

    it('should call context executeAction when context provided via React context', function (done) {
        var component = ReactTestUtils.renderIntoDocument(
            <TestComponent context={componentContext} />
        );
        var node = ReactDOM.findDOMNode(component);
        assert.equal('foo', node.innerHTML);
        ReactTestUtils.Simulate.click(node);
        assert.equal('foobar', node.innerHTML);
        ReactTestUtils.Simulate.click(node);
        assert.equal('foobarbar', node.innerHTML);
        done();
    });
});