Redux lib pattern
GF
Posted on March 30, 2020
I would like to describe an approach (could be called "redux lib pattern") which I use in react-redux applications for interacting between modules are not good associated with trivial react-redux way. Also, this approach is useful for using very complex react components extracted to separate modules or packages.
The redux lib pattern allows us to separate applications with almost any module and provides independent development process, deploy and testing for a module, however, let us organize really simple and convenient interacting with a module in an application. Also, the experience of using that pattern showed that it is really convenient to be used by application developers, especially in the case the development process is spread to a few teams with constrained areas of responsibility.
Problems which redux lib pattern allows to avoid:
1) Multiple implementations of code responsible for managing the same modules/components in different applications
2) Absence of architectural borders between modules/components and applications
3) Complex and "weird" integration of modules to react-redux flow
4) Lack of control for changes in interacting between applications and modules
5) Complexity and labor input extracting some code to architectural layer
Let's start from a complex react component example which is extracted to a separate package. Imagine that we have an application are using that component. Component, of course, has defined Props interface, for instance:
interface ComponentProps {
prop1: boolean;
prop2: number;
prop3: SomeEnum;
...
prop20: Array<number>;
}
interface ComponentCallbacks {
callback1: function;
...
callback5: function;
}
type SomeComponentProps = ComponentProps & ComponentCallbacks;
Usually, props for that component could be prepared in mapStateToProps
and mapDispactchToProps
function in an application. However, responsibility for storing and management of that data lies with an application and the data could be got from different parts of an application redux store. In case our Component is used in a few applications, developers of each of them have to provide management for data required in Component in the application redux store. Of course, it is better to not to do the same work twice. It could be much more simple to pass a whole redux store to Component and it would pick necessary props from a store. On the other hand, it is obvious that Component must not know anything about an application store.
Unification of a part of an application redux store which are contains a data for Component could be solution for problem above, but just agreement about it isn't enough. It is necessary to create solution that will be essential for using. It is the core idea of redux lib pattern - creation additional package which provides sufficient reducer and actions set for interact with Component.
Let's start with a reducer. It implements the ComponentPops interface, excludes callbacks. Also, it is useful to provide the ability to adjust default state:
// component-redux-lib/reducer.ts
const defaultState: ComponentProps = {
prop1: true;
prop2: 42;
prop3: SomeEnum.Value;
...
prop20: [4, 2];
};
export const createReducer = (defaultStatePatch: Partial<ComponentProps> = {}) => {
const defaultState = {
...defaultState,
...defaultStatePatch
}
// reducer itself
return (state = defaultState, action) => {
...
}
};
export const reducer = createReducer();
So, redux lib should provide a sufficient set of actions for managing all of Components abilities:
// component-redux-lib/actions.ts
const setProp1 = (value: boolean) = ({
// it is convenient for debug to use lib prefix
type: 'COMPONENT-REDUX-LIB/SET-PROP1',
payload: value
})
...
export default {
setProp1,
setProp2,
...
}
It could be necessary to have thunk actions. However, what if we want to get some data from a store inside a thunk? For example, we need to create toggle action (in fact, I don't recommend to provide any toggle actions from lib and create in an application instead):
// component-redux-lib/actions.ts
const toggleProp1 = (value: boolean) = (getState, dispatch) => {
const state = getState();
// we don't know where component reducer is located
const prop1 = state[?];
dispatch(setProp1(!prop1));
}
...
export default {
setProp1,
setProp2,
...
toggleProp1
}
For that case let's add constant which determines a location for reducer from redux lib into root application redux store.
// component-redux-lib/constants.ts
const componentReducerKey = 'ComponentState';
export default {
componentReducerKey
}
And let's create selector:
// component-redux-lib/selectors.ts
import {componentReducerKey} from './constants.ts';
interface State {
[componentReducerKey]: ComponentProps
}
const getComponentState = (state: State) => state[componentReducerKey];
export default {
getComponentState
}
Now it is possible to create thunk action:
// component-redux-lib/actions.ts
import {getComponentState} from './selectors.ts'
const toggleProp1 = (value: boolean) = (getState, dispatch) => {
const state = getState();
// Now we know where component reducer is located
const {prop1} = getComponentState(state);
dispatch(setProp1(!prop1));
}
...
export default {
setProp1,
setProp2,
...
toggleProp1
}
In case we can't store all necessary data in redux lib selector it is possible to add additional parameters to selector function:
// component-redux-lib/selectors.ts
import {componentReducerKey} from './constants.ts';
interface State {
[componentReducerKey]: ComponentProps
}
interface AdditionalProps {
prop20: Array<number>
}
const createComponentProps = (state: State, additionalProps: AdditionalProps) => {
// there are could be more complex calculating
return {
...getComponentState(state),
...additionalProps
}
}
export default {
getComponentState,
createComponentProps
}
Redux lib pattern makes Component using surprisingly simple in three steps:
1) Add reducer from lib to root application reducer
2) Pass props to Component via selector from lib
3) Dispatch any necessary action from lib in any place
Step 1:
// application/create-root-reducer.ts
import {constants, createReducer} from 'component-redux-lib';
const reducer = combineReducers({
...
[constants.componentReducerKey]: createReducer(),
...
});
Step 2:
// application/component-container.ts
import {Component} from 'component-package';
import {selectors} from 'component-redux-lib';
const mapStateToProps = state => {
const additionalProps = state.someKey;
return selectors.createComponentProps(selectors.getComponentProps(state), additionalProps)
}
export const ReadyToUseComponent = connect(mapStateToProps)(Component)
Step 3:
// application/anywhere-button.ts
import {actions} from 'component-redux-lib';
const Button = (props) => <button onClick={props.toggleProp1}>
Toggle component prop1
</button>
const mapDispatchToProps = dispatch => ({
toggleProp1: () => dispatch(actions.toggleProp1())
})
export const ReadyToUseButton = connect(null ,mapDispatchToProps)(Button)
If Component should give some data to application it worth to add to lib reducer corresponding fields, actions and pass that actions as callbacks to a Component. This data will be available for all of the application components due to lid reducer is placed to application redux store:
// application/component-container.ts
import {Component} from 'component-package';
import {selectors, actions} from 'component-redux-lib';
const mapStateToProps = state => {
const additionalProps = state.someKey;
return selectors.createComponentProps(selectors.getComponentProps(state), additionalProps)
}
const mapDispatchToProps = (dispatch) => {
giveSomeDataOutside: (internalComponentData) => dispatch(actions.giveSomeDataOutside(internalComponentData));
}
export const ReadyToUseComponent = connect(mapStateToProps, mapDispatchToProps)(Component);
Thus, all that required for interacting and management with Component we encapsulated in one separate module which is simple to control, change, test and develop. But we can use Component in three same simple steps in any application. No longer need to implement it in each application.
Moreover, redux lib pattern could be used for interacting with module which isn't a react component. Let's say, for interacting with a package provides some interface for use an API. We can easily integrate it into a react-redux application using thunk actions. The example is a little bit naive, but it demonstrates the principle:
// web-api-module/index.ts
export class WebApi {
async method1(params: any) {
// do smth
}
async method2(params: any) {
// do smth
}
async method3(params: any) {
// do smth
}
}
// web-api-redux-lib/actions.ts
import {WebApi} from 'web-api-module';
let webApi;
const setSmth1Result = (result: Any) => ({
type: WEB-API-REDUX-LIB/SET-SMTH1,
payload: result
})
const doSmth1 = (params) => async (getState, dispatch) => {
if (webApi === undefined) {
webApi = new WebApi();
}
const result = await webApi.method1(params);
dispatch(setSmth1Result(result));
}
Reducer, selectors, and constants for web-api-redux-lib
create like in the example above.
With redux lib, it is possible to abstract WebApi class instance from application. We can develop, test and even deploy the WebApi package in an independent way. However, integration and using it in a react-redux application will be simple. Moreover, WebApi class can be stateful and redux lib can expose to an application only necessary for UI data. It helps to avoid storing in redux store data which isn't necessary for UI, but developers could rely on.
Described "pattern" has been using for more than a year in our team and proved to be good and really convenient. I hope that approach will help someone to make interacting and management react-redux application with other modules more simple and convenient too.
Posted on March 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.