Choosing and committing to a framework is often a hard decision. Redux has a strong architectural model, that transcends frameworks. Writing your code in a framework agnostic way, offers a hedge on your framework bets.
It is also a common desire to share code across applications. For example, you may want a React Native app to share features with your web or mobile web versions.
Furthermore, aging AngularJS 1 components, or even J-Query soup, often dominate real-life code bases. Big bang rewrites are expensive and risky. How can you refactor towards a new improved architecture?
In this post, I describe an approach to build your core client logic in a framework agnostic and reusable way.
A common approach to organizing your code files is to put them into directories that match the file’s role. Reducers are in one directory, actions are in another, etc.
This is a pain because you’re likely to be changing the actions, reducer and side effects when making a single functional change. This means you must navigate around your project to make related changes.
|Type of File Organization||Functional Organization|
But more importantly, this makes it difficult to share code across different projects.
A good first step in creating reusability for redux code is to simply group files that are related into the same directory. For this discussion, we will define a collection of related redux files (actions, reducers, side effect managers, etc.) as a redux package.
A further step in promoting reusability, is to be deliberate about interface. One normally thinks about this when designing an API. When considering an interface to a redux package, some patterns emerge.
Subscribing to State Change
A key aspect of the redux package interface strategy, is that any service or presentation component that is interested in the state data, simply subscribes to it. Having made the subscription, it merely waits for data to appear. And that’s the only way to get state.
Typescript makes an invaluable contribution to defining the interface because you can define the attributes of the state and benefit with code completion in your IDE and build and edit time error detection.
Actions as Interface
Actions define part of the interface:
- Consumers of a redux package need to conveniently fire an action. This can be done be creating methods in an exported action class. These methods could dispatch the action directly (LoginActions.login(userId, password) ). Or, the method could be an action object factory that consumers could use in RxJS chains. (e.g. Observeable.of( ConnectActions.successFactory(serverURL) ) )
- You may also want to hide some internal actions. For example, the consumer of the component may want to query a server, but does not need to respond to the related server response action (because they will subscribe to state instead). A way to hide internal actions is to have a “private” actions class that inherits the public actions. This private actions class is used by internal files and is not exported.
- Some actions generated by the redux packages are of interest to consumers. For example, if you have logged in successfully, the app may automatically navigate to a home screen. You can expose a subset of actions as string constants in your actions class so other packages can watch for it and respond.
It is best practice to keep your redux state as minimal as practical. (This is the equivalent to a highly-normalized database.) But there may be many useful derivations that you want package consumers to leverage.
Your redux package can expose pure functions that take the state and other parameters as inputs. For example, your Login package may provide functions to compute things like display name, initials or avatar URL.
Server and Cloud Interfaces
Typically, with redux, connections to servers are managed through asynchronous tooling such as redux-observables or redux middleware. In many cases, the redux package will be tightly bound to the service. You gain some service-portability because, if you want to swap out services, you can implement another “action and state” compatible redux package.
Another strategy is to define a service interface and inject it into the redux package. For example, a login redux package could define a service that includes methods such as “register” or “login”. When instancing the package, inject a service that implements the interface. The redux package will call the service that was injected.
Continuing with the login example, this approach would allow for a register and login component to work with multiple back end services.
Injecting a service comes at the cost of the tyranny of common denominators. Each service you are wrapping must express itself in the interface. You may find the resulting contortions and limitations not worth the code-reuse gained.
Creating a tool to configure the packages and define some interfaces can further reduce API surface area and increase commonality. For example, rather than having package consumers implement boilerplate code for each package, a package manager can be responsible for combining the reducers and middleware. This can be accomplished by defining an abstract base class for each redux package to implement.
The package manager can also provide commonality that can be leveraged across frameworks. For example, in Angular 2+, the library “angular-redux/store” can provide the “select” method for subscribing to state changes. In React, this feature can be provided by the package manager itself.
Conclusion and Source Code Examples
Redux functional units can be re-used with varying degrees of formality. By grouping your reducers, actions, asynchronous handlers and functions, you push code into a platform agnostic layer. Your only hard dependency is on a Redux (or NgRx) approach. However, TypeScript, RxJS and Redux-Observable, immutable-js, and typed-immutable-record are also useful and relatively light weight components that you may want make part of your common tool set.
An experimental, early stage, implementation of a package manager is at: https://github.com/kokokenada/redux-package.
An experimental, early stage, library of redux-packages is at: https://github.com/kokokenada/common-app.
An experimental app that is (slowly) trying to leverage these techniques to share code between React Native and Angular 2+ is at https://github.com/kokokenada/for-real-cards and https://github.com/kokokenada/for-real-cards-rn.