Navigation:

Down-Arrow for next page,

Right-Arrow for next section

React Hooks  or  Reactive Stores ?

Michael Brutskiy

Senior Developer

Ameriprise Financial Services

In the course of presenting this architecture solution, I created 4 samples. I challenged Michael to use the traditional React hooks for those same samples.

 

With both the traditional Hook and Reactive versions, we could compare and contrast Reactive architectures.

 

Great work Michael!

hat State Management to use ?

Should you use React Hooks or Reactive Stores in your ReactJS applications?

Let's look at four (4) sample usages and compare, contrast both approaches:

W

CodeSandbox Live Demos

Reactive State Management

(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

1

 

 

Mutable  State

Hook always publishes same structure

State is NOT shared across components

Reactive State Management

(with Immer, RxJS, and Akita)

Traditional Approach

(with React Hooks)

2

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

const service = new EmailService();

export const useStore = createStore<MessagesState>(
  ({ set, setIsLoading, applyTransaction }) => {

    return {
      messages   : [],
      
      async refresh() {
        applyTransaction(() => {
          setIsLoading();
          set((s) => { s.messages = [] });
        });
  
        const messages = await service.loadAll();
  
        applyTransaction(() => {
          set((s) => {
            s.messages = messages;
            s.isLoading = false;
          });
        });
      }
    };

  }
);
import {useState, useCallback }from "react";


export function useMessages(): MessagesStore {
  const [service]                     = useState(() => new EmailService());
  const [messages, setMessages]       = useState([] as string[]);
  const [isLoading, setIsLoading]     = useState(false);

  const refresh = useCallback(async () => {
    setIsLoading(true);    
    const newMessages   = await service.loadAll();
    
    setMessages(newMessages);
    setIsLoading(false);

  }, [setIsLoading, service, setMessages]);

  return {
    isLoading,
    messages,
    refresh
  };
}

More Intuitive (Store like)

Store + State are hidden

Only Hook is public

Shared state between multiple uses of hook

Expert Knowledge Required

`useCallback()` required

`useState(()=>{})` trick required

Reactive State Management

(with Immer, RxJS, and Akita)

Traditional Approach

(with React Hooks)

3

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


export const useStore = createStore<MessagesStore>(
  ({ set, addComputedProperty }) => {
    const store = {
      filterBy: "",
      messages: 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 } from "react";
import { onlyFilteredMessages, ViewModel, MESSAGES } from "./common";


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

  return [
    filterBy, 
    onlyFilteredMessages([messages, filterBy]), 
    updateFilter
  ];
}

Immutability Guaranteed

Full Store API access

Mutable Risks

Reactive State Management

(with Immer, RxJS, and Akita)

Traditional Approach

(with React Hooks)

import { debounce } from "lodash";
import { useState, useCallback, useEffect } from "react";
import { callWtfApi, WTF, ViewModel } from "./common";


export function useQa(): ViewModel {
  const [question, updateQuestion] = useState("");
  const [answer, updateAnswer]     = useState("");

  const verify = useCallback(
    debounce(async (newQuestion: string) => {
      updateAnswer(WTF.wait);
      if (newQuestion.indexOf("?") > -1) {
        try {
          const newAnswer = await callWtfApi();
          updateAnswer(newAnswer);

          return;
        } catch (error) {
          updateAnswer(`${WTF.error}: ${error}`);
        }
      }

      updateAnswer(WTF.hint);
    }, 200),
    [updateAnswer]
  );

  useEffect(() => {
    verify(question);
  }, [verify, question]);

  return [question, answer, updateQuestion];
}
import { debounce } from "lodash";
import { createStore, State, StateSelector } from "@mindspace-io/react";
import { ViewModel, WTF, callWtfApi } from "./common";


export const useStore = createStore<QAState>(({ set, watchProperty }) => {
  const store = {
    question: "",
    answer: "",
    updateQuestion(answer: string) {
      set((s: QAState) => {
        s.question = answer;
      });
    }
  };
  const updateAnswer = (value: string) => set(d => { d.answer = value });  
  const verify = debounce(async (newQuestion: string) => {
    updateAnswer(WTF.wait);
    if (newQuestion.indexOf("?") > -1) {
      try {
        const newAnswer = await callWtfApi();
        updateAnswer(newAnswer);

        return;
      } catch (error) {
        updateAnswer(`${WTF.error}: ${error}`);
      }
    }
    updateAnswer(WTF.hint);
  }, 200);

  watchProperty(store, "question", verify);
  return store;
});

4

Summary

  1. Code can be concise and clear
  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

Custom 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