@wfh/redux-toolkit-observable
v1.1.1-0
Published
A convenient Redux-toolkit + Redux-observable encapsulation
Downloads
9
Maintainers
Readme
Redux-toolkit And Redux-abservable
- Redux-toolkit And Redux-abservable
Reference to https://redux-toolkit.js.org/ https://redux-observable.js.org/
Understand React component, Redux and Epic
flowchart TD
Epic([Epic])
subgraph Redux
rtk[Redux-toolkit<br>middleware]
ro[Redux-observable<br>middleware]
reduxStore[(Redux store)]
end
comp([component]) --> |dispatch<br>reducer action| rtk
comp -.-> |useEffect,<br>props| hook["@wfh hook,<br>react-redux"]
hook -.-> |subscribe,<br>diff| reduxStore
rtk --> |update| reduxStore
rtk --> ro
ro --> |action stream,<br>state change stream| Epic
Epic --> |dispatch<br>async<br>reducer action| rtk
Epic --> |request| api[(API)]
ro -.-> |diff| reduxStore
Author slice store
0. import dependencies and polyfill
Make sure you have polyfill for ES5:
core-js/es/object/index
, if your framework is not using babel loader, like Angular.
import { PayloadAction } from '@reduxjs/toolkit';
// For browser side Webpack based project, which has a babel or ts-loader configured.
import { getModuleInjector, ofPayloadAction, stateFactory } from '@wfh/redux-toolkit-abservable/es/state-factory-browser';
For Node.js server side project, you can wrapper a state factory somewhere or directly use "@wfh/redux-toolkit-abservabledist/redux-toolkit-observable'"
1. create a Slice
Define your state type
export interface ExampleState {
...
}
create initial state
const initialState: ExampleState = {
foo: true,
_computed: {
bar: ''
}
};
create slice
export const exampleSlice = stateFactory.newSlice({
name: 'example',
initialState,
reducers: {
exampleAction(draft, {payload}: PayloadAction<boolean>) {
// modify state draft
draft.foo = payload;
},
...
}
});
"example" is the slice name of state true
exampleAction
is one of the actions, make sure you tell the TS type PayloadAction<boolean>
of action parameter.
Now bind actions with dispatcher.
export const exampleActionDispatcher = stateFactory.bindActionCreators(exampleSlice);
2. create an Epic
Create a redux-abservable epic to handle specific actions, do async logic and dispatching new actions .
const releaseEpic = stateFactory.addEpic((action$) => {
return merge(
// observe incoming action stream, dispatch new actions (or return action stream)
action$.pipe(ofPayloadAction(exampleSlice.actions.exampleAction),
switchMap(({payload}) => {
return from(Promise.resolve('mock async HTTP request call'));
})
),
// observe state changing event stream and dispatch new Action with convient anonymous "_change" action (reducer callback)
getStore().pipe(
map(s => s.foo),
distinctUntilChanged(),
map(changedFoo => {
exampleActionDispatcher._change(draft => {
draft._computed.bar = 'changed ' + changedFoo;
});
})
),
// ... more observe operator pipeline definitions
).pipe(
catchError(ex => {
// tslint:disable-next-line: no-console
console.error(ex);
// gService.toastAction('网络错误\n' + ex.message);
return of<PayloadAction>();
}),
ignoreElements()
);
}
action$.pipe(ofPayloadAction(exampleSlice.actions.exampleAction)
meaning filter actions for only interested action , exampleAction
, accept multiple arguments.
getStore().pipe(map(s => s.foo), distinctUntilChanged())
meaning observe and reacting on specific state change event.
getStore()
is defined later.
exampleActionDispatcher._change()
dispatch any new actions.
3. export useful members
export const exampleActionDispatcher = stateFactory.bindActionCreators(exampleSlice);
export function getState() {
return stateFactory.sliceState(exampleSlice);
}
export function getStore() {
return stateFactory.sliceStore(exampleSlice);
}
4. Support Hot module replacement (HMR)
if (module.hot) {
module.hot.dispose(data => {
stateFactory.removeSlice(exampleSlice);
releaseEpic();
});
}
5. Connect to React Component
TBD.
Use slice store in your component
1. Use reselect
2. About Normalized state and state structure
Why we wrapper redux-toolkit + redux-observable
What's different from using redux-toolkit and redux-abservable directly, what's new in our encapsulation?
newSlice()
vs Redux'screateSlice()
newSlice()
implicitly creates default actions for each slice:_init()
action
Called automatically when each slice is created, since slice can be lazily loaded in web application, you may wonder when a specific slice is initialized, just look up its_init()
action log._change(reducer)
action
Epic is where we subscribe action stream and output new action stream for async function.Originally to change a state, we must defined a reducer on slice, and output or dispatch that reducer action inside epic.
In case you are tired of writing to many reducers on slice which contains very small change logic,
_change
is a shared reducer action for you to call inside epic or component, so that you can directly write reducer logic as an action payload within epic definition.But this shared action might be against best practice of redux, since shared action has no meaningful name to be tracked & logged. Just save us from defining to many small reducers/actions on redux slice.
Global Error state
With a Redux middleware to handle dispatch action error (any error thrown from reducer), automatically update error state.export declare class StateFactory { getErrorState(): ErrorState; getErrorStore(): Observable<ErrorState>; ... }
bindActionCreators()
our store can be lazily configured, dispatch is not available at beginning, thats why we need a customizedbindActionCreators()
The most frequently used RxJS operators
There are 2 scenarios you need to interect directly with RxJS.
In Redux-observable Epic, to observe incoming action stream, and dispatching new actions (or return outgoing action stream)
In Redux-observable Epic, observe store changing events and react by dispatching new actions.
- First you have imports like beblow.
import * as rx from 'rxjs';
import * as op from 'rxjs/operators';
- Return a merge stream from Epic function
Typescript compile with compiler option "declaration: true" issue
"This is likely not portable, a type annotation is necessary" https://github.com/microsoft/TypeScript/issues/30858
It usally happens when you are using a "monorepo", with a resolved symlink pointing to some directory which is not under "node_modules", the solution is, try not to resolve symlinks in compiler options, and don't use real file path in "file", "include" property in tsconfig.
Tiny redux toolkit
It does not depends on Redux, no Redux is required to be packed with it.
This file provide some hooks which leverages RxJS to mimic Redux-toolkit + Redux-observable which is supposed to be used isolated within any React component in case your component has complicated and async state changing logic.
Redux + RxJs provides a better way to deal with complicated UI state related job.
- it is small and supposed to be well performed
- it does not use ImmerJS, you should take care of immutability of state by yourself
- because there is no ImmerJS, you can put any type of Object in state including those are not supported by ImmerJS