inferno-mobx
    TypeScript icon, indicating that this package has built-in type declarations

    8.0.1 • Public • Published

    inferno-mobx

    This is a fork of mobx-react for Inferno

    The module is compatible with Inferno v1+, for older versions use mobx-inferno

    This package provides the bindings for MobX and Inferno. Exports observer and inject decorators, a Provider and some development utilities.

    New: exports observerPatch, a function to turn Component classes into MobX observers. observerPatch is implemented in a better manner, but is separate as it would break compatibility in some cases.

    New: exports observerWrap, a function to turn functional Components into MobX observers. Unlike observer, observerWrap does not wrap them in a class Component. This allows the base Component and the observer to be interchangable.

    Install

    npm install --save inferno-mobx
    

    Also install mobx dependency (required) if you don't already have it

    npm install --save mobx
    

    observerPatch Examples

    Pass a class Component to observerPatch to have in automatically re-render if MobX observables read by render are modified.

    // MyComponent.tsx (also works with plain JavaScript)
    import { Component } from 'inferno';
    import { observerPatch } from 'inferno-mobx';
    
    interface CountStore {
        readonly count: number
    }
    
    export class MyComponentA extends Component<{ countStore: CountStore }> {
        render({ countStore }: { countStore: CountStore }) {
            return (<p>Current Count: {countStore.count.toString()}</p>);
        }
    }
    
    observerPatch(MyComponentA);
    
    // Or you can use functions that read from stores instead of the stores
    export class MyComponentB extends Component<{ count: () => number }> {
        render({ count }: { count: () => number }) {
            return (<p>Current Count: {count().toString()}</p>);
        }
    }
    
    observerPatch(MyComponentB);
    
    // However, passing a value from an observable directly as a property will NOT work!
    export class MyComponentC extends Component<{ count: number }> {
        render({ count }: { count: number }) {
            return (<p>Current Count: {count.toString()}</p>);
        }
    }
    
    observerPatch(MyComponentC);
    
    function MyComponentF({ count }: { count: () => number }) {
        return (<p>Current Count: {count().toString()}</p>);
    }
    
    // Detection does NOT cross component boundaries. So this does NOT work:
    export class MyComponentD extends Component<{ countStore: CountStore }> {
        render({ countStore }: { countStore: CountStore }) {
            return (<div className="fancy">
                <MyComponentF count={() => countStore.count} />
            </div>);
        }
    }
    
    observerPatch(MyComponentD);
    
    // You can use simple functional components as functions:
    export class MyComponentE extends Component<{ countStore: CountStore }> {
        render({ countStore }: { countStore: CountStore }) {
            // But keep in mind that the whole component will re-render.
            return (<div className="fancy">{
                MyComponentF({count: () => countStore.count})
            }</div>);
        }
    }
    
    observerPatch(MyComponentE);
    
    // Only Components that depend on MobX observables need to be observers.
    export class MyComponentG extends Component<{ countStore: CountStore }> {
        render({ countStore }: { countStore: CountStore }) {
            // MyComponentB is an observer and will re-render when countStore.count changes.
            return (<div className="fancy">
                <MyComponentB count={() => countStore.count} />
            </div>);
        }
    }
    
    // observerPatch(MyComponentG) is not needed and would add overhead for no reason.
    
    // If you want both an observer and a non observer versions of a component,
    // then you can just extend the non observer and patch the sub class.
    export class MyComponentH extends Component<{ count: () => number }> { // non observer base class
        render({ count }: { count: () => number }) {
            return (<p>Current Count: {count().toString()}</p>);
        }
    }
    
    export class MyComponentI extends MyComponentH {} // sub class indended to be an observer
    observerPatch(MyComponentI); // make the sub class an observer
    
    // DO NOT extend from Components that are obsevers.
    // If you do have reason to extend a Component class that will be an observer,
    // see above on how to easily have both an obsever and a non-observer version.
    export class MyComponentJ extends MyComponentA {
        render(properties: { countStore: CountStore }) {
            return (<div className="fancy">
                {super.render(properties)}
            </div>);
        }
    }
    
    // Even if you do not call observerPatch on the sub class, extending
    // from observers can create problems.
    observerPatch(MyComponentJ);
    
    // DO NOT call observerPatch more than once on a clase.
    export class MyComponentK extends Component<{ countStore: CountStore }> {
        render({ countStore }: { countStore: CountStore }) {
            return (<p>Current Count: {countStore.count.toString()}</p>);
        }
    }
    
    observerPatch(MyComponentK);
    observerPatch(MyComponentK); // NEVER call more than once per class!
    // index.tsx
    import {
        MyComponentA,
        MyComponentB,
        MyComponentC,
        MyComponentD,
        MyComponentE,
        MyComponentG,
        MyComponentH,
        MyComponentI,
        MyComponentJ,
        MyComponentK
    } from './MyComponent';
    import { render } from 'inferno';
    import { action, observable } from 'mobx';
    
    const store = observable({ count: 0 });
    
    render(<div>
      <MyComponentA countStore={store} />
      <MyComponentB count={() => store.count} />
      <MyComponentC count={store.count} /> {/* This component WILL NOT detect when count changes! */}
      <MyComponentD countStore={store} /> {/* This component WILL NOT detect when count changes! */}
      <MyComponentE countStore={store} />
      <MyComponentG countStore={store} />
      <MyComponentH count={() => store.count} /> {/* Not an observer so no updating when count changes. */}
      <MyComponentI count={() => store.count} /> {/* Is an observer so it will update. */}
      <MyComponentJ countStore={store} /> {/* Works... BUT! when it unmounts there will be an error! */}
      <MyComponentK countStore={store} /> {/* Works... BUT! when it unmounts there will be an error! */}
      <button type="button" onClick={action(() => {store.count += 1})}>Click Me</button>
    </div>, document.getElementById('components'));

    NOTES:

    observerPatch installs a componentWillUnmount hook to dispose of the MobX Reaction. It will then call the componentWillUnmount from the class's prototype. If you dynamically add a componentWillUnmount to a class you pass to observerPatch, be sure it calls the hook installed by observerPatch.

    observerPatch also replaces the render method in the prototype. The render method gets replaced twice on a per instance basis during the life cycle of a Component. It is replaced the first time render is called with a version that is wrapped in a MobX reaction. Then it is restored to the class's original render method when componentWillUnmount is called.

    It is strongly recommended to avoid replacing the render method on classes observerPatch is applied to.

    observerWrap Examples

    observerWrap is the companion to observerPatch, but for functional Components.

    // MyComponent.tsx (also works with plain JavaScript)
    import { observerWrap } from 'inferno-mobx';
    
    interface CountStore {
        readonly count: number
    }
    
    // For a good debugging experience, have named functions
    function MyComponentA({ countStore }: { countStore: CountStore }) {
        return (<p>Current Count: {countStore.count.toString()}</p>);
    }
    
    // If you prefer arrow functions, assign them to a const before passing to observerWrap
    const MyComponentB = ({ countStore }: { countStore: CountStore }) => {
        return (<p>Current Count: {countStore.count.toString()}</p>);
    };
    
    // This works, but if you need to debug the MobX reactions things will be less nice
    const MyComponentC = observerWrap(({ countStore }: { countStore: CountStore }) => {
        return (<p>Current Count: {countStore.count.toString()}</p>);
    });
    
    const MyObserverB = observerWrap(MyComponentB);
    
    // double wrapping does not cause errors, but is inefficient
    // a warning will be logged for non-production builds
    const MyComponentD = observerWrap(MyComponentC);
    
    const MyComponentE = observerWrap(MyComponentA);
    
    // calling observerWrap on the same component multiple times is fine
    const MyComponentF = observerWrap(MyComponentA);
    
    // but storing the result of the first call for reuse is still better
    // as each wrapper will take memory
    const MyComponentG = MyComponentE;
    
    export {
        // wrapping separately allow the non-observer component to be used
        MyComponentA, // not an observer
        MyObserverB as MyComponentB, // export observer under name of the component
        MyComponentC,
        MyComponentD,
        MyComponentE, // observer version of MyComponentA
        MyComponentF,
        MyComponentG
    }

    Everything mentioned for observerPatch about passing you observables to the components applies to observerWrap.

    As a note, the function returned by observerWrap should only be used for constructing Inferno VNodes. That means JSX, createElement, inferno-hyperscript, or createComponentVNode. For any other usage, you should use the base function directly.

    // index.tsx
    import {
        MyComponentA,
        MyComponentB,
        MyComponentC,
        MyComponentD,
        MyComponentE,
        MyComponentF,
        MyComponentG
    } from './MyComponent';
    import { render } from 'inferno';
    import { action, observable } from 'mobx';
    
    const store = observable({ count: 0 });
    
    // All but MyComponentA will update when count changes
    // MyComponentA was not wrapped
    render(<div>
      <MyComponentA countStore={store} />
      <MyComponentB countStore={store} />
      <MyComponentC countStore={store} />
      <MyComponentD countStore={store} />
      <MyComponentE countStore={store} />
      <MyComponentF countStore={store} />
      <MyComponentG countStore={store} />
      <button type="button" onClick={action(() => {store.count += 1})}>Click Me</button>
    </div>, document.getElementById('components'));

    There are a few things to say about defaultHooks and defaultProps.

    import { observerWrap } from 'inferno-mobx';
    
    // For this first component, both the base function and the wrapper will share default hooks and props
    function MyComponentA(props) { /* ... */ }
    MyComponentA.defaultHooks = { /* ... */ }
    MyComponentA.defaultProps = { /* ... */ }
    const MyObserverA = observerWrap(MyComponentA);
    // MyComponentA.defaultHooks === MyObserverA.defaultHooks &&
    // MyComponentA.defaultProps === MyObserverA.defaultProps
    
    // So the following would affect the defaultHooks for both as they refer to the same object:
    MyComponentA.defaultHooks.onComponentShouldUpdate = /* hook */;
    
    // But settting a new object only affects the one being set:
    MyObserverA.defaultProps = { /* ... */ } // Now MyComponentA.defaultProps !== MyObserverA.defaultProps
    
    // If the defaults are set after the wrapper is made, the observer will not have default Hooks nor Props
    function MyComponentB(props) { /* ... */ }
    const MyObserverB = observerWrap(MyComponentB);
    MyComponentB.defaultHooks = { /* ... */ }
    MyComponentB.defaultProps = { /* ... */ }
    // MyObserverB.defaultHooks === undefined && MyObserverB.defaultProps === undefined
    
    // Setting defaults on the observer does not affect the base component
    function MyComponentC(props) { /* ... */ }
    const MyObserverC = observerWrap(MyComponentC);
    MyObserverC.defaultHooks = { /* ... */ }
    MyObserverC.defaultProps = { /* ... */ }
    // MyComponentC.defaultHooks === undefined && MyComponentC.defaultProps === undefined

    Generally the first example is what you want. The third is fine if you only use the observer.

    The most like case you would want the defaults to be different is for the onComponentShouldUpdate hook. This is because the re-rendering of the observer is more expensive than the base component. That does not mean you always want onComponentShouldUpdate for observers and never to base components. But it can be worth having on observers even if it is not for the base.

    Note: The MobX Reaction will call onComponentWillUpdate and onComponentDidUpdate when re-rendering, if present. When called by the Reaction the 'previous' and 'next' parameters will be passed the same object.

    Warning: The onComponentWillUpdate and onComponentDidUpdate are bound when the component is rendered by Inferno. The onComponentShouldUpdate can prevent the binding from being updated if new callbacks are assigned.

    To have a default hook (or property) on just the observer, you need to create a new object that is a copy of the base version. And then add the new hook or property.

    function MyComponent(props) { /* ... */ }
    MyComponent.defaultHooks = { /* ... */ }
    MyComponent.defaultProps = { /* ... */ }
    
    const MyObserver = observerWrap(MyComponent);
    
    MyObserver.defaultHooks = {
        ...MyComponent.defaultHooks, // copy original
        onComponentShouldUpdate: (prev, next) => { /* ... */ }
    }

    Legacy Example

    You can inject props using the following syntax ( This example requires, babel decorators-legacy plugin )

    // MyComponent.js
    import { Component } from 'inferno';
    import { inject, observer } from 'inferno-mobx';
    
    @inject('englishStore', 'frenchStore') @observer
    class MyComponent extends Component {
        render({ englishStore, frenchStore }) {
            return <div>
                <p>{ englishStore.title }</p>
                <p>{ frenchStore.title }</p>
            </div>
        }
    }
    
    export default MyComponent

    If you're not using decorators, you can do this instead:

    // MyComponent.js
    import { Component } from 'inferno';
    import { inject, observer } from 'inferno-mobx';
    
    class MyComponent extends Component {
        render({ englishStore, frenchStore }) {
            return <div>
                <p>{ englishStore.title }</p>
                <p>{ frenchStore.title }</p>
            </div>
        }
    }
    
    export default inject('englishStore', 'frenchStore')(observer(MyComponent));

    Just make sure that you provided your stores using the Provider. Ex:

    // index.js
    import { render } from 'inferno';
    import { Provider } from 'inferno-mobx';
    import { observable } from 'mobx';
    import MyComponent from './MyComponent';
    
    const englishStore = observable({
        title: 'Hello World'
    });
    
    const frenchStore = observable({
        title: 'Bonjour tout le monde'
    });
    
    render(<Provider englishStore={ englishStore } frenchStore={ frenchStore }>
        <MyComponent/>
    </Provider>, document.getElementById('root'));

    Migrating from observer to observerPatch

    The observerPatch was added because the way observer was implemented cannot work on class Components that implement either getSnapshotBeforeUpdate or getDerivedStateFromProps. Having differences in implementation that can matter to user code based on which lifecycle hooks are present is not good design. Changing how observer is implemented in ways that could break existing user code is not worth the cost. Furthermore, observerPatch provides better performance in the resulting class than observer.

    The differences to be aware of when switching from observer to observerPatch are:

    1. observerPatch is not implemented to be used as a decorator
    2. observerPatch returns void instead of returning the class it was applied to
    3. observerPatch will not have the observer call this.componentWillReact() if such a member exists
    4. observerPatch does not add a shouldComponentUpdate hook to classes that do not have one
    5. observerPatch will not forward exceptions thrown by render to errorsReporter
    6. observerPatch ignores useStaticRendering(true)
    7. observerPatch will not emit events through renderReporter that list how long render took
    8. observerPatch does not make this.props nor this.state observable
    9. observerPatch does not set isMobXReactObserver = true as a static class member
    10. observerWrap returns a functional Component, where observer returns a class Component.

    Points 1 and 2 are a simple change to call observerPatch after the class is defined and removing observer.

    To replicate the behavior of observer for point 3, call this.componentWillReact() at the start of your componentWillMount hook. Or if your Component does not have a componentWillMount, rename componentWillReact to componentWillMount.

    For point 4, you can implement your own shouldComponentUpdate hook is you want to prevent needless re-renders. The shouldComponentUpdate does not affect re-renders triggered by MobX obervables being modified. So it exists for when new properties are set or this.setState is used.

    For point 5, you can intercept the exceptions in your render method and send them to a handler if desired.

    For point 6, all your components other than those passed to observer already ignore it. If there is demand, generating warning messages might return useStaticRendering(true) is called. But it would only be in development builds of Inferno and would not prevent methods from running.

    For point 7, you can do it better that observer did. Also, it only did so if you toggled it on by calling trackComponents().

    Point 8 means that if you directly set this.props or this.state to a new value it will not trigget a re-render. You should not be directly setting this.props. Let Inferno update Component properties. You should not be directly setting this.state outside of the componentWillMount and componentWillReceiveProps hooks. The component will always have render called after componentWillMount. The component will have render called after componentWillReceiveProps unless shouldComponentUpdate returns false. Any time Inferno updates this.props or this.setState is used to update state, the component will re-render unless shouldComponentUpdate returns false.

    For point 9, observerPatch instead sets isMobXInfernoObserver = true as a static member of the class. But it only does do in development builds as it is intended for use internally for sanity checks. The issues it is used to spot and warn about should be fixed before reaching production.

    Finally for point 10, if you specify the Component VNode type to optimize renders, you will need to change the flags.

    If this seems intimidating, know that nearly all unit tests for observer worked as they were after replacing observer with observerPatch. Outside of needing to switch functional Components to class Components, that is.

    Using observerPatch with Provider and inject

    The observerPatch function can be used with Provider and inject just like with observer. However, using inject as a decorator along side observerPatch is not supported.

    // MyInjected.tsx
    import { Component } from 'inferno';
    import { observerPatch, observerWrap, inject } from 'inferno-mobx';
    
    interface CountStore {
        readonly count: number
    }
    
    // The class produced by inject will require the injected properties if required by the base class
    class MyComponentA extends Component<{ countStore?: CountStore }> {
        render({ countStore }: { countStore?: CountStore }) {
            // If only the injected version will be used, casting is safe as an exception is thrown
            // if the property is unavailable
            const count = (countStore as CountStore).count.toString(); // unsafe if MyComponentA was exported
            return (<p>Current Count: {count}</p>);
        }
    }
    
    // Recommended order
    observerPatch(MyComponentA);
    export const MyInjectedA = inject('countStore')(MyComponentA);
    
    class MyComponentB extends Component<{ countStore?: CountStore }> {
        render({ countStore }: { countStore?: CountStore }) {
            const count = (countStore as CountStore).count.toString();
            return (<p>Current Count: {count}</p>);
        }
    }
    
    // The order of inject and observerPatch does not matter.
    export const MyInjectedB = inject('countStore')(MyComponentB);
    observerPatch(MyComponentB); // Works, but this order is more prone to the mistake shown below
    
    class MyComponentC extends Component<{ countStore?: CountStore }> {
        render({ countStore }: { countStore?: CountStore }) {
            const count = (countStore as CountStore).count.toString();
            return (<p>Current Count: {count}</p>);
        }
    }
    
    // Be sure to use observerPatch on the class, not the injected class.
    // A warning message is output if you do this.
    export const MyInjectedC = inject('countStore')(MyComponentC);
    observerPatch(MyInjectedC); // WRONG! Should be: observerPatch(MyComponentC);
    //Having observerPatch before inject lets tools detect this issue.
    
    // Functional components with observerWrap
    function MyComponentD({ countStore }: { countStore?: CountStore }) {
        const count = (countStore as CountStore).count.toString();
        return (<p>Current Count: {count}</p>);
    }
    export const MyInjectedD = inject(observerWrap(MyComponentD));
    // index.tsx
    import {
        MyInjectedA,
        MyInjectedB,
        MyInjectedC,
        MyInjectedD
    } from './MyInjected';
    import { render } from 'inferno';
    import { Provider } from 'inferno-mobx';
    import { action, observable } from 'mobx';
    
    const store = observable({ count: 0 });
    
    const store2 = observable({ count: 0 });
    
    // NOTE: Do not use Provider and inject for trivial cases like this in real code.
    render(<div>
        <Provider countStore={store}>
            <MyInjectedA />
            <MyInjectedB />
            <MyInjectedC /> {/* This one will not update as MyComponentC was not made into an observer. */}
            <MyInjectedD />
            <MyInjectedA countStore={store2} /> {/* Will not update as direct properties override injection */}
        </Provider>
      <button type="button" onClick={action(() => {store.count += 1})}>Click Me</button>
    </div>, document.getElementById('root'));

    IMPORTANT: The values injected are the ones available to Provider when it is first mounted. So Provider and inject are only useful for properties that will NEVER change.

    Server Side Rendering and Hydration

    When rendering server side, components only need to be rendered once and does not update. This means you do not need your components to act as observers when running on the server.

    Furthermore, the functions provided by inferno-server initialize components but does not call their unmount hooks. Which for observers means their internal MobX reactions are not disposed.

    So for your server code components should not act as observers.

    The best way to achieve this is to have calls to observer and observerPatch be conditional and not called server side.

    // MyComponent.tsx
    import { Component } from 'inferno';
    import { observerPatch, observerWrap } from 'inferno-mobx';
    
    interface CountStore {
        readonly count: number
    }
    
    export class MyComponent extends Component<{ countStore: CountStore }> {
        render({ countStore }: { countStore: CountStore }) {
            return (<p>Current Count: {countStore.count.toString()}</p>);
        }
    }
    
    function MyFunctional({ countStore }: { countStore: CountStore }) {
        return (<p>Current Count: {countStore.count.toString()}</p>);
    }
    
    // You do not have to use an environment variable nor is the specific one used important.
    // The idea would be to replace 'process.env.SERVER_SIDE' with a literal true or false.
    // This can be done when transpiling with Babel or many bundling tools have plugins that can do so.
    // Then a good minifier such a terser can elimitate the if statement for production.
    if (process.env.SERVER_SIDE !== true) {
        observerPatch(MyComponent); // The same goes can be done for 'observer' calls.
    }
    
    // For functional components
    const MyObserver = process.env.SERVER_SIDE !== true ? observerWrap(MyFunctional) : MyFunctional;
    export {MyObserver as MyFunctional};

    Then you can use renderToString and hydrate as you would with any other component.

    If you use observer instead of observerPatch you can alternatively call useStaticRendering(true) in your server code. The useStaticRendering function is exported by inferno-mobx and only needs to be called once before any rendering occurs. When called with true, it will make the components observer skip the code that turns them into observers. However, simply not calling observer server side will yield better performance.

    If you use inject to supply properties in you render code, you still need to call it on the server side.

    When a custom function to get stores is passed to inject, it internally calls observer on the created class. As such if you are using a custom function to get stores for inject you need to call useStaticRendering(true) in your server side code.

    Install

    npm i inferno-mobx

    DownloadsWeekly Downloads

    264

    Version

    8.0.1

    License

    MIT

    Unpacked Size

    164 kB

    Total Files

    18

    Last publish

    Collaborators

    • longlho
    • nightwolfz
    • lukesheard
    • trueadm
    • havunen