├── .gitignore
├── LICENSE.txt
├── README.md
├── example
└── simple-counter
│ ├── containers
│ └── App.tsx
│ ├── index.tsx
│ ├── redux
│ ├── actions
│ │ ├── counter.ts
│ │ └── index.ts
│ ├── connector.ts
│ ├── interfaces
│ │ ├── actions.ts
│ │ ├── app.ts
│ │ └── state.ts
│ └── paths.ts
│ └── webpack.config.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── connector.test.tsx
├── connector.ts
├── createApp.ts
├── implementAction.ts
├── index.ts
├── paths.test.ts
├── paths.ts
├── selectors.test.ts
├── selectors.ts
├── types.ts
├── typesafeRedux.ts
└── utils
│ ├── comparators.test.ts
│ ├── comparators.ts
│ ├── flow.ts
│ ├── get.test.ts
│ ├── get.ts
│ ├── ofType.ts
│ ├── set.test.ts
│ ├── set.ts
│ ├── unset.test.ts
│ └── unset.ts
├── test
├── app.ts
├── composability.test.tsx
├── lib.ts
└── testability.test.ts
├── tsconfig.json
├── typings
└── lodash-fp.d.ts
├── usage-diagram-lg.png
└── usage-diagram-sm.png
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .vscode
4 | compiled
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | npm i types-first-ui
3 | ```
4 |
5 | # Types-First UI
6 |
7 | Types-First UI is an opinionated framework for building long-lived, maintainable UI codebases. It uses TypeScript and the power of its type system to guarantee the completeness and correctness of a Redux backend that can be connected to React components in a performant way. It places the focus on first defining your system in terms of the data and types that will drive it, then filling in the blanks. This project is inspired heavily by [re-frame](https://github.com/Day8/re-frame).
8 |
9 | ## Who is this for?
10 |
11 | Well, first and foremost, it's for us at Avero. This library represents the codification and enforcement of what we have established as our best practices for building interfaces. However, we believe that there is significant value in this approach and that it is worth sharing.
12 |
13 | Hopefully, there are others who can use and benefit from this work directly. But even if not, we think it is valuable to share our approach and give visibility into how one group of engineers chooses to approach some of the hard problems of building maintainable UI codebases.
14 |
15 | More generally, this project might be for you if you believe...
16 |
17 | - Types are important
18 | - TypeScript is good
19 | - Flat state tree -- all reducers have access to the entire tree
20 | - Actions may have a reducer, an epic, both, or neither. M:N relationship between Actions => Redux primitives
21 | - Epics (backed by redux-observable) as mechanism for side effects/middleware (vs. thunks or sagas)
22 | - Observables are pretty dope
23 |
24 | ## **Philosophy**
25 |
26 | Redux is an event dispatching system that operates on a single global state atom that is only modifiable via actions. It makes little sense to begin building your application before defining two interfaces: the shape of your state and the shape of all actions that operate on that state. From there we can leverage the strict unidirectional flow of Redux in our type system.
27 |
28 | You should be familiar with the basic terminology of Redux (Actions, Reducers, Action Creators, Stores, Middleware) and RxJS (Observables, Epics) before reading further. The documentation for [Redux](https://redux.js.org/) and [Epics](https://redux-observable.js.org/docs/basics/Epics.html) are excellent, so we'd strongly encourage reading it.
29 |
30 | This project aims to facilitate this "types-first" style of application design while providing utilities focused around maximizing type safety and maintaining interop with existing Redux libraries.
31 |
32 | ## **Anti-Pitch**
33 |
34 | If you dislike types "getting in your way" or think of them as secondary to your application, then this is not the framework for you. At Avero we believe in starting with your API first, which in this case is the interfaces of your application.
35 |
36 | If you do not care about the long-term maintainability of your codebase, such as writing a todo app or a school project, this may be overkill for you. This framework is designed and optimized for building large, production UI projects that will have a long lifespan and many contributors.
37 |
38 | You will occasionally run into cryptic error messages (e.g. key inference on Paths) due to relying heavily on type inference. Deep lookup types + mapped types + conditional types + inference makes the compiler work pretty hard... which leads to the next point.
39 |
40 | VS Code is a bit sluggish with this right now. You will not receive the instant 10ms feedback you may be used to, so there is a tradeoff here. Hopefully this will improve as the compiler matures around these newer type features. When I tried Webstorm it was not giving me the same level of inference as VS Code (Types-First UI requires TS 2.9+).
41 |
42 | ## Usage
43 |
44 | Here's a diagram showing the following usage steps. Click to embiggen.
45 |
46 |
47 |
48 | Let's create a Redux counter application with Types-First UI. We will include basic pending/error handling and a mock API call to show slightly more complexity than the normal counter app.
49 |
50 | ```typescript
51 | // 1. Create State Tree interface / initialState
52 | export interface State {
53 | counter: number;
54 | error: string;
55 | pendingRequest: boolean;
56 | };
57 |
58 | export const initialState: State = {
59 | counter: 0,
60 | error: '',
61 | pendingRequest: false
62 | };
63 |
64 | // 2. Create ActionTypes enum
65 | export enum ActionTypes = {
66 | ADD_REQUEST = 'ADD_REQUEST',
67 | ADD_FAIL = 'ADD_FAIL'
68 | ADD_SUCCESS = 'ADD_SUCCESS'
69 | };
70 |
71 | // 3. Create Action interfaces
72 | export interface Actions {
73 | [ActionTypes.ADD_REQUEST]: { type: ActionTypes.ADD_REQUEST, payload: { tryAdd: number } };
74 | [ActionTypes.ADD_FAIL]: { type: ActionTypes.ADD_FAIL, payload: { error: string } };
75 | [ActionTypes.ADD_SUCCESS]: { type: ActionTypes.ADD_SUCCESS, payload: { newCount: number} };
76 | };
77 |
78 | export type AppActions = Actions[keyof Actions]; // creates Union type of actions that we will pass in to createTypesafeRedux
79 | ```
80 |
81 | The first thing we want to do is define the interfaces of our application. The entire UI depends on state and action interfaces, so it makes sense to start with them first. Additionally, most maintenance work involves adding or modifying actions, so keeping them in one place is helpful for maintainability. Any changes to these interfaces should propagate down to the rest of the system. When we talk about "unidirectional" types, we are referring to these two interfaces driving downstream functions and utilities.
82 |
83 | ```typescript
84 | // 4. Define any Epic Dependencies
85 | export interface EpicDependencies {
86 | // Represents API call
87 | counterAddSvc: {
88 | add: (tryAdd: number) => Observable<{ newCount: number } | { error: string }>;
89 | };
90 | }
91 | ```
92 |
93 | Our Redux application has little knowledge of the outside world. Its focus is around actions and how those actions affect the state atom. We use [Epics](#epics) as a primitive for side effects, but the logic of those side effects should live behind "Services", which generically encapsulate any API that returns an observable. This is helpful for testing and separation of concerns.
94 |
95 | ```typescript
96 | // 5. Pass our interfaces to createTypesafeRedux
97 | import { createTypesafeRedux } from 'types-first-ui';
98 |
99 | export const { path, selector, action, createApp } = createTypesafeRedux<
100 | State,
101 | AppActions,
102 | EpicDependencies
103 | >();
104 | ```
105 |
106 | Now we can use these interfaces to drive our typesafe "utility" functions. These are the functions you will use to build your Redux application. Any changes to the interfaces described above will be immediately reflected in these function signatures.
107 |
108 | ```typescript
109 | // 6a. Create paths using the 'path' utility function from createTypesafeRedux
110 | export const Paths = {
111 | counter: path(['counter'], 0),
112 | error: path(['error'], ''),
113 | pendingRequest: path(['pendingRequest']),
114 | };
115 |
116 | // 6b. Create selectors using 'selector' util function from createTypesafeRedux
117 | export const doubleCounter = selector(Paths.counter, counter => {
118 | return counter * 2;
119 | });
120 | ```
121 |
122 | We now use the utility functions from `createTypesafeRedux` to create our [Paths](#path) and [Selectors](#selector). Generically, these represent observables of derived values from your state tree. Specifically, Paths are a directly referenceable property on your state tree; Selectors are derived values that are computed as a function of input Paths and Selectors.
123 |
124 | Additionally, Paths include utility get, set, & unset functions that will be used in your reducers.
125 |
126 | ```typescript
127 | import { flow } from 'types-first-ui';
128 |
129 | // 7. Implement Actions: use 'action' util function from createTypesafeRedux
130 | const addRequest = action(ActionTypes.ADD_REQUEST, {
131 | reducer: (state, action) => {
132 | // action inferred as {type: ActionTypes.ADD_REQUEST, payload: {tryAdd: number} }
133 | return Paths.pendingRequest.set(true)(state);
134 | },
135 | epic: (action$, { counterAddSvc }) => {
136 | return action$.pipe(
137 | mergeMap(action => {
138 | // action inferred as {type: ActionTypes.ADD_REQUEST, payload: {tryAdd: number} }
139 | return counterAddSvc.add(action.payload.tryAdd).pipe(
140 | map(newCount => {
141 | // API call was successful
142 | return addSuccess.creator({ newCount });
143 | }),
144 | catchError(error => {
145 | // API call failed
146 | return addFail.creator({ error });
147 | })
148 | );
149 | })
150 | );
151 | },
152 | });
153 |
154 | const addSuccess = action(ActionTypes.ADD_SUCCESS, {
155 | reducer: (state, action) => {
156 | const togglePending = Paths.pendingRequest.set(false);
157 | const updateCounter = Paths.counter.set(action.payload.newCount);
158 | return flow(
159 | togglePending,
160 | updateCounter
161 | )(state);
162 | },
163 | });
164 |
165 | const addFail = action(ActionTypes.ADD_FAIL, {
166 | reducer: (state, action) => {
167 | const togglePending = Paths.pendingRequest.set(false);
168 | const updateError = Paths.error.set(action.payload.error);
169 | return flow(
170 | togglePending,
171 | updateError
172 | )(state);
173 | },
174 | });
175 |
176 | export const ActionsMap = {
177 | [ActionTypes.ADD_REQUEST]: addRequest,
178 | [ActionTypes.ADD_SUCCESS]: addSuccess,
179 | [ActionTypes.ADD_FAIL]: addFail,
180 | };
181 | ```
182 |
183 | This is the primary focus of the developer when creating Redux applications. Given a set of actions and a state tree, we now need to implement the concrete instances of these actions.
184 | This may include reducers, epics, both, or neither. The return type of `action` is an [ActionImplementation](#action-implementation). The exported ActionsMap represents an exhaustive collection of implementations for each action type that we defined in our initial interfaces. It will be passed into our `createApp` function below.
185 |
186 | ```typescript
187 | // 8. Create the app instance
188 | const app = createApp({
189 | actions: ActionsMap,
190 | initialState,
191 | });
192 |
193 | // 9. Initialize the app instance by creating & binding to a new redux store
194 | app.createStore({
195 | epicDependencies: {
196 | // concrete instance of interface
197 | counterAddSvc: CounterAddSvc,
198 | },
199 | // enables redux dev tools
200 | dev: true,
201 | });
202 |
203 | export default app;
204 | ```
205 |
206 | With our concrete instances, we can now create our app instance. This is used to bridge React and Redux--the app exposes a `connect` function that mirrors the Redux variation, except with support for our path and selector primitives. We use an explicit import of the app rather than a `Provider` component using React's context API.
207 |
208 | ```typescript
209 | import { ActionCreator } from 'types-first-ui';
210 | // helper utility to extract ActionCreator type given the discriminant
211 | // useful to minimize boilerplate in ActionProps
212 | type Creator = ActionCreator>;
213 |
214 | // 10. Define a React component with Types-First framework
215 | interface DataProps {
216 | counter: number;
217 | doubleCounter: number;
218 | }
219 |
220 | interface ActionProps {
221 | addRequest: Creator;
222 | }
223 |
224 | type Props = DataProps & ActionProps;
225 |
226 | export class CounterComponent extends React.PureComponent {
227 | add = () => {
228 | this.props.addRequest({ tryAddBy: 1 });
229 | };
230 |
231 | render() {
232 | return (
233 |
234 |
{this.props.counter}
235 |
{this.props.doubleCounter}
236 |
237 |
238 | );
239 | }
240 | }
241 |
242 | // 11. Connect your React component to the TFUI app
243 | const observableProps = {
244 | counter: Paths.COUNTER,
245 | doubleCounter: Selectors.doubleCounter,
246 | };
247 |
248 | const dispatchProps = {
249 | addRequest: app.actionCreator(ActionTypes.COUNTER_ADD_REQUEST),
250 | };
251 |
252 | export default app.connect(
253 | observableProps,
254 | dispatchProps
255 | )(CounterComponent);
256 | ```
257 |
258 | ## **Concepts**
259 |
260 | ### Selector
261 |
262 | A selector is an observable representing some directly derivable value from your state atom. Selectors can be recursively combined to create other selectors. These are conceptually similar to [ngrx](https://github.com/ngrx/platform/blob/master/docs/store/selectors.md) selectors, as well as [reselect](https://github.com/reduxjs/reselect).
263 |
264 | Selectors are closed over an observable of the state tree, and include a number of performance optimizations to guarantee that they will be shared, they will not leak subscriptions, they will emit at most once per change to the state tree, and they will only evaluate their projector function when their input values change. Selectors may optionally provide a comparator function to determine when new values should be emitted, for further performance optimizations. This is useful for selectors that return values which are not referentially equal (i.e. mapping over an array).
265 |
266 | Selectors are created using the `selector` utility function.
267 |
268 | ```typescript
269 | interface Todo {
270 | text: string;
271 | completed: boolean;
272 | }
273 |
274 | interface State {
275 | app: {
276 | todos: {
277 | [todoId: string]: Todo;
278 | };
279 | };
280 | }
281 |
282 | export const Paths = {
283 | TODOS: path(['app', 'todos']),
284 | };
285 |
286 | // Example selector usage--usually more than one input selector
287 | export const completedTodos = selector(Paths.TODOS, todos => {
288 | return _.pickBy(todos, todo => {
289 | return todo.completed;
290 | });
291 | });
292 | // completedTodos is type Observable<[todoId: string]: Todo>
293 |
294 | // selectors may also be parameterized
295 | // Here is an example that returns a selector curried over a parameter
296 | export const todoById = (id: string) => selector(Paths.TODOS, todos => todos[id]);
297 | ```
298 |
299 | ### Path
300 |
301 | ```typescript
302 | export interface PathAPI {
303 | get: (state: TState) => TVal;
304 | set: (nextVal: TVal) => (state: TState) => TState;
305 | unset: (state: TState) => TState;
306 | }
307 | export declare type Path = Selector &
308 | PathAPI;
309 | ```
310 |
311 | A path is a selector with special properties and constraints. Paths represent observables of some subtree of your state atom. A path is constructed by providing the literal path to a subtree of your state interface. It is an observable that will emit whenever the focused piece of the state tree has changed (i.e. it is no longer referentially equal to its previous value). Paths are the primitive from which we compute other derived values, through selectors. For every subtree of your state tree, there should be a corresponding Paths object.
312 |
313 | Because paths are bound directly to a piece of the state tree, they also include typesafe, non-mutating get, set, and unset functions which are used in our reducers.
314 |
315 | Paths are created using the `path` utility function.
316 |
317 | ```typescript
318 | // Example use of path
319 | interface State {
320 | app: {
321 | counter: number;
322 | username: string;
323 | counterById: {
324 | [counterId: string]: number;
325 | };
326 | };
327 | }
328 |
329 | export const Paths = {
330 | COUNTER: path(['app', 'counter'], 0), // optional default argument,
331 | // Paths.COUNTER is type Observable & { get: (State) => number, set: (number) => StateTransform, unset: StateTransform }
332 | USERNAME: path(['app', 'username'], ''),
333 | // Paths.USERNAME is type Observable & { get: (State) => string, set: (string) => StateTransform , unset: StateTransform}
334 | BAD: path(['app', 'badPath']),
335 | // ERROR: argument of 'badPath' is not assignable to 'counter' | 'username'
336 | };
337 | ```
338 |
339 | ### Action Implementation
340 |
341 | ```typescript
342 | export interface ActionImplementation<
343 | TAction extends TAllActions,
344 | TState extends object,
345 | TAllActions extends Action,
346 | TEpicDependencies extends object
347 | > {
348 | constant: TAction['type'];
349 | creator: ActionCreator;
350 | reducer?: IReducer;
351 | epic?: SingleActionEpic;
352 | }
353 | ```
354 |
355 | This interface represents the "implementation" of a single action described in the system. It is the return type of the `action` function provided the framework. This provides strict type safety around your Redux primitives, as well as providing a useful grouping of tightly related code. The community has referred to this pattern as the [ducks](https://github.com/erikras/ducks-modular-redux) pattern. Action implementations are where we combine our typesafe utilities into meaningful business logic that will drive the functionality of our application.
356 |
357 | Actions are implemented using the `action` utility function. When implementing an action you may provide a reducer or an epic for that action; you must exhaustively implement every action defined in your action types before you will be able to create an app instance.
358 |
359 | ```typescript
360 | // the return value of the action utility is the full action implementation including
361 | // a typesafe creator function and reference to the type constant
362 | const addRequest = action(ActionTypes.ADD_REQUEST, {
363 | // reducers receive the full, flat state tree
364 | // type of action object is correctly inferred
365 | reducer: (state, action) => {
366 | // We use our path to return an update state tree
367 | return Paths.pendingRequest.set(true)(state);
368 | },
369 | // action epics are scoped to the specific action being implemented
370 | // notice there is no need to use ofType() here, because it is already
371 | // a stream of the ActionTypes.ADD_REQUEST action
372 | epic: (action$, { counterAddSvc }) => {
373 | return action$.pipe(
374 | // type of action object is correctly inferred
375 | mergeMap(action => {
376 | return counterAddSvc.add(action.payload.tryAdd).pipe(
377 | map(newCount => {
378 | // API call was successful
379 | return addSuccess.creator({ newCount });
380 | }),
381 | catchError(error => {
382 | // API call failed
383 | return addFail.creator({ error });
384 | })
385 | );
386 | })
387 | );
388 | },
389 | });
390 | ```
391 |
392 | ### Epics
393 |
394 | Types-First UI is built on top of redux-observable. We use epics as the primitive for managing asynchrony and side-effects within our system.
395 |
396 | In general, epics meet the contract of actions in, actions out. For a full exploration of the concept, you should refer to the [redux observable guide](https://redux-observable.js.org/docs/basics/Epics.html). However, we also provide a set of more constrained, specific epic definitions: single-action epics, middleware.
397 |
398 | ```typescript
399 | export declare type Epic<
400 | TWatchedAction extends Action,
401 | TReturnedAction extends Action,
402 | TEpicDependencies extends object
403 | > = (
404 | action$: Observable,
405 | deps: TEpicDependencies
406 | ) => Observable;
407 | ```
408 |
409 | #### Single-Action Epic
410 |
411 | Single-action epics are provided as part of an action implementation. They are scoped to the specific action being implemented, and only have access to the stream of all actions emitted from the system as the third parameter. As a user, this means you do not have to use the `ofType()` operator within your action epics to narrow the action stream. This also guarantees that action implementations do not bleed concerns. Although most single action epics won't care about the allActions stream, it is useful for use cases such as cancellation.
412 |
413 | ```typescript
414 | export declare type SingleActionEpic<
415 | TAllActions extends Action,
416 | TAction extends TAllActions,
417 | TEpicDependencies extends object
418 | > = Epic;
419 | ```
420 |
421 | ```typescript
422 | const addRequest = action(ActionTypes.ADD_REQUEST, {
423 | // action epics are scoped to the specific action being implemented
424 | // notice there is no need to use ofType() here, because it is already
425 | // a stream of the ActionTypes.ADD_REQUEST action
426 | epic: action$ => {
427 | return action$.pipe(
428 | // type of action object is correctly inferred
429 | map(action => {
430 | //...
431 | })
432 | );
433 | },
434 | });
435 | ```
436 |
437 | #### Middleware
438 |
439 | Not to be confused with [Redux middleware](https://redux.js.org/advanced/middleware), this represents a particular type of "Epic"--one that never returns new actions. This is a good place to put "side effects" in your application (logging, tracing, setting context/storage, persistence, etc).
440 |
441 | ```typescript
442 | export declare type MiddlewareEpic<
443 | TAllActions extends Action,
444 | TEpicDependencies extends object
445 | > = Epic;
446 | ```
447 |
448 | For example, we could have:
449 |
450 | ```typescript
451 | import { empty } from 'rxjs';
452 | import { tap, mergeMapTo } from 'rxjs/operators';
453 |
454 | const logEverything: Middleware = actions$ => {
455 | return actions$.pipe(
456 | tap(console.log),
457 | mergeMapTo(empty())
458 | );
459 | };
460 | ```
461 |
462 | ### App
463 |
464 | App is the atomic unit of functionality in Types-First UI. Apps are recursively composable, which is cool...but scary and complicated. So I'm going to wait to document this feature.
465 |
466 | ## Foundational Technologies
467 |
468 | - [TypeScript](https://github.com/Microsoft/TypeScript)
469 | - [Redux](https://github.com/reactjs/redux)
470 | - [RxJS](https://github.com/ReactiveX/rxjs)
471 | - [Redux-Observable](https://redux-observable.js.org/)
472 |
--------------------------------------------------------------------------------
/example/simple-counter/containers/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import app from '../redux/connector';
3 | import { ActionTypes, AppActions } from '../redux/interfaces/actions';
4 | import { Paths } from '../redux/paths';
5 | import { Creator } from '../redux/interfaces/app';
6 |
7 | interface DataProps {
8 | counter: number;
9 | }
10 |
11 | interface ActionProps {
12 | add: Creator;
13 | subtract: Creator;
14 | }
15 |
16 | type Props = DataProps & ActionProps;
17 |
18 | export class App extends React.PureComponent {
19 | add = () => {
20 | this.props.add({ tryAddBy: 1 });
21 | };
22 |
23 | subtract = () => {
24 | this.props.subtract({ subtractBy: 1 });
25 | };
26 |
27 | render() {
28 | return (
29 |