Skip to content

Latest commit

 

History

History
214 lines (164 loc) · 7.67 KB

README.md

File metadata and controls

214 lines (164 loc) · 7.67 KB

@nanux/store 💎

All Contributors

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

Motivation

There are great solutions including vanilla Redux implementations but most of them end in a very verbose code.

How to implement @nanux/store in my project?

There are two ways to use it:

Decorator Reducer 🎖️

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

Traditional reducer 👴🏻

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! ❤️

Wondering how to trigger side effects?

@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.

  1. Add a new property in the state
 { counter: 0, emojis: [] }
  1. 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();
  }
  1. 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.

How to Contribute?

Visit the code of conduct and contribution guidelines

Roadmap

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

@nanux/store

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

FAQ

  1. Can I generate my own selectors? Yes, use the select function to get pieces of the state e.g public customSelector = this.store.state(select((state) => state.counter)) where store is the Redux store service
  2. 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
  3. Can I mix the use of the reducer decorator and switch syntax? Nope. Each implementation is treated differently.
  4. 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.

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Mariano Alvarez
Mariano Alvarez

🤔 💻 🚧 ⚠️

This project follows the all-contributors specification. Contributions of any kind welcome!