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
Custom Hooks
Reactive Stores