An experimental library to do Redux in Angular with the new Ivy Engine and higher-order components.
🔥 Why you should use @nanux/store?
- 🧙 Selectors are automagically generated!
- 🏎️ Straight forward implementation and in only a few steps you can have a Redux store up and running
- 🐛 Redux Dev Tools support
- 👷 Encourage functional and reactive programming
There are great solutions including vanilla Redux implementations but most of them end in a very verbose code.
There are two ways to use it:
1. Import NanuxStore
in the app.module.ts
import { NanuxStore } from '@nanux/store';
@NgModule({
declarations: [...],
imports: [
...
NanuxStore.forRoot({ decorators: true })
]
bootstrap: [...]
})
export class AppModule {}
Note: decorators
enable the use of the brand new reducer
decorator.
2. Create State
import { Reducer, GetStore, Store } from '@nanux/store';
export enum TODOs {
INCREMENT = '[UI] INCREMENT'
}
@Injectable({
providedIn: 'root',
})
@GetStore('counter', { counter: 0 })
export class StateService {
counter$: Observable<number>;
@Reducer(TODOs.INCREMENT)
increment = (state) => ({ ...state, counter: state.counter + 1 });
constructor(public store: Store) { }
increment(: void {
this.store.dispatch(new Action(TODOs.INCREMENT));
}
}
Done! Easy-peasy!..... Wait, wait... How do I access the store data?
4. Well, behind the scenes the selectors are automagically generated!
What name do my selectors have?
@nanux/store uses the schema of the initial state you defined to generate the selectors with the same name plus $
symbol.
In the example above counter$: Observable<number>;
has access to counter
value in the store.
Why do I need to declare my selectors properties? There is a limitation of Typescript that enforces us to declare the properties that will store the selectors otherwise, the compiler will yell because it currently does not have a way to know that new properties were dynamically generated by @nanux/store.
To know more about this issue go here
5. Consume the selector in your component by <h1>Nanux! {{ appState.counter$ | async }}</h1>
Not feeling that adventurous to go with the reducer decorator? no problem
This library also supports the traditional switch style to declare reducers.
1. Create a new file for the reducer and remove the initial state from the GetStore
decorator
export const initialState: State = {
counter: 0
};
export const reducer = (state = initialState, action: Action): State => {
switch (action.type) {
case TODOs.GET_DATA: {
return { ...state, counter: state.counter + action.payload };
}
default:
return state;
}
};
2. Go to the app.module
and define thereducerMap
config by importing the reducer we created above
@NgModule({
imports: [
...
NanuxStore.forRoot({ reducerMap: { counterState: reducer })
]...
})
That's it! ❤️
@nanux/store relies on the observable data service and facade patterns, these are simple, keep the componets clean with no heavy logic and removes the need of having an extra package.
Let's update our state.service
file. For the propuse of this example, assume that we have a service that returns your favorite emojis.
- Add a new property in the state
{ counter: 0, emojis: [] }
- Create a function to run the request along with the actions that we want to trigger
public getMyFavoriteEmojis() {
this.store.dispatch(new Action(TODOs.GET_EMOJIS));
const request$ = this.api.getBestEmojis().pipe(
tap((data) => this.store.dispatch(new Action(TODOs.GET_EMOJIS_SUCCESS, data))),
catchError(() => {
this.store.dispatch(new Action(TODOs.GET_EMOJIS_FAIL));
return empty();
}));
return request$.subscribe();
}
- Call the new function in the component
constructor(public state: AppState) {
this.state.getMyFavoriteEmojis();
}
Do you find weird subscribing to an observable in a service? At first you might feel dirty but if you re-think it, it's the same subscription we would do in a component. In case that you want to cancel the observable, you can do it whenever you want, unsubscription is always returned.
Note: Keep in mind that http request would complete itself automatically once the request is solved but if you plan to implement a custom observable use the take
operator to make sure it completes after one call.
Visit the code of conduct and contribution guidelines
This document pretends to show what work is in progress and future plans to improve the library. Feel free to create an issue and explain what features would like to have
Alpha
- Basic service to handle store
- Ivy support / NG9
- Higher-Order Components to create a store
- Auto-generation of selectors
- Basic Redux DevTools support
- Basic documentation
- Evergreen Browser support
- First release to NPM registry
- Implementation of pattern to handle sideffects
Beta
- 80% Unit testing coverage
- Improve action typing
- Memoization of data
- Create or separate classes in charge of the busineess logic into a different lib to
- Schematics to implement the library faster
V1
- CI/CD support to run unit tests and E2E
- Persistent data - Rehydrid data from storage or HTTP service
V1-Next
- Better type support
- Lazy loading of states
- Normalization state proposal
- Custom pipes to resolve state in the HTML
- Can I generate my own selectors?
Yes, use the
select
function to get pieces of the state e.gpublic customSelector = this.store.state(select((state) => state.counter))
wherestore
is the Redux store service - Why my selectors are not being generated? Make sure that you have defined a initial state to all your properties. Selectors are created based on the schema of the initial state
- Can I mix the use of the reducer decorator and switch syntax? Nope. Each implementation is treated differently.
- Why would I use the reducer decorator? Less code and works faster. Each action will map to the reducer specify. A difference to the switch syntax the decorator flow does not need to evaluate all the cases.
Thanks goes to these wonderful people (emoji key):
Mariano Alvarez 🤔 💻 🚧 |
This project follows the all-contributors specification. Contributions of any kind welcome!