A convenient Redux-toolkit + Redux-observable encapsulation
npm install @wfh/redux-toolkit-observableReference to
https://redux-toolkit.js.org/
https://redux-observable.js.org/
``mermaid
flowchart TD
Epic([Epic])
subgraph Redux
rtk[Redux-toolkit
middleware]
ro[Redux-observable
middleware]
reduxStore[(Redux store)]
end
comp([component]) --> |dispatch
reducer action| rtk
comp -.-> |useEffect,
props| hook["@wfh hook,
react-redux"]
hook -.-> |subscribe,
diff| reduxStore
rtk --> |update| reduxStore
rtk --> ro
ro --> |action stream,
state change stream| Epic
Epic --> |dispatch
async
reducer action| rtk
Epic --> |request| api[(API)]
ro -.-> |diff| reduxStore
`core-js/es/object/index
$3
#### 0. import dependencies and polyfill
> Make sure you have polyfill for ES5: , if your framework is not using babel loader, like Angular.
`ts`
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 typets`
export interface ExampleState {
...
}
create initial state
`ts`
const initialState: ExampleState = {
foo: true,
_computed: {
bar: ''
}
};
create slice
`ts`
export const exampleSlice = stateFactory.newSlice({
name: 'example',
initialState,
reducers: {
exampleAction(draft, {payload}: PayloadAction
// 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 of action parameter.
Now bind actions with dispatcher.
`ts`
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 .
`ts`
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
}),
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
`ts`
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)
`ts`
if (module.hot) {
module.hot.dispose(data => {
stateFactory.removeSlice(exampleSlice);
releaseEpic();
});
}
#### 5. Connect to React Component
TBD.
#### 1. Use reselect
#### 2. About Normalized state and state structure
- newSlice() vs Redux's createSlice()newSlice()
implicitly creates default actions for each slice:_init()
- action\_init()
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 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.
`ts`
export declare class StateFactory {
getErrorState(): ErrorState;
getErrorStore(): Observable
...
}
- bindActionCreators()\bindActionCreators()
our store can be lazily configured, dispatch is not available at beginning, thats why we need a customized
- In Redux-observable Epic, observe store changing events and react by dispatching new actions.
1. First you have imports like beblow.
`ts`
import * as rx from 'rxjs';
import * as op from 'rxjs/operators';
2. Return a merge stream from Epic function
`ts
``
> "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.
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