1.24.0 • Published 6 years ago

react-view-controllers v1.24.0

Weekly downloads
1
License
MIT
Repository
-
Last release
6 years ago

React View Controllers

A state management library for react, based on Mobx

npm version

Installation

npm install react-view-controllers --save

Basic usage example:

Inside NotesListController.js:

import { Controller } from 'react-view-controllers';

export class NotesListController extends Controller {
  constructor(compInstance) {
    super(compInstance);
    this.state = {
      message: 'hello' 
    };
  }
  getMessage() {
    return this.state.message;
  }
  setMessage(value) {
    this.state.message = value;
  }
}

Inside NotesList.jsx:

import React, { Component } from 'react';
import { observer } from 'react-view-controllers';
import { NotesListController } from './NotesListController';

class NotesList extends Component {
  componentWillMount() {
    this.controller = new NotesListController(this);
  }

  render() {
    return (
      <div>
        <div>{this.controller.getMessage()}</div>
        <button onClick={() => this.controller.setMessage('hello world!')}>Click me to change message</button>
      </div>
    );
  }
}

export default observer(NotesList);

Agenda:

  • Data flow is unidirectional- from parent down to the children. A parent cannot fetch data from child controllers.
  • Every 'smart component' should have a controller.
  • A controller is a plain Javascript class and is not tightly coupled to any view.
  • The controller holds a state and methods for manipulating the state.
  • The controller's lifecycle should be bounded to the component's lifecycle. when a component enters the screen, A new fresh controller will be created, and when the component is destroyed the controller will be destroyed.
  • A component can get and set data from parents' controllers (not necessarily direct parents), but it cannot use data from the controllers of sibling components.
  • Every controller should be explicitly exposed (provided) in order to be used by child components.
  • Any changes to the controller state will be automatically reflected by the view.

Why

  • Zero boilerplate: Controllers are just plain javascript classes, and All you need to do in order to make your views react to changes in the controllers, is just to wrap them with observer and you are good to go.

  • No need for singleton stores: If you ever used Redux, you probably knows what happens when you forget to clean your stores when a component leave the screen- the next time it enters the screen, it fetches some old state-related data from the store and bad things happens. You may say that stores should not contain state related data, but sometimes you just need to share state across multiple components, for example, currentSelectedItem, isCartFull, canMoveToNextStep etc. Controllers lifecycle is binded to the component lifecycle, so you get a fresh controller out of the box whenever a component enters the screen.

  • Reusability: Each component holds an instance of it's Controller (again, no singletons!), so you can create multiple instances of a component (see example project). When you have a singleton store its much more cumbersome to support multiple instance of a component.

  • Better encapsulation: A component can fetch data only from it's direct controller and it's parents controllers. You cannot feth data from sibling component's Controllers. If you need some piece of data to be visible for two sibling components, it means that this data should sit within their first common parent. If you need a piece of data to be visible to all other component, put it in your AppController.

  • Mimics React's basic state (native componet's state): React state is really good for dumb components, but its not so good for smart components. It cannot be esaly shared to deep nested children, it prevents separation of logic and view to different files, and it is not easly testable. React view controllers tackles exactly those points.

How

Most of the heavy lifting is being done behind the scenes with the help of Mobx.

Example project

Here is a Simple example project You can see the source code under the example folder: example/src If you want to run it locally: After cloning the repository, nevigate to the example folder and type in your terminal:

npm install
npm start

Api

Controller(componentInstance)

Every view should have a controller that extends Controller. A controller is a plain javascript class that holds an observable state. a controller should contain only a state and methods that manipulate the state. Make sure to call super(componentInstance) from your controller constructor. Every controller exposes getParentController() (See bellow for more details).

  • state:

    Every controller has a state prop. You should initiate the state inside the controller's constructor. The observers (React Components that you wrapped within observer) will react to any change in the state, even changes of deep nested properties. for example:
changeName(){
  this.state.listOfItems[0].name = 'foo';
}
  • getParentController(controllerName: string):

    Use this Controller method when you need to fetch data from a parent controller (not necessarily a direct parent). The name of the parent controller is the name of the class, as returned from Class.name. If for example your code looks like this:
class SomeParentController extends Controller{}

then the name will be 'SomeParentController' (SomeParentController.name). Make sure that the parent controller is provided using ProvideController. You cannot get the controller of a sibling component. If you need to use some data from a sibling component, put this data in the first common parent of the two components.

If you need to interact with a parent controller from your React component, you can do something like this:

 componentWillMount() {
    this.controller = new SomeChild(this);
    this.parentController = this.controller.getParentController(SomeParentController.name);
  }

Or directly:

 componentWillMount() {
    this.parentController = new Controller(this).getParentController(SomeParentController.name);
  }

Usage example:

import {Controller} from 'react-view-controllers';
import {SomeParentController} from './SomeParentController';

export class AppController extends Controller {
  constructor(comp) {
    super(comp);
    this.state = {totalNotesCount: 2}; //the state should be the only property of the controller, 
                                       //and should be initialized in the constructor.
  }

  getTotalNotesCount() {
    return this.state.totalNotesCount;
  }

  increaseCounter() {
    this.state.totalNotesCount ++;
  }
  
  getSomePropFromParentController() {
  const someProp = super.getParentController(SomeParentController.name); //you can use the name of the controller as string,                                                                          //but this way is safer.
  //do something with someProp...
  }
}

Your React component will create an instance of the Controller inside componentWillMount like this:

import {AppController} from 'react-view-controllers';

class App extends React.Component {
   componentWillMount() {
    this.controller = new AppController(this);
   }
}

observer(ReactComponent)

To become reactive, every React component that uses a controller should be wrapped within observer.

import {observer} from 'react-view-controllers';

class SomeSmartComponent extends React.Component {
...
}

export default observer(SomeSmartComponent)

<ProvideController controller={controllerInstance}/>:

If you want your controller instance to be visible to your child components, you must explicitly provide it using ProvideController.

import * as React from 'react';
import SomeParentComponentController from './SomeParentComponentController';
import { observer, ProvideController } from 'react-view-controllers';

class SomeParentComponent extends React.Component {
  componentWillMount() {
    this.controller = new SomeParentComponentController(this);
  }

  render() {
    return (
        <ProvideController controller={this.controller}>
           <SomeChild>
           <AnotherChild>
        </ProvideController>
    );
  }
}

In the above example, SomeChild and AnotherChild could make use of SomeParentComponentController using getParentController().

Testing Api

TestUtils.init():

You must call this method before using any other test utils.

TestUtils.clean():

You must call this method after each test (if you used TestUtils.init());

import {TestUtils} from 'react-view-controllers';

beforeEach(() => {
   TestUtils.init();
 });
 
 afterEach(() => {
   TestUtils.clean();
 });

getControllerOf(componentInstance):

use this method to extract component's controller for testing.

mockStateOf(controllerInstance: controller, state: object):

use this method when you need to mock a state of a controller.

  it('some Test', () => {
    TestUtils.init();
    const component = mount(<Test />);
    const controller = TestUtils.getControllerOf(component.instance());
    TestUtils.mockStateOf(controller,{ name: 'mockedName' });
    expect(component.find('[data-hook="name"]').text()).toEqual('mockedName');
  });

mockParentOf(controllerName: string, ParentController: Controller, parentState?: object):

Use this method if you need to mock parent controllers of component.

  • controllerName: The name of the controller which his parent needs to be mocked
  • ParentController The parent controller class that will be mocked.
  • parentState The state of the mocekd parent.
  beforeEach(() => {
    TestUtils.init();
    TestUtils.mockParentOf(NotesListController.name, AppController);    
  });
1.24.0

6 years ago

1.23.0

6 years ago

1.22.0

6 years ago

1.21.0

6 years ago

1.20.0

6 years ago

1.19.0

6 years ago

1.18.0

6 years ago

1.17.0

6 years ago

1.16.0

6 years ago

1.15.0

6 years ago

1.14.0

6 years ago

1.13.0

6 years ago

1.12.0

6 years ago

1.11.0

6 years ago

1.10.0

6 years ago

1.9.0

6 years ago

1.8.0

6 years ago

1.7.0

6 years ago

1.6.0

6 years ago

1.5.0

6 years ago

1.4.0

6 years ago

1.3.0

6 years ago

1.2.0

6 years ago

1.1.0

6 years ago

1.0.0

6 years ago