├── .eslintrc
├── .gitignore
├── .lintignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.md
├── angular.json
├── images
├── branding
│ └── juliette-logo.svg
├── juliette-architecture.png
└── juliette-in-action.gif
├── package-lock.json
├── package.json
├── projects
├── juliette-ng
│ ├── README.md
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ │ ├── lib
│ │ │ ├── effects.mapper.ts
│ │ │ ├── effects.module.ts
│ │ │ ├── store.module.ts
│ │ │ └── tokens.ts
│ │ └── public-api.ts
│ ├── tsconfig.lib.json
│ └── tsconfig.lib.prod.json
├── juliette-react
│ ├── .eslintrc
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── lib
│ │ │ ├── contexts.ts
│ │ │ └── hooks.ts
│ │ └── public-api.ts
│ └── tsconfig.json
├── juliette
│ ├── package.json
│ ├── src
│ │ ├── lib
│ │ │ ├── constants.ts
│ │ │ ├── effects.ts
│ │ │ ├── handlers.ts
│ │ │ ├── helpers.ts
│ │ │ ├── log.ts
│ │ │ ├── models.ts
│ │ │ ├── operators.ts
│ │ │ ├── selectors.ts
│ │ │ └── store.ts
│ │ └── public-api.ts
│ └── tsconfig.json
└── playground-ng
│ ├── .browserslistrc
│ ├── src
│ ├── app
│ │ ├── app-routing.module.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── containers
│ │ │ └── users.component.ts
│ │ ├── core
│ │ │ ├── models
│ │ │ │ └── user.model.ts
│ │ │ └── resources
│ │ │ │ └── users.resource.ts
│ │ ├── feature1
│ │ │ ├── feature1-routing.module.ts
│ │ │ ├── feature1.component.ts
│ │ │ ├── feature1.module.ts
│ │ │ └── store
│ │ │ │ ├── feature1.effects.ts
│ │ │ │ ├── feature1.handlers.ts
│ │ │ │ ├── feature1.selectors.ts
│ │ │ │ └── index.ts
│ │ ├── feature2
│ │ │ ├── feature2-routing.module.ts
│ │ │ ├── feature2.component.ts
│ │ │ ├── feature2.module.ts
│ │ │ └── store
│ │ │ │ ├── feature2.effects.ts
│ │ │ │ ├── feature2.handlers.ts
│ │ │ │ └── index.ts
│ │ ├── package.json
│ │ └── store
│ │ │ ├── effects
│ │ │ └── users.effects.ts
│ │ │ ├── handlers
│ │ │ ├── index.ts
│ │ │ └── users.handlers.ts
│ │ │ ├── index.ts
│ │ │ └── selectors
│ │ │ ├── index.ts
│ │ │ └── users.selectors.ts
│ ├── assets
│ │ └── .gitkeep
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ └── styles.scss
│ └── tsconfig.app.json
├── scripts
├── build.ts
├── paths.ts
├── publish.ts
├── tsconfig.json
└── update-version.ts
└── tsconfig.json
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true,
6 | "jest": true
7 | },
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "prettier/@typescript-eslint"
13 | ],
14 | "parser": "@typescript-eslint/parser",
15 | "plugins": ["@typescript-eslint"],
16 | "rules": {
17 | "@typescript-eslint/explicit-module-boundary-types": [
18 | "error",
19 | {
20 | "allowArgumentsExplicitlyTypedAsAny": true
21 | }
22 | ],
23 | "@typescript-eslint/naming-convention": "error",
24 | "@typescript-eslint/no-explicit-any": "off",
25 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
26 | "@typescript-eslint/no-inferrable-types": "error",
27 | "@typescript-eslint/no-var-requires": "off",
28 | "dot-notation": "error",
29 | "indent": "off",
30 | "new-parens": "error",
31 | "no-bitwise": "off",
32 | "no-extra-boolean-cast": "off",
33 | "no-prototype-builtins": "off",
34 | "no-restricted-imports": ["error", "rxjs/Rx"],
35 | "no-shadow": [
36 | "error",
37 | {
38 | "hoist": "all"
39 | }
40 | ],
41 | "no-undef-init": "error",
42 | "no-var": "error",
43 | "prefer-arrow-callback": "error",
44 | "prefer-const": "error",
45 | "prefer-object-spread": "error",
46 | "prefer-spread": "error",
47 | "radix": "error"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /tmp
4 | /out-tsc
5 | # Only exists if Bazel was run
6 | /bazel-out
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # profiling files
12 | chrome-profiler-events*.json
13 | speed-measure-plugin*.json
14 |
15 | # IDEs and editors
16 | /.idea
17 | .project
18 | .classpath
19 | .c9/
20 | *.launch
21 | .settings/
22 | *.sublime-workspace
23 |
24 | # IDE - VSCode
25 | .vscode/*
26 | !.vscode/settings.json
27 | !.vscode/tasks.json
28 | !.vscode/launch.json
29 | !.vscode/extensions.json
30 | .history/*
31 |
32 | # misc
33 | /.sass-cache
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | npm-debug.log
38 | yarn-error.log
39 | testem.log
40 | /typings
41 |
42 | # System Files
43 | .DS_Store
44 | Thumbs.db
45 |
--------------------------------------------------------------------------------
/.lintignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /dist
3 | /images
4 | /node_modules
5 | *.md
6 | LICENSE
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": true,
4 | "printWidth": 100,
5 | "semi": true,
6 | "singleQuote": true,
7 | "tabWidth": 2,
8 | "trailingComma": "all",
9 | "useTabs": false
10 | }
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12.18
4 | cache:
5 | directories:
6 | - node_modules
7 | notifications:
8 | email: false
9 | script:
10 | - npm run lint
11 | - npm run build
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2021 Marko Stanimirović
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | # Juliette
10 |
11 | [](./LICENSE)
12 | [](https://www.npmjs.com/package/juliette)
13 | [](https://travis-ci.org/markostanimirovic/juliette)
14 | [](https://npmcharts.com/compare/juliette?interval=30)
15 |
16 | **Reactive State Management Powered by [RxJS](https://rxjs-dev.firebaseapp.com/)**
17 |
18 |
23 |
24 | ## Contents
25 |
26 | - [Overview](#overview)
27 | - [Architecture](#architecture)
28 | - [Installation](#installation)
29 | - [Guide](#guide)
30 | - [Handlers](#handlers)
31 | - [Store](#store)
32 | - [Selectors](#selectors)
33 | - [Effects](#effects)
34 | - [Angular Plugin](#angular-plugin)
35 | - [React Plugin](#react-plugin)
36 | - [Examples](#examples)
37 | - [Show Your Support](#show-your-support)
38 | - [License](#license)
39 |
40 | ## Overview
41 |
42 | Juliette is a reactive state management library inspired by [NgRx](https://ngrx.io/).
43 | It reduces Redux boilerplate, eliminates reducer's conditional branching, simplifies
44 | the configuration and introduces NgRx-like architecture into the framework-agnostic world.
45 | Juliette is a TypeScript friendly library and can be used in Angular, React or any JavaScript application.
46 |
47 | ### Reduced Boilerplate Without Conditional Branching
48 |
49 | Juliette reduces Redux boilerplate by merging the action, and the reducer into one component called handler.
50 | To better understand the benefits of the handler, let's first look at how actions and reducers are defined by using NgRx.
51 |
52 |
53 | Old NgRx Approach
54 |
55 | ```typescript
56 | // users.actions.ts
57 |
58 | export const FETCH_USERS = '[Users] Fetch Users';
59 | export const FETCH_USERS_SUCCESS = '[Users] Fetch Users Success';
60 | export const FETCH_USERS_ERROR = '[Users] Fetch Users Error';
61 |
62 | export class FetchUsers implements Action {
63 | readonly type = FETCH_USERS;
64 | }
65 |
66 | export class FetchUsersSuccess implements Action {
67 | readonly type = FETCH_USERS_SUCCESS;
68 |
69 | constructor(public payload: User[]) {}
70 | }
71 |
72 | export class FetchUsersError implements Action {
73 | readonly type = FETCH_USERS_ERROR;
74 | }
75 |
76 | export type Action = FetchUsers | FetchUsersSuccess | FetchUsersError;
77 |
78 | // users.reducer.ts
79 |
80 | import * as UsersActions from './users.actions';
81 |
82 | export interface State {
83 | users: User[];
84 | loading: boolean;
85 | }
86 |
87 | const initialState: State = {
88 | users: [],
89 | loading: false,
90 | };
91 |
92 | export function reducer(state = initialState, action: UsersActions.Action): State {
93 | switch (action.type) {
94 | case UsersActions.FETCH_USERS:
95 | return { ...state, loading: true };
96 | case UsersActions.FETCH_USERS_SUCCESS:
97 | return { ...state, users: action.payload, loading: false };
98 | case UsersActions.FETCH_USERS_ERROR:
99 | return { ...state, users: [], loading: false };
100 | default:
101 | return state;
102 | }
103 | }
104 | ```
105 |
106 |
107 | TypeScript code above shows the old NgRx syntax and it is pretty similar to the traditional Redux approach.
108 | As you can see, it's too much code for three simple actions. Then, NgRx team introduced a new way
109 | to define actions and reducers.
110 |
111 |
112 | New NgRx Approach
113 |
114 | ```typescript
115 | // users.actions.ts
116 |
117 | export const fetchUsers = createAction('[Users] Fetch Users');
118 | export const fetchUsersSuccess = createAction(
119 | '[Users] Fetch Users Success',
120 | props<{ users: User[] }>(),
121 | );
122 | export const fetchUsersError = createAction('[Users] Fetch Users Error');
123 |
124 | // users.reducer.ts
125 |
126 | import * as UsersActions from './users.actions';
127 |
128 | export interface State {
129 | users: User[];
130 | loading: boolean;
131 | }
132 |
133 | const initialState: State = {
134 | users: [],
135 | loading: false,
136 | };
137 |
138 | export const reducer = createReducer(
139 | initialState,
140 | on(UsersActions.fetchUsers, state => ({ ...state, loading: true })),
141 | on(UsersActions.fetchUsersSuccess, (state, { users }) => ({
142 | ...state,
143 | users,
144 | loading: false,
145 | })),
146 | on(UsersActions.fetchUsersError, state => ({
147 | ...state,
148 | users: [],
149 | loading: false,
150 | })),
151 | );
152 | ```
153 |
154 |
155 | With new NgRx syntax, less amount of code is needed to define actions and reducers. Conditional
156 | branching for actions in the reducer is masked by the `on` operator, but it is still present.
157 | Let's now look at how the same example is implemented using Juliette handlers.
158 |
159 |
160 | Juliette Approach
161 |
162 | ```typescript
163 | // users.handlers.ts
164 |
165 | export const featureKey = 'users';
166 |
167 | export interface State {
168 | users: User[];
169 | loading: boolean;
170 | }
171 |
172 | export const initialState: State = {
173 | users: [],
174 | loading: false,
175 | };
176 |
177 | export const fetchUsers = createHandler(
178 | '[Users] Fetch Users',
179 | featureKey,
180 | state => ({ ...state, loading: true }),
181 | );
182 | export const fetchUsersSuccess = createHandler(
183 | '[Users] Fetch Users Success',
184 | featureKey,
185 | (state, { users }) => ({ ...state, users, loading: false }),
186 | );
187 | export const fetchUsersError = createHandler(
188 | '[Users] Fetch Users Error',
189 | featureKey,
190 | state => ({ ...state, users: [], loading: false }),
191 | );
192 | ```
193 |
194 |
195 | As you can see, Juliette way is declarative. Also, the least amount of code is required to define the same logic.
196 | Instead of creating actions and then adding new conditional branches to the reducer, Juliette's handler creator accepts
197 | the reducer function on-site.
198 |
199 | ### Simplified Configuration
200 |
201 | You don't need to register reducers to the store anymore!
202 |
203 | ### Framework Agnostic
204 |
205 | Core features of Juliette are implemented in pure TypeScript. The library is small-sized and has RxJS as the only production dependency.
206 | All framework specific stuff is in separate libraries.
207 |
208 | We currently support Angular and React via dedicated plugins. They provide core functionalities adapted to the framework design.
209 | Of course, Juliette can be used in Angular or React without plugins, but that way wouldn't be native.
210 |
211 | ## Architecture
212 |
213 | Juliette is a great solution for large-scale applications, because merging action and reducer into the handler will reduce the boilerplate,
214 | but won't make a mess in complex systems. Let's look at the diagram.
215 |
216 |
221 |
222 | When an event occurs on the view, it will dispatch the handler. Then, if the handler has a reducer function, it will be executed by the store
223 | and new state will be reflected in the view through the selector. After that, if the handler has a side effect, that effect will be performed.
224 | Lastly, if the effect returns a new handler, the execution process will be repeated.
225 |
226 | ## Installation
227 |
228 | Run `npm install juliette` to install core Juliette library.
229 |
230 | For Angular, install an additional package by running `npm install juliette-ng` command.
231 |
232 | For React, install an additional package by running `npm install juliette-react` command.
233 |
234 | ## Guide
235 |
236 | ### Handlers
237 |
238 | As already mentioned, handler is the component that merges the action and the reducer. You can create the handler by using `createHandler`
239 | function and there are four different ways to do this. Let's look at the simplest first.
240 |
241 | ```typescript
242 | const showCreateTodoDialog = createHandler('[Todos] Show Create Todo Dialog');
243 | ```
244 |
245 | `createHandler` requires only `type` as an argument. `type` is similar to Redux action type and must be unique at the application
246 | level. Another case is when the handler requires a payload whose type must be passed as a generic argument.
247 |
248 | ```typescript
249 | const createTodo = createHandler<{ todo: Todo }>('[Todos] Create Todo');
250 | ```
251 |
252 | The third case is when the handler needs state changes. Then, you need to pass `featureKey` as a second argument. `featureKey` is the key of the state piece
253 | from the application state to which the defined handler refers. The third argument is a function that accepts the old state and returns a new state, similar
254 | to the reducer from Redux.
255 |
256 | ```typescript
257 | const fetchTodos = createHandler(
258 | '[Todos] Fetch Todos',
259 | featureKey,
260 | state => ({ ...state, loading: true }),
261 | );
262 | ```
263 |
264 | If you try to compile the code above, you will get a compilation error. That is because `createHandler` function is strongly typed in order to avoid
265 | potential mistakes. To fix the error, you need to pass the type of todos state as a generic argument.
266 |
267 | ```typescript
268 | const fetchTodos = createHandler(
269 | '[Todos] Fetch Todos',
270 | featureKey,
271 | state => ({ ...state, loading: true }),
272 | );
273 | ```
274 |
275 | The last case is when the handler needs both, the payload and the reducer. Let's see it in action.
276 |
277 | ```typescript
278 | const fetchTodosSuccess = createHandler(
279 | '[Todos] Fetch Todos Success',
280 | featureKey,
281 | (state, { todos }) => ({ ...state, todos, loading: false }),
282 | );
283 | ```
284 |
285 | ### Store
286 |
287 | To create the store, Juliette provides `createStore` function. It accepts the initial application state as the first argument.
288 | The second argument is `devMode` and it's optional. You can enable it when the application is in development mode
289 | in order to log the state and handlers on every dispatch. Also, when `devMode` is enabled, you'll get an error if
290 | try to mutate the state.
291 |
292 | ```typescript
293 | const store = createStore(initialAppState, true);
294 | ```
295 |
296 | To dispatch handlers, the store provides `dispatch` function.
297 |
298 | ```typescript
299 | store.dispatch(fromTodos.fetchTodos());
300 | ```
301 |
302 | There are two ways to get the application state. In both cases, you will get the state as an observable. First way is to get the entire
303 | state by using `state$` property from the store.
304 |
305 | ```typescript
306 | const appState$ = store.state$;
307 | ```
308 |
309 | Second option is to select a partial state. For this purpose, Juliette store provides `select` function. You can pass the key of the feature state
310 | or a selector function that accepts the state as an argument and returns the selected slice.
311 |
312 | ```typescript
313 | const todosState1$ = store.select(fromTodos.featureKey);
314 | const todosState2$ = store.select(state => state[fromTodos.featureKey]);
315 | ```
316 |
317 | Another way to select a state is to use regular RxJS operators.
318 |
319 | ```typescript
320 | const todosState3$ = store.state$.pipe(
321 | pluck(fromTodos.featureKey),
322 | distinctUntilChanged(),
323 | );
324 | const todosState4$ = store.state$.pipe(
325 | map(state => state[fromTodos.featureKey]),
326 | distinctUntilChanged(),
327 | );
328 | ```
329 |
330 | In case you need to initialize a feature state on the fly, there is `addFeatureState` method.
331 |
332 | ```typescript
333 | store.addFeatureState(fromTodos.featureKey, fromTodos.initialState);
334 | ```
335 |
336 | ### Selectors
337 |
338 | Juliette provides `composeSelectors` function for selector composition. It accepts an array of selector functions as the first argument
339 | and composer function as the second argument. Selectors created with `composeSelectors` function are memoized.
340 |
341 | ```typescript
342 | const selectTodosState = (state: AppState) => state[fromTodos.featureKey];
343 | const selectAllTodos = composeSelectors([selectTodosState], state => state.todos);
344 |
345 | const selectInProgressTodos = composeSelectors(
346 | [selectAllTodos],
347 | todos => todos.filter(todo => todo.status === 'IN_PROGRESS'),
348 | );
349 | const selectDoneTodos = composeSelectors(
350 | [selectAllTodos],
351 | todos => todos.filter(todo => todo.status === 'DONE'),
352 | );
353 |
354 | const selectInProgressAndDoneTodos = composeSelectors(
355 | [selectInProgressTodos, selectDoneTodos],
356 | (inProgressTodos, doneTodos) => [...inProgressTodos, ...doneTodos],
357 | );
358 | ```
359 |
360 | ### Effects
361 |
362 | If you need to perform a side effect when some handler is dispatched, the effect component is the right place to do that. This approach to managing
363 | side effects was introduced by the NgRx team and is more reactive and declarative than the use of Redux middleware. To create an effect, create a RxJS
364 | observable that returns a new handler, any other value or nothing. If a new handler is returned, Juliette will dispatch it when the task within
365 | the effect is completed. Otherwise, the returned value will be ignored. Unlike NgRx, where you need to use `createEffect` function and pass
366 | an additional configuration if you want the effect not to return a new handler, with Juliette it will be done automatically. Enough theory, let's move
367 | on to examples.
368 |
369 | Juliette store provides `handlers$` stream that will emit a new value every time when any handler is dispatched. If you need to perform a side effect
370 | when some handler is dispatched, you can filter `handlers$` stream by using `ofType` operator and pass that handler as an argument. Then, the operators
371 | chained after the `ofType` operator will be executed only when passed handler is dispatched.
372 |
373 | ```typescript
374 | const showCreateTodoDialog$ = store.handlers$.pipe(
375 | ofType(fromTodos.showCreateTodoDialog),
376 | tap(() => todosService.showCreateTodoDialog()),
377 | );
378 | ```
379 |
380 | If passed handler has a payload, you can access it in the next operator's callback as an argument.
381 |
382 | ```typescript
383 | const createTodo$ = store.handlers$.pipe(
384 | ofType(fromTodos.createTodo),
385 | switchMap(handler => todosService.createTodo(handler.payload.todo)),
386 | );
387 | ```
388 |
389 | Juliette also provides `toPayload` operator that will extract the payload from the dispatched handler.
390 |
391 | ```typescript
392 | const createTodo$ = store.handlers$.pipe(
393 | ofType(fromTodos.createTodo),
394 | toPayload(),
395 | switchMap(({ todo }) => todosService.createTodo(todo)),
396 | );
397 | ```
398 |
399 | When the effect needs data from the store, you can use `withLatestFrom` operator. If you need to dispatch a new handler when the effect task
400 | is completed, you can return it from the last operator in the chain.
401 |
402 | ```typescript
403 | const fetchTodos$ = store.handlers$.pipe(
404 | ofType(fromTodos.fetchTodos),
405 | withLatestFrom(store.select(fromTodos.featureKey)),
406 | switchMap(([, { search, currentPage, itemsPerPage }]) =>
407 | todosService.getTodos(search, currentPage, itemsPerPage).pipe(
408 | map(todos => fromTodos.fetchTodosSuccess({ todos })),
409 | catchError(() => of(fromTodos.fetchTodosError())),
410 | ),
411 | ),
412 | );
413 | ```
414 |
415 | Also, `ofType` operator can accept a sequence of handlers as an argument. This allows multiple handlers to be listened to in the same effect.
416 |
417 | ```typescript
418 | const invokeFetchTodos$ = store.handlers$.pipe(
419 | ofType(
420 | fromTodos.updateSearch,
421 | fromTodos.updateCurrentPage,
422 | fromTodos.updateItemsPerPage,
423 | ),
424 | map(() => fromTodos.fetchTodos()),
425 | );
426 | ```
427 |
428 | When the effect needs to dispatch multiple handlers, you can return them in array by using `switchMap` or `mergeMap` operators.
429 |
430 | ```typescript
431 | const resetPagination$ = store.handlers$.pipe(
432 | ofType(fromTodos.resetPagination),
433 | switchMap(() => [
434 | fromTodos.updateCurrentPage({ currentPage: 1 }),
435 | fromTodos.updateItemsPerPage({ itemsPerPage: 10 }),
436 | ]),
437 | );
438 | ```
439 |
440 | Finally, use `registerEffects` function to start up the effects machinery.
441 |
442 | ```typescript
443 | registerEffects(store, [
444 | showCreateTodoDialog$,
445 | createTodo$,
446 | fetchTodos$,
447 | invokeFetchTodos$,
448 | resetPagination$,
449 | ]);
450 | ```
451 |
452 | ### Angular Plugin
453 |
454 | [JulietteNg](https://github.com/markostanimirovic/juliette/tree/master/projects/juliette-ng) library has additional functionalities for using
455 | Juliette in the Angular way. Instead of creating the store via `createStore` function, it provides `StoreModule` to do so.
456 |
457 | ```typescript
458 | @NgModule({
459 | ...
460 | imports: [
461 | ...
462 | StoreModule.forRoot(initialAppState, !environment.production),
463 | ],
464 | })
465 | export class AppModule {}
466 | ```
467 |
468 | `forRoot` method from `StoreModule` accepts the same arguments as `createStore` function. Creating the store using `StoreModule` allows the store
469 | to be injected as a service within any Angular component or service.
470 |
471 | ```typescript
472 | @Component({
473 | ...
474 | })
475 | export class TodosComponent {
476 | todosState$ = this.store.select(fromTodos.featureKey);
477 |
478 | constructor(private store: Store) {}
479 |
480 | fetchTodos(): void {
481 | this.store.dispatch(fromTodos.fetchTodos());
482 | }
483 | }
484 | ```
485 |
486 | To initialize the feature state in lazy-loaded module, use `forFeature` method.
487 |
488 | ```typescript
489 | @NgModule({
490 | ...
491 | imports: [
492 | ...
493 | StoreModule.forFeature(fromTodos.featureKey, fromTodos.initialState),
494 | ],
495 | })
496 | export class TodosModule {}
497 | ```
498 |
499 | To register the effects, JulietteNg provides `EffectsModule`.
500 |
501 | ```typescript
502 | @NgModule({
503 | ...
504 | imports: [
505 | ...
506 | EffectsModule.register([TodosEffects]),
507 | ],
508 | })
509 | export class AppModule {}
510 | ```
511 |
512 | `register` method from `EffectsModule` accepts an array of classes with effects and can be used in both, root and feature modules.
513 | By creating effects within the class, you can use all the benefits of dependency injection.
514 |
515 | ```typescript
516 | @Injectable()
517 | export class TodosEffects {
518 | fetchTodos$ = this.store.handlers$.pipe(
519 | ofType(fromTodos.fetchTodos),
520 | withLatestFrom(this.store.select(fromTodos.featureKey)),
521 | switchMap(([, { search, currentPage, itemsPerPage }]) =>
522 | this.todosService.getTodos(search, currentPage, itemsPerPage).pipe(
523 | map(todos => fromTodos.fetchTodosSuccess({ todos })),
524 | catchError(() => of(fromTodos.fetchTodosError())),
525 | ),
526 | ),
527 | );
528 |
529 | constructor(private store: Store, private todosService: TodosService) {}
530 | }
531 | ```
532 |
533 | ### React Plugin
534 |
535 | [JulietteReact](https://github.com/markostanimirovic/juliette/tree/master/projects/juliette-react) library contains custom hooks
536 | for easier state accessibility within the React components. To use them, provide the store via `StoreContext`.
537 |
538 | ```typescript jsx
539 | ReactDOM.render(
540 |
541 |
542 | ,
543 | document.getElementById('root'),
544 | );
545 | ```
546 |
547 | This plugin provides `useSelect` hook that accepts a selector function or feature key and `useDispatch` hook that returns the dispatch function.
548 |
549 | ```typescript jsx
550 | function Todos() {
551 | const todosState = useSelect(fromTodos.featureKey);
552 | const dispatch = useDispatch();
553 |
554 | return (
555 |