All Rights Reserved 

Copyright 2021, 2022

Mindspace, LLC

Front-End Architectures

Reactive Solutions

Part 2

"A program is said to be reactive when an input change leads to a corresponding change in output without any need to update the output change manually."

Reactive Programming

Reactive for All Layers

Business

Data

Services

Business

Hooks

UI

View Models

Reactive Pathways

  • UI  <== reacts to === user
  • UI  <== reacts to === UI

?

?

  • UI  <== reacts to === Service
  • Service <== reacts to === Service

What are the paths to building Reactive solutions ?

{

Framework makes this "easy"

{

Where the hard architecture work is done

Reactive Libraries

Reactive Architectures

Reactive Stores

Part 2 of 5

  • Manages state/data changes
  • Hides details of internal mechanics
  • Supports change notifications to external world
  • Build custom view models for external consumption
  • Pushes updates to all connected observers
  • Supports long-term connections
  • Enforces 1-Way Data Flows

Reactive Stores ?

Why use

1-Way Data Flows

Store

Reactive Stores ?

Why use

No need to understand artifacts and mechanics of complex state management like NgRx.

 

Reactive Stores ?

Why use

Reactive Stores

Simple API

  1. Define a custom store
  2. Build a React hook to the store
  3. Use the hook to select store
  4. Render immutable data

The store you define is a mix of state and APIs. This is your full view model.

Your hook has built-in change detection

Select the entire store or optimize rendering with selectors to request a custom VM

View Model state is always immutable; to enforce 1-way data flows.

Steps to build a Reactive Store:

import { createStore } from '@mindspace-io/react';

// Define a factory to create your custom store; which 
// will be managed by the internal Reactive store

const factoryFn = ( api: StoreAPI ) => {
	return <store>;	// aka View Model
}
    
// Build a React hook to a managed store

const useStore : HookAPI = createStore( 
   factoryFn : StateCreator, 
   options  ?: Options 
): HookAPI { }

    
// Access read-only store. 
// May Use opitonal selectors to build viewModel 
// and optimize change detection
 
const {} = useStore();    
    

Simple Counter

Reactive Store Demo:

React

Define your store API:

import { State } from '@mindspace-io/react';

export interface CounterStore extends State {
  count: number;
  incrementCount: () => void;
  decrementCount: () => void;
}

Define the store Factory:

import { StoreAPI } from '@mindspace-io/react';
import type { CounterStore } from './counter.interfaces';

const factory = (api: StoreAPI<CounterStore> ): CounterStore => ({
  count: 0,
  
  incrementCount() { },
  decrementCount() { },
});



counter.store.ts

StoreAPI<T>:

export interface StoreApi<T extends State> {
  set: SetState<T>;
  get: GetState<T>;

  // Used to batch state changes
  applyTransaction: ApplyTransaction<T>;

  // Used during store configuration
  addComputedProperty: AddComputedProperty<T>;
  watchProperty: WatchProperty<T>;

  // Used to announce status
  setIsLoading: SetLoading;
  setError: SetError;
}

This API is accessible ONLY within your factory function used with createStore(<factoryFn>).

@mindspace-io/react

Use API within the store Factory:

import { StoreAPI } from '@mindspace-io/react';
import type { CounterStore } from './counter.interfaces';

const factory = (api: StoreAPI<CounterStore> ): CounterStore => ({
  count: 0,
  
  incrementCount() {
    api.set((draft) => {
      draft.count += 1;
    });
  },
  decrementCount() {    
    api.set((draft) => {
      draft.count -= 1;
    });
  },
  
});



counter.store.ts

Under the hood with the store createStore:

import { StoreAPI } from '@mindspace-io/react';
import type { CounterStore } from './counter.interfaces';

const factory = (api: StoreAPI<CounterStore> ): CounterStore => ({
  count: 0,
  
  incrementCount() {
    api.set((draft) => {
      draft.count += 1;
    });
  },
  decrementCount() {    
    api.set((draft) => {
      draft.count -= 1;
    });
  },
  
});



  • API is a service that internal manages the reactive engine
  • Store mutator actions ALWAYS work with draft data... using the Immer JS curry `produce()`
  • Store data (post initialization) is always IMMUTABLE

Use createStore for a custom hook:

import { createStore, StoreAPI } from '@mindspace-io/react';
import type { CounterStore } from './counter.interfaces';

const factoryFn = (api: StoreAPI<CounterStore> ): CounterStore => ({
  count: 0,  
  incrementCount() { /*...*/ },
  decrementCount() { /*...*/ },  
});

export const useCounterStore = createStore( factoryFn )

createStore() returns a custom hook that is automatically connected to the reactive store. 

 

 

 

 

To access the view model, use the hook...

counter.store.ts

Use custom hook in a Functional Component:

import React, { useCallback } from 'react';
import { useCounterStore, CounterStore } from './counter.store';


export const SimpleCounter: React.FC = () => {
  const { count, incrementCount } = useCounterStore<CounterStore>();
  
  
  return (
    <>
       <p> Count = {count} </p>
       <button onClick={incrementCount}>Increment</button>
    </>
  );
};
  • Whenever useCounterStore() is used, the same store instance will be used
  • No wrapper nor explicit memoization of the API method `incrementCount`
  • Can destructure only a portion of the Store properties

SimpleCounter.tsx

Reactive Store

(with Immer, RxJS, and Akita)

import { createStore } from "@mindspace-io/react";


export const useStore = createStore<CounterStore>(({ set }) => {
 const store = ({
    visits  : 0,
    messages: [],

    incrementCount() { set((d) => { d.visits += 1;})     },
    decrementCount() { set((d) => { d.visits -= 1; })    }
  });

  return store;
});

Traditional Approach

(with React Hooks)

import { useState, useCallback } from "react";


export function useCounter(): CounterStore {
  const [visits, setVisits] = useState(0);
  const [messages]          = useState([]);

  const incrementCount = useCallback(() => {
    setVisits((prev) => prev + 1);
  }, [setVisits]);

  const decrementCount = useCallback(() => {
    setVisits((prev) => prev - 1);
  }, [setVisits]);

  return {
    visits,
    messages,
    incrementCount,
    decrementCount
  };
}

Publish hook

Immutable  State

Hook accepts selectors

Uses drafts to mutate internally

State is now inherently observable

`useStore(<selector>)` decides what  state is desired

State is shared across components

 

 

Mutable  State

Hook always publishes same structure

State is NOT shared across components

Async + Computed Properties

React

Reactive Store Demo:

Define your store API:

import { State } from '@mindspace-io/react';

export interface MessageStore extends State {
  filterBy         : string;
  messages         : string[];
  filteredMessages : string[];
  
  updateFilter     : (filterBy: string) => void;
}

Implement the store API:

import { StoreEffect } from '@mindspace-io/react';
import type { MessageStore } from './message.store.interfaces';

const factoryFn = ({ set, addComputedProperty }, useStoreEffect: StoreEffect) => {
  
    // Create store with API and initializations
    const store = {
      filterBy          : '',
      messages          : [], // all messages
      filteredMessages  : [], // only messages with filte matches
      
      updateFilter(filterBy: string) {
        set((s) => {
          s.filterBy = filterBy;
        });
      }    
    };

    //useStoreEffect(() => { }, []);
    //return addComputedProperty();
}


message.store.ts

StoreEffect:

import { DependencyList, EffectCallback } from 'react';

/**
 * Feature to enables stores to internal perform 
 * initialization side affect AFTER store is ready
 */
export type StoreEffect = (
  effect : EffectCallback, 
  deps  ?: DependencyList
) => void;

This effect is accessible ONLY within the createStore(<factoryFn>).

@mindspace-io/react

Use the StoreEffect:

import { StoreEffect } from '@mindspace-io/react';

import { MessageService } from './messages.service';
import type { MessageStore } from './message.store.interfaces';

const service = new MessageService();
const factoryFn = ({ set, addComputedProperty }, useStoreEffect: StoreEffect) => {
  
    // Create store with API and initializations
    const store = { /*...*/ ;

    // Async side effect to 1x load all messages
    useStoreEffect(() => {
      service.loadAll().then((messages) => {
        set((d) => {
          d.messages = messages;
        });
      });
    }, []);
  
    return addComputedProperty( /*..*/ );
}


message.store.ts

Add a Computed Property

import { StoreEffect } from '@mindspace-io/react';

import { onlyFilteredMessages } from './messages.utils';
import { MessageService } from './messages.service';
import type { MessageStore } from './message.store.interfaces';

//const service = new MessageService();
const factoryFn = ({ set, addComputedProperty }, useStoreEffect: StoreEffect) => {
  
    const store = { /*...*/ ;
    useStoreEffect(() => { /*...*/ }, []);
  
   // Create a computed property dependent on two (2) queries
    // and chain a transformation operator to add search match highlights

    return addComputedProperty(store, {
      name: 'filteredMessages',
      selectors: [(s: MessagesState) => s.messages, (s: MessagesState) => s.filterBy],
      transform: onlyFilteredMessages,
    });
}


message.store.ts

Define Transform Operator

type QueryResults = [string[], string];  // Tuple
type MessageList = string[];


const injectHighlights = (match: string) => (s: string) => {
  return s.replace(new RegExp(filterBy, 'gi'), (match) => `<span class='match'>${match}</span>`);
}

const hasCritiera = (filterBy: string) => (s: string) => {
  return s.toLowerCase().indexOf(criteria) > -1;
}

/**
 * Build computed property for `filteredMessages`
 * Uses an array of selectors [<all messages>, <filterBy criteria>]
 * for optimized change propagation
 */

export const onlyFilteredMessages = ([messages, filterBy]: QueryResults): MessageList => {
  const criteria = filterBy.toLowerCase();    

  return !filterBy ? [...messages] : messages
    .filter( hasCritiera(criteria) )
    .map( injectHighlights(match) );
};

message.utils.ts

Use createStore for a custom hook:

import { createStore, StoreEffect } from '@mindspace-io/react';

import { MessageService } from './messages.service';
import { MessagesState } from './messages.interfaces';
import { onlyFilteredMessages } from './messages.utils';


const service = new MessageService(); 

/*******************************************
 * Instantiate store with state
 * Note: The `filteredMessages` value is updated via a 'computed' property
 *******************************************/

export const useMessageStore = createStore<MessagesState>( /* ... */ );

To access the view model, use the hook...

To build a custom ViewModel, use a Hook Query!

message.store.ts

Define a custom View Model + Query

import { State, QueryList } from '@mindspace-io/react';

export interface MessageStore extends State {
  filterBy         : string;
  messages         : string[];
  filteredMessages : string[];
  
  updateFilter     : (filterBy: string) => void;
}

// Define a custom Message View Model. (Btw, this is a Typle)
// 
export type ViewModel = [string, string[], (v: string) => void];  


// Define a Hook Query to select the following store properties
// 
export const storeQuery = QueryList<MessageStore, ViewModel>[
  (s: MessagesState) => s.filterBy,
  (s: MessagesState) => s.filteredMessages,
  (s: MessagesState) => s.updateFilter,
];

message.interfaces.ts

Use custom hook in a Functional Component:

import React from 'react';

import { storeQuery } from './messages.interfaces';
import { useMessageStore } from './messages.store';

/**
 * Show list of messages that container the searchCriteria
 */
export const FilteredMessages: React.FC = () => {
  const [searchCriteria, messages, updateSearchBy] = useMessageStore(storeQuery);
  
  return (
   <>
    // See live demo...
   </>
  )
};

FilteredMessages.tsx

Reactive Store

(with Immer, RxJS, and Akita)

Traditional Approach

(with React Hooks)

import { createStore } from "@mindspace-io/react";
import { onlyFilteredMessages, MessageStore } from "./common";


export const useStore = createStore<MessageStore>(
  ({ set, addComputedProperty }) => {
    const store = {
      filterBy: "",
      messages: [],
      updateFilter(filterBy: string) {
        set((s) => {
          s.filterBy = filterBy;
        });
      },
    };

    return addComputedProperty(store, {
      name: "filteredMessages",
      selectors: [
        (s: MessagesState) => s.messages,
        (s: MessagesState) => s.filterBy
      ],
      transform: onlyFilteredMessages      
    });
  }
);
import { useState, useEffect } from "react";
import { onlyFilteredMessages, ViewModel, MESSAGES } from "./common";


export function useMessages(): ViewModel {
  const [allMessages] = useState(MESSAGES);
  const [filterBy, updateFilter] = useState("");
  const [messages, setMessages] = useState(allMessages);

  useEffect(() => {
    const filtered = onlyFilteredMessages([allMessages, filterBy]);
    setMessages(filtered);
  },[allMessages, filterBy])

  return [
    filterBy, 
    messages,
    updateFilter
  ];
}

Full Store API access

Immutability Guaranteed

Store-like API easily managed.

 

Mutable Risks

Multiple re-Renders

"Feels" Clumsy

  • Transactions
  • Error Reporting
  • Loading Status
  • Paginated Data
  • Reset Store
  • Publish View Facade
  • Integrate with DI
  • Global or Multiple Store Instances
  • Optimized Store Selectors
  • Transactions

Reactive Stores

More features

Reactive Stores

Transactions

Text

const useStore = createStore<MessageStore>((
  { set, setIsLoading, applyTransaction }) => {
  
    return {
      messages    : [],
      
      async refresh() {
        applyTransaction(() => {
          setIsLoading();         
          set((s) => { s.messages = [] });
        });

        const messages = await emailService.loadAll();

        applyTransaction(() => {
          set((s) => {
            s.messages = messages;
            s.isLoading = false;
          });
        });
      },
    };
  
 });

Reactive Stores

More features

  • Transactions
  • Error Reporting
  • Loading Status
  • Paginated Data
  • Reset Store
  • Publish View Facade
  • Integrate with DI
  • Global or Multiple Store Instances
  • Optimized Store Selectors
  • Publish View Facade

Define your

View Facades

Text

export const useTodoStore = createStore<TodoStore>(({set}) => {
  
  return ({
    todos: [],
    filter: VISIBILITY_FILTER.SHOW_ALL,
    
    facade: {
      async addTodo(text: string): boolean {  },
      async deleteTodo({ id }): boolean { },
      
      toggleComplete({ id }) { },
      updateFilter(filter: VISIBILITY_FILTER) { },
      
      history: { 
        undo: () => {}, 
        redo: () => {}, 
        hasPast: false, 
        hasFuture: false 
      }
    }
  });
});

Using your

View Facades

Text

import { useTodoStore, TodoStore } from '@customer/todo-store';

export const TodosPage: React.FC = () => {
  const { todos, filter, facade } = useTodoStore<TodoStore>();

  return (
    <>
      <div className="todoBar">
    
        <AddTodo onAdd={ facade.addTodo }  />

        <Filters selectedFilter={ filter } 
                 onChange={ facade.updateFilter } />
      </div>

      <TodoList  todos={ todos }
                 onToggle={ facade.toggleComplete }
                 onDelete={ facade.deleteTodo }
      />

    </>
  );
};

Reactive Store + View Facades

Demo

Reactive Stores

More features

  • Transactions
  • Error Reporting
  • Loading Status
  • Paginated Data
  • Reset Store
  • Publish View Facade
  • Integrate with DI
  • Global or Multiple Store Instances
  • Optimized Store Selectors
  • Optimized Store Selectors

Define and Use your

Store Query

export type QueryResults = [Todo[], TodoFacade];

export const query: QueryList<TodoStore, QueryResults>[] = [
  (store) => store.todos, 
  (store) => store.facade
];


export const TodoList: FC = () => {
  const [todos, facade] = useTodoStore<StoreViewModel>(query);
  
  return ( <div> ... </div> );
};

When using a store query (aka selector), a Tuple is returned instead of the store object.

within the React Platform

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

within the React Platform

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

within the React Platform

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

within the React Platform

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

within the React Platform

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

within the React Platform

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

within the React Platform

React Hooks

  1. Code is concise and clear
  2. Good for simple state, better with complex state
  3. Encourage ideas of internal store & view models
  4. No need for `useCallback()` protection
  5. Stores are protected, isolated, and immutable
  6. Immutable Data with internal draft deep-mutation 
  7. Properties are Observable
  8. Computed values are optimized-derived
  9. Shared State
  10. Selectors are used with published "hook"

Reactive Stores

Reactive Stores

  1. Code can be concise and cleary
  2. Good for simple state
  3. Encourage ideas of managed values
  4. Computed values are always recalculated
  5. No shared state (OoB)
  6. Mutable Data
  7. Not Observable
  8. Request-Response Only

Reactive Stores

... more Libraries

Reactive Solutions - Part 2

By Thomas Burleson

Private

Reactive Solutions - Part 2