Rex is a Redux-like state management library that is reactive and immutable out of the box. It's just a tiny wrapper lib on top of RxJS and immer.
To create a new store you call the createStore
function with a reducer and its initial state.
There is no need to supply a default case in your reducer, and the reducer shouldn't return anything.
import { ReducerAction, createStore } from "@evanion/rex";
import { Draft } from "immer";
interface UserState {
data: User | null;
loading: boolean;
loaded: boolean;
error?: string;
}
enum UserAction {
FetchUserInit = "FETCH_USER_INIT",
FetchUserSuccess = "FETCH_USER_SUCCESS",
FetchUserError = "FETCH_USER_ERROR",
ResetUser = "RESET_USER",
}
type UserStoreActions =
| ReducerAction<UserAction.FetchUserInit, { id: string }>
| ReducerAction<UserAction.FetchUserSuccess, User>
| ReducerAction<UserAction.FetchUserError, Error>
| ReducerAction<UserAction.ResetUser, null>;
const initialState = {
data: null,
loading: false,
loaded: false,
};
function reducer(state: Draft<UserState>, action: UserStoreActions) {
switch (action.type) {
case UserAction.FetchUserInit:
state.loading = true;
break;
case UserAction.FetchUserSuccess:
state.data = action.payload;
state.loading = false;
state.loaded = true;
delete state.error;
break;
case UserAction.FetchUserError:
state.loading = false;
state.error = action.payload;
break;
case UserAction.ResetUser:
state.user = null;
state.loading = false;
state.loaded = false;
delete state.error;
}
}
export const userStore = createStore(reducer, initialState);
To listen to updates to a store, you can to subscribe to it:
const onStoreChange = (state) => {
console.log("State have changed", state);
};
const subscription = userStore.subscribe(onStoreChange);
// when you want to stop subscribing to the store, you call unsubscribe
subscription.unsubscribe();
To dispatch an action call the stores dispatch action:
userStore.dispatch(UserAction.ResetUser);
You can easily listen for specific actions to perform other operations and then dispatch a new action with that result
const actionSub = userStore.on(UserAction.FetchUserInit, async (payload) =>
fetch(`https://fakestoreapi.com/products/${payload.id}`)
.then((res) => res.json())
.then((res) => res.body)
.then((usr) => userStore.dispatch(UserAction.FetchUserSuccess, usr))
);
// when you want to stop listening for the action, unsubscribe
actionSub.unsubscribe();
This way, you can listen to an action in one store and dispatch an action in another store based on that action and its payload.
const actionSub = userStore.on(UserAction.FetchUserInit, (payload) => {
orderStore.dispatch(OrderAction.FetchUserOrdersInit, { userId: payload.id });
});
Using the store in react is relatively simple:
const useUser = () => {
const [userState, setUserState] = useState({});
useEffect(() => {
const sub = userStore.subscribe(setUserState);
return () => sub.unsubscribe();
});
return userState;
};
It's even easier if you use the provided useStore
hook:
function ListUsers(){
const {state, dispatch} = useStore<UserState, UserActions>(userStore)
const fetchUser = (id:string) => dispatch(USerAction.FetchUserInit, id);
return (
<div>
<button onClick={fetchUser('5abd7c')}>Get User</button>
<pre>
{JSON.stringify(state, null, 2)}
</pre>
</div>
)
}
If you want to consume the state or action streams, their observables are directly available
userStore.state$.subscribe((state)=>console.log('state change', state))
userStore.action$.subscribe((action)=>console.log('action dispatched', action)).