Objective Redux

Redux made better, objectively.

State Controllers

The StateControllers Class

What is it?

A state controller defines a slice of your applications state. For instance, if you want to share information between components, a StateController is the object that will manage that shared piece of state.

How does it work?

This is an abstraction of Redux's reducers, actions, and selectors. It defines a slice of the state in the Redux store and automatically sets up up the reducer that manages its mutations. It is initialized on-demand only when it's needed to fire an action or respond to a state change.

Defining a StateController

A StateController instance needs two things: a constructor and a getName function.

Constructor

The construction must call super with the initial state.


import { StateController } from 'objective-redux';

const initialState = { isOn: false };

class SwitchStateController extends StateController {
  // The constructor should look like this
  constructor() {
    super(initialState);
    // ... additional setup code can go here
  }
}

export SwitchStateController;

getName

The static getName method should returns the string name of the state slice. The name it returns will need to be unique for all of the Objective Redux controllers.1 This allows Objective Redux to lazy-load controllers on-the-fly and prevents reducer collisions in the store.


import { StateController } from 'objective-redux';

const initialState = { isOn: false };

class SwitchStateController extends StateController {
  constructor() {
    super(initialState);
  }

  // The getName function must look like this
  static getName() {
    // The name returned must be unique
    return 'switch';
  }
  
  // ... our actions will go here ...
}

export SwitchStateController;

Defining Actions

Actions and their associated mutations are created by using the createReducingAction method. This method takes a function of the form (state, payload) => nextState. That is, it accepts the current state and the action payload as parameters and returns the new state. When the action is called, the mutation function will run and the state will be updated.

Below is an example of a state controller that has two actions: one that toggles a switch on and off, one that sets the switch to a value.


import { StateController } from 'objective-redux';

const initialState = { isOn: false };

class SwitchStateController extends StateController {
  constructor() {
    super(initialState);
  }

  static getName() {
    return 'switch';
  }

  // Our actions:

  // When we call controller.toggleSwitch(), isOn will be inverted
  toggleSwitch = this.createReducingAction(
    (state) => ({ isOn: !state.isOn })
  );

  // When we call controller.setSwitch(true), isOn will be set to true
  setSwitch = this.createReducingAction(
    (state, isOn) => ({ isOn })
  );
}

export SwitchStateController;

Using the StateController

Below are two examples of how the above class would be used with React.

With a connected component

For higher order components, a ComponentConnector can be used to create a wrapper that connects the component to the ObjectiveStore. This allows the component and controllers used by it to have access to the ObjectiveStore.


import React from 'react';
import { ComponentConnector } from 'objective-redux';
import { SwitchStateController } from './store/switch-state-controller';

function AppComponent(props) {
  const { isOn, objectiveStore } = props;

  const sendToggleAction = () => {
    SwitchStateController.getInstance(objectiveStore).toggleSwitch();
    // or we could call our set method
    // SwitchStateController.getInstance(objectiveStore).setSwitch(!isOn);
  }

  return (
      <button onClick={sendToggleAction}>
        Turn the switch { isOn ? 'Off' : 'On' }
      </button>
  );
}

export default ComponentConnector
  .addPropsTo(AppComponent)              // Our component we are connecting to the ObjectiveStore
  .fromController(SwitchStateController) // (optional) Adds the StateController's state as properties to the component
                                         // Alternatively, .fromController(SwitchStateController, state => ({ isOn: state.isOn }))
                                         // for custom mappings. More than one controller can be chained with .fromController to
                                         // add more props to the component.
  .connect(); // Returns a new component that wraps the provided component

With a React hook

React hooks can be used for functional components.


  import React from 'react';
  import { useController } from 'objective-redux';
  import { SwitchStateController } from './store/switch-state-controller';
  
  function AppComponent() {
    const switchController = useController(SwitchStateController);
    const { isOn } = switchController.getStateSlice();
  
    const sendToggleAction = () => {
      switchController.toggleSwitch();
      // or we could call our set method
      // switchController.setSwitch(!isOn);
    }
  
    return (
        <button onClick={sendToggleAction}>
          Turn the switch { isOn ? 'Off' : 'On' }
        </button>
    );
  }

Notes

1 Optionally, namespaces can be used to help with scoping controllers and Redux store slices. See the StateController API documentation for more.