Giter Site home page Giter Site logo

danwahlin / observable-store Goto Github PK

View Code? Open in Web Editor NEW
637.0 27.0 121.0 8.9 MB

Observable Store provides a simple way to manage state in Angular, React, Vue.js and other front-end applications.

License: MIT License

TypeScript 91.56% JavaScript 2.03% HTML 5.05% CSS 0.68% SCSS 0.68%

observable-store's Introduction

Node.js CI npm version

Observable Store - State Management for Front-End Applications (Angular, React, Vue.js, or any other)

Observable Store is a front-end state management library that provides a simple yet powerful way to manage state in front-end applications. Front-end state management has become so complex that many of us spend more hours working on the state management code than on the rest of the application. Observable Store has one overall goal - "keep it simple".

The goal of observable store is to provide a small, simple, and consistent way to manage state in any front-end application (Angular, React, Vue.js or any other) while achieving many of the key goals offered by more complex state management solutions. While many front-end frameworks/libraries provide state management functionality, many can be overly complex and are only useable with the target framework/library. Observable Store is simple and can be used with any front-end JavaScript codebase.

Using Obervable Store

Key Goals of Observable Store:

  1. Keep it simple!
  2. Single source of truth for state
  3. Store state is immutable
  4. Provide state change notifications to any subscriber
  5. Track state change history
  6. Easy to understand with a minimal amount of code required to get started
  7. Works with any front-end project built with JavaScript or TypeScript (Angular, React, Vue, or anything else)
  8. Integrate with the Redux DevTools (Angular and React currently supported)

Steps to use Observable Store

Here's a simple example of getting started using Observable Store. Note that if you're using TypeScript you can provide additional details about the store state by using an interface or class (additional examples of that can be found below).

  1. Install the Observable Store package:

    npm install @codewithdan/observable-store

  2. Install RxJS - a required peer dependency if your project doesn't already reference it:

    npm install rxjs

  3. Create a class that extends ObservableStore. Optionally pass settings into super() in your class's constructor (view Observable Store settings). While this shows a pure JavaScript approach, ObservableStore also accepts a generic that represents the store type. See the Angular example below for more details.

    export class CustomersStore extends ObservableStore {
    
        constructor() {
            super({ /* add settings here */ });
        }
    
    }
  4. Update the store state using setState(state, action).

    addCustomerToStore(newCustomer) {
        this.setState({ customer: newCustomer }, 'add_customer');
    }
  5. Retrieve store state using getState().

    getCustomerFromStore() {
        this.getState().customer;
    }
  6. Subscribe to store changes in other areas of the application by using the store's stateChanged observable.

    // Create CustomersStore object or have it injected if platform supports that
    
    init() {
        this.storeSub = this.customersStore.stateChanged.subscribe(state => {
            if (state) {
                this.customer = state.customer;
            }
        });
    }
    
    // Note: Would need to unsubscribe by calling this.storeSub.unsubscribe()
    // as the target object is destroyed
  7. Access store state history in CustomersStore by calling the stateHistory property (this assumes that the trackStateHistory setting is set to true)

    console.log(this.stateHistory);

API and Settings

Observable Store API

Observable Store Settings

Observable Store Global Settings

Observable Store Extensions

Running the Samples

Open the samples folder available at the Github repo and follow the instructions provided in the readme file for any of the provided sample projects.

Sample Applications

Using Observable Store with Angular

See the samples folder in the Github repo for examples of using Observable Store with Angular.

  1. Create an Angular application using the Angular CLI or another option.

  2. Install @codewithdan/observable-store:

    npm install @codewithdan/observable-store

  3. Add an interface or model object that represents the shape of the data you'd like to add to your store. Here's an example of an interface to store customer state:

    export interface StoreState {
        customers: Customer[];
        customer: Customer;
    }
  4. Add a service (you can optionally calll it a store if you'd like) that extends ObservableStore. Pass the interface or model class that represents the shape of your store data in for T as shown next:

    @Injectable()
    export class CustomersService extends ObservableStore<StoreState> {
    
    }
  5. In the constructor add a call to super(). The store allows you to turn tracking of store state changes on and off using the trackStateHistory property. See a list of Observable Store Settings.

    constructor() { 
        super({ trackStateHistory: true });
    }
  6. Add functions into your service/store to retrieve, store, sort, filter, or perform any actions you'd like. To update the store call setState() and pass the action that is occuring as well as the store state. To get the state out of the store call getState(). Note that store data is immutable and getState() always returns a clone of the store data. Here's a simple example:

    @Injectable()
    export class CustomersService extends ObservableStore<StoreState> {
        sorterService: SorterService;
    
        constructor(sorterService: SorterService) { 
            const initialState = {
                customers: [],
                customer: null
            }
            super({ trackStateHistory: true });
            this.setState(initialState, 'INIT_STATE');
            this.sorterService = sorterService;
        }
    
        get() {
            const { customers } = this.getState();
            if (customers) {
                return of(customers);
            }
            // call server and get data
            // assume async call here that returns observable
            return asyncData;
        }
    
        add(customer: Customer) {
            let state = this.getState();
            state.customers.push(customer);
            this.setState({ customers: state.customers }, 'ADD_CUSTOMER');
        }
    
        remove() {
            let state = this.getState();
            state.customers.splice(state.customers.length - 1, 1);
            this.setState({ customers: state.customers }, 'REMOVE_CUSTOMER');
        }
        
        sort(property: string = 'id') {
            let state = this.getState();
            const sortedState = this.sorterService.sort(state.customers, property);
            this.setState({ customers: sortedState }, 'SORT_CUSTOMERS');
        }
    
    }

    While strings are used for actions in the prior examples, you can use string enums (a TypeScript feature) as well if you want to have a set list of actions to choose from:

        export enum CustomersStoreActions {
            AddCustomer = 'ADD_CUSTOMER',
            RemoveCustomer = 'REMOVE_CUSTOMER',
            GetCustomers = 'GET_CUSTOMERS',
            SortCustomers = 'SORT_CUSTOMERS'
        }
    
        // Example of using the enum in a store
        add(customer: Customer) {
            let state = this.getState();
            state.customers.push(customer);
            this.setState({ customers: state.customers }, CustomersStoreActions.AddCustomer);
        }
  7. If you want to view all of the changes to the store you can access the stateHistory property:

    console.log(this.stateHistory);
  8. An example of the state history output is shown next:

    // example stateHistory output
    [
        {
            "action": "INIT_STATE",
            "beginState": null,
            "endState": {
                "customers": [
                    {
                        "id": 1545847909628,
                        "name": "Jane Doe",
                        "address": {
                            "street": "1234 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    }
                ],
                "customer": null
            }
        },
        {
            "action": "ADD_CUSTOMER",
            "beginState": {
                "customers": [
                    {
                        "id": 1545847909628,
                        "name": "Jane Doe",
                        "address": {
                            "street": "1234 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    }
                ],
                "customer": null
            },
            "endState": {
                "customers": [
                    {
                        "id": 1545847909628,
                        "name": "Jane Doe",
                        "address": {
                            "street": "1234 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    },
                    {
                        "id": 1545847921260,
                        "name": "Fred",
                        "address": {
                            "street": "1545847921260 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    }
                ],
            "customer": null
            }
        }
    ]
  9. Any component can be notified of changes to the store state by injecting the store and then subscribing to the stateChanged observable:

    customers: Customer[];
    storeSub: Subscription;
    
    constructor(private customersService: CustomersService) { }
    
    ngOnInit() {
        // If using async pipe (recommend renaming customers to customers$)
        // this.customers$ = this.customersService.stateChanged;
    
        // Can subscribe to stateChanged observable of the store
        this.storeSub = this.customersService.stateChanged.subscribe(state => {
            if (state) {
                this.customers = state.customers;
            }
        });
    
        // Can call service/store to get data directly 
        // It won't fire when the store state changes though in this case
        //this.storeSub = this.customersService.get().subscribe(custs => this.customers = custs);
    }

    You'll of course want to unsubscribe in ngOnDestroy() (check out SubSink on npm for a nice way to easily subscribe/unsubscribe):

    ngOnDestroy() {
        if (this.storeSub) {
            this.storeSub.unsubscribe();
        }        
    }

Using Observable Store with React

See the samples folder in the Github repo for examples of using Observable Store with React.

  1. Create a React application using the create-react-app or another option.

  2. Install @codewithdan/observable-store:

    npm install @codewithdan/observable-store

  3. Install RxJS (a required peer dependency):

    npm install rxjs

  4. Add a store class (you can call it whatever you'd like) that extends ObservableStore.

    export class CustomersStore extends ObservableStore {
    
    }
  5. In the constructor add a call to super(). The store allows you to turn tracking of store state changes on and off using the trackStateHistory property. See a list of Observable Store Settings.

    export class CustomersStore extends ObservableStore {
        constructor() {
            super({ trackStateHistory: true });
        }
    }
  6. Add functions into your service/store to retrieve, store, sort, filter, or perform any actions you'd like. To update the store call setState() and pass the action that is occuring as well as the store state. To get the state out of the store call getState(). Note that store data is immutable and getState() always returns a clone of the store data. Here's a simple example:

    export class CustomersStore extends ObservableStore {
    
        constructor() {
            super({ trackStateHistory: true });
        }
    
        fetchCustomers() {
            // using fetch api here to keep it simple, but any other
            // 3rd party option will work (Axios, Ky, etc.)
            return fetch('/customers')
                .then(response => response.json())
                .then(customers => {
                    this.setState({ customers }, 'GET_CUSTOMERS');
                    return customers;
                });
        }
    
        getCustomers() {
            let state = this.getState();
            // pull from store cache
            if (state && state.customers) {
                return this.createPromise(null, state.customers);
            }
            // doesn't exist in store so fetch from server
            else {
                return this.fetchCustomers();
            }
        }
    
        getCustomer(id) {
            return this.getCustomers()
                .then(custs => {
                    let filteredCusts = custs.filter(cust => cust.id === id);
                    const customer = (filteredCusts && filteredCusts.length) ? filteredCusts[0] : null;                
                    this.setState({ customer }, 'GET_CUSTOMER');
                    return customer;
                });
        }
    
        createPromise(err, result) {
            return new Promise((resolve, reject) => {
                return err ? reject(err) : resolve(result);
            });
        }
    }

    While strings are used for actions in the prior example, you can use an object as well if you want to have a set list of actions to choose from:

    const CustomersStoreActions = {
        GetCustomers: 'GET_CUSTOMERS',
        GetCustomer: 'GET_CUSTOMER'
    };
    
        // Example of using the enum in a store
    getCustomer(id) {
        return this.getCustomers()
            .then(custs => {
                let filteredCusts = custs.filter(cust => cust.id === id);
                const customer = (filteredCusts && filteredCusts.length) ? filteredCusts[0] : null;                
                this.setState({ customer }, CustomersStoreActions.GetCustomer);
                return customer;
            });
    }
  7. Export your store. A default export is used here:

    export default new CustomersStore();
  8. If you want to view all of the changes to the store you can access the store's stateHistory property:

    console.log(this.stateHistory);
    
    // example stateHistory output
    [
        {
            "action": "INIT_STATE",
            "beginState": null,
            "endState": {
                "customers": [
                    {
                        "id": 1545847909628,
                        "name": "Jane Doe",
                        "address": {
                            "street": "1234 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    }
                ],
                "customer": null
            }
        },
        {
            "action": "ADD_CUSTOMER",
            "beginState": {
                "customers": [
                    {
                        "id": 1545847909628,
                        "name": "Jane Doe",
                        "address": {
                            "street": "1234 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    }
                ],
                "customer": null
            },
            "endState": {
                "customers": [
                    {
                        "id": 1545847909628,
                        "name": "Jane Doe",
                        "address": {
                            "street": "1234 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    },
                    {
                        "id": 1545847921260,
                        "name": "Fred",
                        "address": {
                            "street": "1545847921260 Main St.",
                            "city": "Phoenix",
                            "state": "AZ",
                            "zip": "85258"
                        }
                    }
                ],
            "customer": null
            }
        }
    ]
  9. Import your store into a component:

    import CustomersStore from '../stores/CustomersStore';
  10. Now use your store to access or update data. Any component can be notified of changes to the store state by subscribing to the stateChanged observable:

    storeSub = null;
    
    componentDidMount() {
        // ###### CustomersStore ########
        // Option 1: Subscribe to store changes
        // Useful when a component needs to be notified of changes but won't always
        // call store directly.
        this.storeSub = CustomersStore.stateChanged.subscribe(state => {
          if (state) {
            this.setState({ customers: state.customers });
          }
        });
    
        // In this example we trigger getting the customers (code above receives the customers)
        CustomersStore.getCustomers();
    
        // Option 2: Get data directly from store
        // If a component triggers getting the data it can retrieve it directly rather than subscribing
        // CustomersStore.getCustomers()
        //     .then(customers => {
        //       ....
        //     });
    }

    You'll want to unsubscribe in componentWillUnmount():

    componentWillUnmount() {
        if (this.storeSub) {
          this.storeSub.unsubscribe();
        }
    }

Using Observable Store with Vue.js

....

Store API

Observable Store provides a simple API that can be used to get/set state, subscribe to store state changes, and access state history. If you're new to TypeScript generics, the T shown in the APIs below represents your store's state.

Functions Description
dispatchState(stateChanges: Partial<T>, dispatchGlobalState: boolean = true) : T Dispatch the store's state without modifying the state. Service state can be dispatched as well as the global store state. If dispatchGlobalState is false then global state will not be dispatched to subscribers (defaults to true).
getState(deepCloneReturnedState: boolean = true) : T Retrieve store's state. If using TypeScript (optional) then the state type defined when the store was created will be returned rather than any. The deepCloneReturnedState boolean parameter (default is true) can be used to determine if the returned state will be deep cloned or not. If set to false, a reference to the store state will be returned and it's up to the user to ensure the state isn't change from outside the store. Setting it to false can be useful in cases where read-only cached data is stored and must be retrieved as quickly as possible without any cloning.
getStateProperty<TProp>(propertyName: string, deepCloneReturnedState: boolean = true) : TProp Retrieve a specific property from the store's state which can be more efficient than getState() since only the defined property value will be returned (and cloned) rather than the entire store value. If using TypeScript (optional) then the generic property type used with the function call will be the return type.
getStateSliceProperty<TProp>(propertyName: string, deepCloneReturnedState: boolean = true): TProp If a stateSliceSelector has been set, the specific slice will be searched first. Retrieve a specific property from the store's state which can be more efficient than getState() since only the defined property value will be returned (and cloned) rather than the entire store value. If using TypeScript (optional) then the generic property type used with the function call will be the return type.
logStateAction(state: any, action: string): void Add a custom state value and action into the state history. Assumes trackStateHistory setting was set on store or using the global settings.
resetStateHistory(): void Reset the store's state history to an empty array.
setState(state: T, action: string, dispatchState: boolean = true, deepCloneState: boolean = true) : T Set the store state. Pass the state to be updated as well as the action that is occuring. The state value can be a function (see example below). The latest store state is returned and any store subscribers are notified of the state change. The dispatchState parameter can be set to false if you do not want to send state change notifications to subscribers. The deepCloneReturnedState boolean parameter (default is true) can be used to determine if the state will be deep cloned before it is added to the store. Setting it to false can be useful in cases where read-only cached data is stored and must added to the store as quickly as possible without any cloning.
static addExtension(extension: ObservableStoreExtension) Used to add an extension into ObservableStore. The extension must implement the ObservableStoreExtension interface.
static clearState(): void Clear/null the store state across all services that use it.
static initializeState(state: any) Used to initialize the store's state. An error will be thrown if this is called and store state already exists so this should be set when the application first loads. No notifications are sent out to store subscribers when the store state is initialized.
static resetState(state, dispatchState: boolean = true) Used to reset the state of the store to a desired value for all services that derive from ObservableStore. A state change notification and global state change notification is sent out to subscribers if the dispatchState parameter is true (the default value).

Properties Description
globalStateChanged: Observable<any> Subscribe to global store changes i.e. changes in any slice of state of the store. The global store may consist of 'n' slices of state each managed by a particular service. This property notifies of a change in any of the 'n' slices of state. Returns an RxJS Observable containing the current store state.
globalStateWithPropertyChanges: Observable<StateWithPropertyChanges<any>> Subscribe to global store changes i.e. changes in any slice of state of the store and also include the properties that changed as well. The global store may consist of 'n' slices of state each managed by a particular service. This property notifies of a change in any of the 'n' slices of state. Upon subscribing to globalStateWithPropertyChanges you will get back an object containing state (which has the current store state) and stateChanges (which has the individual properties/data that were changed in the store).
stateChanged: Observable<T> Subscribe to store changes in the particlar slice of state updated by a Service. If the store contains 'n' slices of state each being managed by one of 'n' services, then changes in any of the other slices of state will not generate values in the stateChanged stream. Returns an RxJS Observable containing the current store state (or a specific slice of state if a stateSliceSelector has been specified).
stateWithPropertyChanges: Observable<StateWithPropertyChanges<T>> Subscribe to store changes in the particlar slice of state updated by a Service and also include the properties that changed as well. Upon subscribing to stateWithPropertyChanges you will get back an object containing state (which has the current slice of store state) and stateChanges (which has the individual properties/data that were changed in the store).
stateHistory: StateHistory Retrieve state history. Assumes trackStateHistory setting was set on the store.
static allStoreServices: any[] Provides access to all services that interact with ObservableStore. Useful for extensions that need to be able to access a specific service.
static globalSettings: ObservableStoreGlobalSettings get/set global settings throughout the application for ObservableStore. See the Observable Store Settings below for additional information. Note that global settings can only be set once as the application first loads.
static isStoreInitialized: boolean Used to determine if the the store's state is currently initialized. This is useful if there are multiple scenarios where the store might have already been initialized such as during unit testing etc or after the store has been cleared.

Note that TypeScript types are used to describe parameters and return types above. TypeScript is not required to use Observable Store though.

Passing a Function to setState()

Here's an example of passing a function to setState(). This allows the previous state to be accessed directly while setting the new state.

this.setState(prevState => { 
    return { customers: this.sorterService.sort(prevState.customers, property) };
}, 'SORT_CUSTOMERS');

Store Settings (per service)

Observable Store settings can be passed when the store is initialized (when super() is called in a service). This gives you control over how things work for each service within your application that extends the store.

Setting Description
trackStateHistory: boolean Determines if the store's state will be tracked or not (defaults to false). Pass it when initializing the Observable Store (see examples above). When true, you can access the store's state history by calling the stateHistory property.
logStateChanges: boolean Log any store state changes to the browser console (defaults to false).
includeStateChangesOnSubscribe: boolean DEPRECATED. Returns the store state by default when false (default). Set to true if you want to receive the store state as well as the specific properties/data that were changed when the stateChanged subject emits. Upon subscribing to stateChanged you will get back an object containing state (which has the current store state) and stateChanges (which has the individual properties/data that were changed in the store). Since this is deprecated, use stateWithPropertyChanges or globalStateWithPropertyChanges instead.
stateSliceSelector: function Function to select the slice of the store being managed by this particular service. If specified then the specific state slice is returned. If not specified then the total state is returned (defaults to null).

Example of passing settings to the store:

export class CustomersStore extends ObservableStore {

    constructor() {
        super({ /* add settings here */ });
    }

}

Using the stateSliceSelector() Function

The stateSliceSelector() function can be used to return a "slice" of the store state that is managed by a Service to any subscribers. For example, if a CustomersService manages a customers collection and a selectedCustomer object you can return only the selectedCustomer object to subscribers (rather than customers and selectedCustomer) by creating a stateSliceSelector() function.

Define it as you initialize the service when passing a settings object to super() in the Service's constructor.

export class CustomersService extends ObservableStore<StoreState> {
  constructor() { 
    super({ stateSliceSelector: state => { 
        return {
          customer: state.selectedCustomer
          // return other parts of the store here too if desired
        };
      } 
    });
 }
}

Global Store Settings

You can set the following Observable Store settings globally for the entire application if desired. For details, view the Observable Store Settings section. This allows you to define the settings once and all services that extend Observable Store will automatically pick these settings up. You can override these properties at the service level as well which is nice when you want a particular service to have more logging (as an example) while other services don't.

  • trackStateHistory
  • logStateChanges
  • includeStateChangesOnSubscribe [DEPRECATED]
  • isProduction [RESERVED FOR FUTURE USE]

Global store settings are defined ONCE when the application first initializes and BEFORE the store has been used:

ObservableStore.globalSettings = {  /* pass settings here */ };

Extensions

Observable Store now supports extensions. These can be added when the application first loads by calling ObservableStore.addExtension().

Redux DevTools Extension

The first built-in extension adds Redux DevTools integration into applications that use Observable Store. The extension can be found in the @codewithdan/observable-store-extensions package.

Integrating the Redux DevTools

Note about Angular 9/Ivy and the Redux DevTools Support

While the code is in place to support it, The Observable Store Redux DevTools currently do not work with Angular 9 and Ivy. Once the findProviders() API is fully implemented and released by Angular then support will be finalized for the Redux DevTools.

Note about the __devTools Store Property:

When the Redux DevTools extension is enabled it will add routing information into your store using a property called __devTools. This property is used to enable the Redux DevTools time travel feature and can be useful for associating different action states with a given route when manually looking at store data using the DevTools. If the Redux DevTools extension is not enabled (such as in production scenarios) then the __devTools property will not be added into your store.

Integrating Angular with the Redux DevTools

See the example in the samples/angular-store-edits folder.

Install the extensions package:

npm install @codewithdan/observable-store-extensions

Add the following into main.ts and ensure that you set trackStateHistory to true:

import { ObservableStore } from '@codewithdan/observable-store';
import { ReduxDevToolsExtension } from '@codewithdan/observable-store-extensions';

...

ObservableStore.globalSettings = {  
    trackStateHistory: true
};
ObservableStore.addExtension(new ReduxDevToolsExtension());

Install the Redux DevTools Extension in your browser, run your Angular application, and open the Redux DevTools extension.

Integrating React with the Redux DevTools

See the example in the samples/react-store folder.

Install the extensions package:

npm install @codewithdan/observable-store-extensions

Add the history prop to your router:

import React from 'react';
import { Router, Route, Redirect } from 'react-router-dom';
import { createBrowserHistory } from 'history';|

export const history = createBrowserHistory();

...

const Routes = () => (
  <Router history={history}>
    <div>
       <!-- Routes go here -->
    </div>
  </Router>
);

export default Routes;

Add the following into index.js and ensure that you set trackStateHistory to true and pass the history object into the ReduxDevToolsExtension constructor as shown:

import Routes, { history } from './Routes';
import { ObservableStore } from '@codewithdan/observable-store';
import { ReduxDevToolsExtension } from '@codewithdan/observable-store-extensions';

...

ObservableStore.globalSettings = {  
    trackStateHistory: true
};
ObservableStore.addExtension(new ReduxDevToolsExtension({ 
    reactRouterHistory: history 
}));

ReactDOM.render(<Routes />, document.getElementById('root'));

Install the Redux DevTools Extension in your browser, run your React application, and open the Redux DevTools extension.

Redux DevTools and Production

While you can enable the Redux DevTools extension in production it's normally recommended that you remove it. That can be done through a custom build process or by checking the environment where your code is running.

Angular Example

import { environment } from './environments/environment';

if (!environment.production) {
    ObservableStore.addExtension(new ReduxDevToolsExtension());
}

React Example

if (process.env.NODE_ENV !== 'production') {
    ObservableStore.addExtension(new ReduxDevToolsExtension({ 
        reactRouterHistory: history 
    }));
}

Changes

1.0.11

Added includeStateChangesOnSubscribe setting (NOW DEPRECATED in 2+) for cases where a subscriber to stateChanged wants to get the current state as well as the specific properties/data that were changes in the store. Defaults to false so prior versions will only receive the current state by default which keeps patched versions compatible in the 1.0.x range.

Set the property to true if you want to receive the store state as well as the specific properties/data that were changed when the stateChanged subject emits. Upon subscribing to stateChanged you will get back an object containing state (which has the current store state) and stateChanges (which has the individual properties/data that were changed in the store).

1.0.12

Changed updateState() to _updateState() since it's a private function. Remove tsconfig.json from package.

1.0.13

Moved BehaviorSubject into ObservableService class so that if multiple instances of a wrapper around the store are created, subscribers can subscribe to the individual instances.

1.0.14

Added logStateChanges setting to write out all state changes to the browser console when true. Defaults to false.

1.0.15

Added action to log output when logStateChanges is true.

1.0.16

Thanks to a great contribution by Mickey Puri you can now globally subscribe to store changes (globalStateChanged event) and even define state slices (stateSliceSelector setting).

1.0.17

Merged in another contribution by Mickey Puri to ensure the settings defaults are always applied regardless of how many properties the user passes. Renamed a settings default property (state_slice_selector => stateSliceSelector). Added editable store example (update/delete functionality) for Angular in the samples folder.

1.0.18

Minor updates to the readme.

1.0.19

Updated Angular example and added stateSliceSelector() information in readme

1.0.20

Updated readme

1.0.21

Updated to latest version of RxJS. Removed subsink from the Angular Simple Store demo to just use a normal Subscription for unsubscribing (just to keep it more "native" and require less dependencies).

1.0.22

Internal type additions and tests contributed by @elAndyG (https://github.com/elAndyG).

2.0.0 - October 13, 2019

  1. Added more strongly-typed information for stateChanged and the overall API to provide better code help while using Observable Store.
  2. RxJS is now a peer dependency (RxJS 6.4.0 or higher is required). This avoids reported versioning issues that have come up when a project already has RxJS in it. The 1.x version of Observable Store added RxJS as a dependency. Starting with 2.0.0 this is no longer the case.
  3. Added an ObservableStore.globalSettings property to allow store settings to be defined once if desired for an entire application rather than per service that uses the store.
  4. getState() and setState() now clone when the global settings isProduction property is false (ObservableStore.globalSettings = { isProduction: false }). When running in production mode no cloning is used in order to enhance performance since mutability issues would've been detected at development time. This technique is used with other store solutions as well. NOTE: isProduction is no longer used. See 2.0.1 below.
  5. Changed TypeScript module compilation to CommonJS instead of ES2015 to aid with testing scenarios (such as Jest) where the project doesn't automatically handle ES2015 module conventions without extra configuration.

2.0.1 - October 14, 2019

Due to edge cases cloning is used in development and production. The isProduction property is left in so builds are not broken, but currently isn't used.

2.1.0 - October 24, 2019

In order to allow stateChanged to be strongly-typed and also allow state changes with property changes to return a strongly-typed object as well, there are now 4 observable options to choose from when you want to know about changes to the store:

// access state changes made by a service interacting with the store
// allows access to slice of store state that service interacts with
stateChanged: Observable<T>   

// access all state changes in the store regardless of where they're
// made in the app
globalStateChanged: Observable<any>  

// access state changes made by a service interacting with the 
// store and include the properties that were changed
stateWithPropertyChanges: Observable<StateWithPropertyChanges<T>> 

// access all state changes in the store and include the 
// properties that were changed
globalStateWithPropertyChanges: Observable<StateWithPropertyChanges<any>>

The includeStateChangesOnSubscribe property is now deprecated since stateWithPropertyChanges or globalStateWithPropertyChanges can be used directly.

Thanks to Michael Turbe for the feedback and discussion on these changes.

2.2.3 - December 10, 2019

This version adds a Redux DevTools Extension. A BIG thank you to @brandonroberts (https://github.com/brandonroberts) of NgRx fame for helping get me started integrating with the Redux DevTools.

New APIs:

  • A static allStoreServices property is now available to access all services that extend ObservableStore and interact with the store. Used by the Redux DevTools extension and can be useful for future extensions.
  • Added static addExtension() function. Used to add the Redux DevTools Extension and any future extensions.
  • Added new @codewithdan/observable-store-extensions package for the redux devtools support.

2.2.4 - January 23, 2019

Minor updates to the Observable Store docs. Fixed a bug in the Redux DevTools extension that would throw an error when the extension wasn't installed or available. Updated readme to discuss how to disable extensions for production scenarios.

Thanks to Matthias Langhard for the feedback and discussion on these changes.

2.2.5 - February 26, 2020

  • Added ObservableStore.initializeState() API.
  • Refactored unit tests.

2.2.6 - February 29, 2020

  • Added ObservableStore.resetState() API.
  • Added unit tests for resetState().

Feedback from Severgyn and Luiz Filipe influenced this feature. Thanks folks!

2.2.7 - March 6, 2020

  • Fixed bug where Redux DevTools code for Angular v8 or lower was also calling code intended for Angular v9 (which is still a work in progress as noted in the Redux DevTools section above).

Thanks to trentsteel84 for reporting the issue.

2.2.8 - April 2, 2020
  • All calls to getState() and setState() clone data now due to edge issues that can arise otherwise with external references. Previously, it would selectively clone based on dev or prod. All functions that get/set state now provide an optional deepClone type of boolean property that can be used in cases where it's not desirable to clone state (large amount of data being added to the store for caching for example).

  • Added ObservableStore.clearState() API to null the store across all services that use it.

  • Added getStateProperty<T>(propName: string) to retrieve a specific property from the store versus retrieving the entire store as getState() does.

2.2.9 - May 5, 2020

Added support for cloning Map and Set objects in the interal cloner service used by Observable Store. Thanks to Chris Andrade for the initial contribution.

2.2.10 - May 20, 2020

External APIs supported turning off cloning but internal APIs still cloned which isn't optimal for people storing a lot of data in the store. Thanks to Steve-RW for asking about it and for the PR that fixed it.

2.2.13 - August 31, 2021

Adds a getStateSliceProperty() function. Thanks to Connor Smith for the contribution. Added strict=true support into Observable Store library tsconfig.json files.

Updates to documentation.

2.2.14 - October 31, 2021

Update readme link to Redux DevTools. Thanks to Ravi Mathpal for the information.

2.2.15 - November 18, 2022

New isStoreInitialized property added. Thanks to Jason Landbridge for the PR!

Building the Project

See the README.md file in the modules folder.

observable-store's People

Contributors

alvarocjunq avatar chrisjandrade avatar connorsmith-pf avatar crowcoder avatar danwahlin avatar dependabot[bot] avatar elandyg avatar jasonlandbridge avatar mickeypuri avatar steve-rw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

observable-store's Issues

Persisting data long term with expiry options

The Observable Store needs to have the ability to persist data after closing the browser tab, window or navigating away from the web application.

This would need to be configurable both at the global level as well as an individual service level. Possible configurations would include how to store the data (i.e. local storage, cookies), assign a time to live, and a mechanism for invalidating data based on the time to live.

This feature could be will be very beneficial when setting and getting data that you expect to rarely or never change.

Store structure: Flat vs Complex + stateWithPropertyChanges returns function if that's what was passed in the setState

Hello @DanWahlin,

Thanks for the library, I'm using in in building a micro frontend platform with influence from Observer pattern.
I wanted to encapsulate the properties that are stored by the services so I though of having a more complex structure for the store:

export interface StoreState {
	[key: string]: any;
	 'pro-registry': ProRegistryStore;
	 'pro-auth': ProAuthStore;
	// 'pro-filters': any;
	// 'pro-settings': any;
}

export interface ProRegistryStore {
	publicApps: ApplicationConfig[];
	privilegedApps: ApplicationConfig[];
}

export interface ProAuthStore {
	isAuthenticated: boolean;
	currentUser?: any; // TODO: set correct interface of User imported from generated OpenApi
}

Now, with this structure I saw several issues:

  1. Weird redux dev extension chart, I opened a separate ticket for this
  2. When I want to setState, I can't update just the property I want, I need to update the entire section of the store, which leads me to the next issue:
  3. When using stateWithPropertyChanges, because I've sent the entire service "store section", I'm going to get back exactly what I sent, not just the difference in the state. Now, If I send only the property I want, I'm gonna get only that one but the store is completely changed
  4. When using a function to setState, the return value of stateWithPropertyChanges is the state and the function that I used in the stateChanges argument. I expected to have an object instead of the function.

More details

Setting a deep property

This is how I do it

private updateStateCurrentUser(user, dispatchState: boolean = true): void {
		const authState = this.getState();
		const newState = {
			[ProAuthStoreName]: { ...authState, ...{ currentUser: user } },
		};

		this.setState(newState, ProAuthStoreActions.SET_CURRENT_USER, dispatchState);
	}

And it works, but unfortunately, it sees it as an update of the entire ProAuthStoreName property, when I only change currentUser. This issue flows down to the stateWithPropertyChanges method, which returns the entire object instead of property that was changed.

Now, This would not be a problem, but when using stateWithPropertyChanges, I can't make use of the stateChanges parameter because it's the same with the actual state, I would like to compare it with the old values, similar to how redux works.

Do you think that would be a good idea, to return something like

interface StateChanges {
    oldState: any;
    newState: any;
}

Losing Class (getters/setters) when returning object from store

When I create a class (with getters/setters) all is well. When I store it in the store and retrieve it however, it is returning as an object and thus have loss the class getters/setters. Is there a way to store or retrieve it so that it maintains the class?

Is it necessary to deep clone the state when dispatching?

I'm using this project with data that can not easily be deepCloned. I know deepCloning has a lot of advantages, but even without it, this library has been very helpful for me.

However with the current versions, it will always do a deep clone when using the setState function with dispatchState=true and deepCloneState = false. (because the dispatchState function doesn't care about any deepCloneState argument). Is this necessary for some reason?

If not I'd propose to make this optional, then I could continue to use this helpful library :-)

Parameter stateChanges: Partial<T> of dispatchState method

Hi there, dispatchState method has the signature
protected dispatchState(stateChanges: Partial<T>, dispatchGlobalState: boolean = true)
What the purpose of stateChanges parameter, if the ideia is to dispatch the current state of the store and the doc says

without modifying the store state?

ReduxDevToolsExtension console error

I'm getting the following console error:

Uncaught TypeError: this.window.getAllAngularRootElements is not a function
    at new AngularDevToolsExtension (angular-devtools-extension.js:5)
    at redux-devtools.extension.js:47
    at ZoneDelegate.invokeTask (zone-evergreen.js:391)
    at Zone.runTask (zone-evergreen.js:168)
    at ZoneTask.invokeTask [as invoke] (zone-evergreen.js:465)
    at invokeTask (zone-evergreen.js:1603)
    at globalZoneAwareCallback (zone-evergreen.js:1629)

In my main.ts i have:
ObservableStore.addExtension(new ReduxDevToolsExtension());
The redux extension seems to be working fine in Chrome with the code there, but if i comment that code out, then the error disappears.

I'm currently using version 2.2.6 for both, but i was previously on version 2.2.3, which showed the same error.

Replay Subject for edge cases

This is more of a feature enhancement request wherein in the configuration object we should be able to request a replaySubject instead of the default behavior Subject.

This is mostly required for edge cases , but so is why replaySubject was designed.

Scenario : I have a search form and based on the result I want to display a two different components . Those components do subscribe to the store to get the data in state. Now these two components are not present in the DOM initially, so they do not form the subscription. However when the main component gets the results one of them gets rendered (by virtue of an *ngIf condition) and thus subscribe to the state, but by then the behaviour subject is complete and hence even if they subscribe they won't get the last fetched data.

This case can be solved if the the subject is a replaySubject where even thought the subject is completed , when freshly subscribed it would get the last fetched data.

Problem with ReduxDevToolsExtension under electron environment

Hi,

the instanciation of ReduxDevToolsExtension throw an error when running under electron environment without react.
The problem comes from the checkIsReact() method : require("react") crash because the react module is not found.

Uncaught Error: Cannot find module 'react'
Require stack:

  • electron/js2c/renderer_init
    at Module._resolveFilename (:9080/internal/modules/cjs/loader.js:961)
    at Function.i._resolveFilename (:9080/electron/js2c/renderer_init.js:39)
    at Module._load (:9080/internal/modules/cjs/loader.js:844)
    at Function.Module._load (:9080/electron/js2c/asar.js:769)
    at Module.require (:9080/internal/modules/cjs/loader.js:1023)
    at ReduxDevToolsExtension.require (:9080/internal/modules/cjs/helpers.js:77)
    at ReduxDevToolsExtension.checkIsReact (:9080/home/ahinsing/Git/astraw-ui/node_modules/@codewithdan/observable-store-extensions/redux-devtools.extension.ts:233)
    at new ReduxDevToolsExtension (:9080/home/ahinsing/Git/astraw-ui/node_modules/@codewithdan/observable-store-extensions/redux-devtools.extension.ts:13)
    at eval (webpack-internal:///./src/renderer/index.ts:16)
    at Module../src/renderer/index.ts (renderer.js:3286)

Anthony.

It would help so much if you included a fully working react example

The line this.storeSub = CustomersStore.stateChanged.subscribe(state => { fails in React, with a

Header.jsx:61 Uncaught TypeError: Cannot read property 'subscribe' of undefined at Header.componentDidMount (Header.jsx:61)

And WebStorm reports 'Instance member is not accessible' for stateChanged

I keep looking at your example code and wondering what code is there outside of the snippet, and if the lack of something "obvious" is causing my problem.

It would be so helpful if you provided a JSFiddle or code sandbox.

SetState method is not working

@DanWahlin

I'm using the setState method but it seems it doesn't work. actually, always the state value is equal to the default value!

this.setState(
        { currentUser: loggedInUser },
        UserStoreActions.userLoggedIn
      );

after setState when I'm subscribing to my state class state value is always equal to the default value!!

  constructor(private authService: AuthService) {
    this.subs.sink = this.authService.stateChanged.subscribe((state) => {
      console.log('user panel sate', state);
      this.user = state.currentUser ?? null;
    });
  }

How do I subscribe to only changes

I am trying to subscribe to changes only and not when initially subscribed. How can I not have the stage changed triggered when initially subscribed?

Thanks for your help!

Type errors in state change subscriptions upgrading to 2.0.1

Hi there, I'm using state services with the includeStateChangesOnSubscribe: true setting, and after updating to the latest version, my project isn't building and all of the subscriptions are showing type errors. Since I was using includeStateChangesOnSubscribe: true, in each subscription I was using the separate state and stateChanges objects as so:
this.fooStateServiceSubscription = this.fooStateService.stateChanged .subscribe(stateEvent => { if (stateEvent.stateChanges.pages) { this.allMyPages = stateEvent.state.pages; }

The compiler now thinks that the stateEvent object dispatched through the description is the state type itself, not the js object that had state and stateChanges on it.

I could go into all of these spots and cast the stateEvent object to any, but that seems rough. Is it possible to ratchet back down the type safety of the dispatched state event object, or somehow make an interface that would work for both versions?

Using JSONPath and Observable store

Hello Dan,
I'm very interested to hear you opinion about the following additional feature to the Observable-Store.
In my case I need to listen to property changes of object which might be nested or perhaps a property of an object in an array.
Therefor I experimented by adding the following method to the Observable store:

  onPropertyChange<T>(jsonPath: string): Observable<T | undefined> {
    return this.stateChanged.pipe(
      map(state => {
        if (state != null) {
          // The return value of JSON Path is array.
          //See for more information https://stackoverflow.com/questions/23608050/getting-a-single-value-from-a-json-object-using-jsonpath
          const values = JSONPath({ path: jsonPath, json: state as any});
          return values[0];
        }
        return undefined;
      }),
      distinctUntilChanged(),
      skip(1)
    )
  }

In the method above I use the npm package jsonpath-plus.
And we can then run the following test to subscribe to changes of a property of an item in an array:

    it('should receive notification upon array item property change', waitForAsync(() => {
      const updatedCity = 'Los Angeles';
      let mockStore = new MockStore({});
      const user = { name: 'U1', address: { city: 'Boston', state: 'Mass', zip: 888 } } as MockUser
      mockStore.UpdateUsers( [user]);
      const sub = mockStore.onPropertyChange<string>(`users[?(@.name=="${user.name}")].address.city`).subscribe((city: string | undefined) => {
        expect(city).toBe(updatedCity);
      })
      if (user?.address != null) {
        user.address.city = updatedCity;
        mockStore.UpdateUsers([user]);
      }

      sub.unsubscribe();
    }));

Any thoughts?
Thank you in advance
Arash

Store state get's overwritten when using ReduxDevToolsExtension

Hi there

Great project, first of all. I really enjoy the simplicity!
I've played around with it a little bit and introduced a simple scenario in one of our angular apps. It all works quite well until I add the following line to my main.ts

ObservableStore.addExtension(new ReduxDevToolsExtension());

Now what I observe is, that the state of my store get's overwritten by what seems are routing events:

{
  "__devTools": {
    "router": {
      "path": "/admin"
    },
    "action": "ROUTE_NAVIGATION"
  }
}

Here is my store:

import {Injectable} from '@angular/core';
import {ObservableStore} from '@codewithdan/observable-store';
import {SettingsStoreState, TenantSettings} from './settings.store.state';
import {Observable, of} from 'rxjs';
import {TenantSettingsDataApiService} from '../services/tenant-settings-data-api.service';
import {tap} from 'rxjs/operators';

@Injectable({
    providedIn: 'root'
})
export class SettingsStore extends ObservableStore<SettingsStoreState> {

    constructor(private tenantSettingsDataApiService: TenantSettingsDataApiService) {
        super({trackStateHistory: true});
    }

    get() {
        const settings = this.getState();
        if (settings) {
            return of(settings);
        } else {
            return this.tenantSettingsDataApiService
                .getSettings()
                .pipe(
                    tap(res => this.setState(res))
                );
        }
    }

    update(settings: TenantSettings): Observable<any> {
        return this.tenantSettingsDataApiService.updateSettings(settings)
            .pipe(
                tap(() => this.setState(settings, 'UPDATE_SETTINGS'))
            );
    }
}

So the first call to this.getState() somehow returns the Routing-Event which I posted above. (When it actually should find an empty state and initialize the store with the data from the backend).
How is this possible?

Thanks for any pointers to a solution.

Kindly, riscie

Edit
So I found the code, which adds the ROUTE_NAVIGATION Action. It's part of the redux extension here: https://github.com/DanWahlin/Observable-Store/blob/master/modules/observable-store-extensions/redux-devtools.extension.ts.
I am just no sure yet, why this is getting written into my own store.

how to implement flattenning strategies

Hello, I would like to know a good way/any ideas to implement RxJs Flattening strategies, for example, the exhaust map, I do have a service.store like this:

@Injectable({providedIn: 'root'})
export class UsersStateService extends ObservableStore<any> {

  constructor(private usersService: UsersService) {}

 // this function is called from a button in the component
  load() {
    // loading sample
    this.setState(
     {
      ...this.getState(), // keeps the state
      loading: true
     }
    );

    this.usersService.getUsers().subscribe(data => { // makes http.get call and returns an obs$
      this.setState({ users: data, loading: false});
    })
  }
}


https://stackblitz.com/edit/observable-store-sample

but my question is, the user can click multiple times on the button and it will make a request per click, solution? exhaust map, I have a solution but I don't know if it is a good approach:

@Injectable({providedIn: 'root'})
export class UsersStateService extends ObservableStore<any> {

  constructor(private usersService: UsersService) {}
    /* 
      I've been thinking about creating a load$ subject and move 
      the code above to a different function, something like:  */

      // I will have a subject for each action that I wanna control and apply flattening strategies
      private load$ = new Subject();

      constructor() { 
        super({});
 
        // a function for each action that I wanna control and apply flattening strategies
        this.loadAction();
      }
      // called from a button in the component
      load() {
        this.load$.next(); // dispatches a value to the subject
      }
      
      // only exhaustMap or any condition I can create decides if it will be called
      private _loadAction() {
        this.load$.pipe(
          exhaustMap(() => this.usersService.getUsers()), // or concat, merge, switchMap, my special condition
        ).subscribe( data => {
          this.setState({ users: data, loading: false});
        });
      }
}

LINK without any flattening strategies applied: https://stackblitz.com/edit/observable-store-sample

Do you guys have any ideas about it? is it a good approach? Thank you!

How to set the initial state?

Hi Dan,

In the current version, the state is null/undefined until the state is set. What's the best way to set an initial state for a store?

Right now I'm doing this:

  constructor() {
    this.setState({}, 'INITIALIZE_STATE');
  }

However, I'm not sure this is semantically correct or just a workaround. Feels wrong somehow, because there was no action triggered - it's just the state being initialized (not an action imho).

Maybe a setInitialState or initializeState method is necessary.

  constructor() {
    this.setInitialState({});
  }

Or maybe in the super constructor since initializing state would be done only once?

  constructor() {
    this.super({});
  }

However, that is already used for the config/settings...

Any thoughts on this?

Incorrect result received in function subscribed to stateChanged

Hi Dan,

First of all, thank you for this great repository. I wanted to use it following your recommendation on Pluralsight.

However, there is a bug with what's being emitted through stateChanged observable.

In https://github.com/DanWahlin/Observable-Store/blob/master/modules/observable-store/observable-store.ts on line 20, we can see stateDispatcher being initialized with generic type T.

private _stateDispatcher$ = new BehaviorSubject<T>(null);

However on line 265, we can see that the state is incorrectly dispatched as a complex object, which has nothing to do with T.

this._stateDispatcher$.next({ state: clonedStateOrSlice, stateChanges } as any);

On line 61, we can see that stateChanged is in fact observable which emits, what stateDispatcher receives, expecting to forward T down the line.

this.stateChanged = this._stateDispatcher$.asObservable();

If we subscribe to stateChanged from our code, we get an incorrect object.

For example, instead of receiving:

{ projects: Array<IProject>, project: IProject> }

we get:
{ state: { projects: Array<IProject>, project: IProject> }, stateChanges: ...}

Strong typed result in subscribe function suggests that result.project should exist, however it is always null in runtime.
The only way to workaround that issue is to cast result to any, so I think this is a bug.

If it was intention to emit the current state, and state changes, there should be a separate object for that.

For example:

export interface IStateChangedResult {
    state: T;
    stateChanges: T;
}

export class StateChangedResult implements IStateChangedResult {
    state: T;
    stateChanges: T;

    public constructor(init?: Partial<StateChangedResult>) {
        (<any>Object).assign(this, init);
    }
}

and then, on line 265 emit:
this._stateDispatcher$.next(new StateChangedResult({ state: clonedStateOrSlice, stateChanges }));

Of course, line 20, should then be replaced with:
private _stateDispatcher$ = new BehaviorSubject<StateChangedResult<T>>(null);

Any help is appreciated.
Thanks

Redux Extension throws runtime error in angular prod build

I am getting the following runtime error in my angular production build, when I add the redux devtools according to the README.md:

TypeError: Cannot read property 'send' of undefined
  at e.sendStateToDevTool (/main-es2015.e6b3fec3e40857a99079.js:1:665719)
  at l._next (/main-es2015.e6b3fec3e40857a99079.js:1:665456)
  at l.__tryOrUnsub (/main-es2015.e6b3fec3e40857a99079.js:1:184374)
  at l.next (/main-es2015.e6b3fec3e40857a99079.js:1:183595)
  at c._next (/main-es2015.e6b3fec3e40857a99079.js:1:182766)
...

I reckon this is because the devtools do not exist in the prod build.
I therefore changed my main.ts to the following:

main.ts

// ...
import { ReduxDevToolsExtension } from '@codewithdan/observable-store-extensions';
// ...
if (!environment.production) {
    ObservableStore.addExtension(new ReduxDevToolsExtension());
}

Should this be added to the docs, or is there another way this should be handled? I could make a quick pr if it helps.

Using state in synchronous manner

Is there a way to use the state of a class in a synchronous way because there is a limitation in some places in angular for using asynchronous value and we should provide the data with synchronous methods for instance we should provide such thing in interceptors, I tried to get rid of state asynchronous behavior with a trick but it didn't work
I created a method in my state class and I tend to call it in other places but it always returns the default value!!!
plz help to overcome this issue

public getUserState(): UserState | null {
  const state = this.getState();
  console.log('state in auth service', state);
  if (state && state.currentUser) {
    return state;
  }
  return null;
}

FYI @DanWahlin

API suggestion: state$ instead of stateChanged

Have you considered using state$ instead of stateChanged for the observable? I think it would be more intuitive for two reasons.

  1. Even if the suffix $ is not a standard, it is used by most developers and libraries using Observables/RxJs. It's widely understood that this variable/property is an observable (or stream of updates if you will). For example the popular library UI-Router uses it for state (state$) and param (param$) updates.

  2. stateChanged sounds like we are dealing with an event (state change event) rather than the state itself. To my understanding this is not an event, right? It's the observable state (i.e. the observable version of getState()). An event would be a subset/superset or completely different structure, but this is not the case if I understand correctly - it's a 1:1 mapping. If this is correct it should be called just state, imho.

Build warning for CommonJS with ng-cli 10

Several dependencies (not just yours) give this message nowadays on ng build --prod

WARNING in [xxxxx]/store.ts depends on '@codewithdan/observable-store'. 
   CommonJS or AMD dependencies can cause optimization bailouts.
For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies
Angular CLI: 10.0.1
Node: 14.4.0
OS: darwin x64

Angular: 10.0.2
Ivy Workspace: Yes
"@codewithdan/observable-store": "^2.2.11",

README.md Angular Sample Incorrect

    ngOnInit() {
        // If using async pipe
        // this.customers$ = this.customersService.stateChanged;

image

Tried submitting a PR for it, but no privs :D

The whole project is remarkably simple. I can see how this would end up with a much simpler approach to state management!

Why won't a getter from a service/state work after any store changes?

Hi there,

Just to clarify my understanding:

If you read: #using-observable-store-with-angular => Point 9, it says the following:

// Can call service/store to get data directly 
// It won't fire when the store state changes though in this case
//this.storeSub = this.customersService.get().subscribe(custs => this.customers = custs);

I find that the following is indeed not working:

public getIsLoggedIn(): Observable<boolean> {
    return this.stateChanged.pipe(switchMap((x) => of(x.isAuthenticated)));
}

Why isn't it the case that when you call a get wrapper function that it won't fire any subsequent, or even initial/last state changes?

Also here: #comment @DanWahlin says the following:

... Currently the store will send the latest state so I wanted to check on that part of your scenario there.

I'm thinking i'm missing some big part of the puzzle but I haven't been able to figure it out.

Thank you for any answers!

Why stateChanged: Observable<any>?

public stateChanged: Observable<any>;

why

stateChanged: Observable<any>?

instead of:

stateChanged: Observable<T>?

Why the generic type passed is not used in the state changes observable type return?

In this case above, I can't take advantages as vscode autocomplete, I needed to create another observable called stageChanges specifying the Observable returned type to have this feature:

Screen Shot 2019-09-04 at 19 29 31

subscribing into stageChanges instead stageChanged, and then I have the autocomplete,

Screen Shot 2019-09-04 at 19 37 25

I was thinking about to send a PR but maybe there is a reason for it.

Are there any reasons why there is no generics usage?

Weird store structure when using stateSliceSelector

Issue

When I'm using the StateSliceSelector along the reduxExtension I see some weird behavior regarding the store. Depending on the moment I call setState, the store is updated but with a different structure:

export class RegistryService extends ObservableStore<StoreState> {
	constructor() {
		super({
			trackStateHistory: true,
			stateSliceSelector: (state) => state && state[ProRegistryStoreName],
		});
		logger.log('REGISTRY', 'Initialize registry service');

		this.setStoreInitialState();
	}
}
private setStoreInitialState(): void {
		const initialState = {
			[ProRegistryStoreName]: {
				'test': 'test',
				[ProRegistryEnum.publicApps]: [],
				[ProRegistryEnum.privilegedApps]: [],
			},
		};
		this.setState(initialState, 'INITIALIZE_REGISTRY_STATE');
	}

SaveApplications is called from other 2 methods

private saveApplications(type: ProRegistryEnum, applications: ApplicationConfig[], storeAction: ProteanStoreActions): void {
		const state = this.getState(); // We need to cast it as any because it will break when setting the state
		state[type] = applications;
		const newState = Object.assign({}, state, {
			[ProRegistryStoreName]: {
				[type]: applications,
			},
		});

		this.setState(newState, storeAction);
	}

If I don't use the stateSliceSelector option in my constructor everything seems to work fine but would like to be able to use it as I have multiple sections in my store and I want them separated and I'm also listening for specific store changes on those sections.

this.registryService.stateChanged.subscribe( ... do something here)

I expect that the above should be called only when that part of the store is updated, which works only with the stateSliceSelector, right?

I'm not sure if this is an issue or not, but the store doesn't seem right.

withoutStateSliceSelector
withStateSliceSelector
next step

can't build the project

ERROR in projects/mariage/src/app/app.component.ts(39,7): error TS2345: Argument of type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/rxjs/inter
nal/types").OperatorFunction<any, {}>' is not assignable to parameter of type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/@codewithdan/observable-s
tore/node_modules/rxjs/internal/types").OperatorFunction<any, {}>'.
Types of parameters 'source' and 'source' are incompatible.
Type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/@codewithdan/observable-store/node_modules/rxjs/internal/Observable").Observable' is
not assignable to type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/rxjs/internal/Observable").Observable'.
Types of property 'operator' are incompatible.
Type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/@codewithdan/observable-store/node_modules/rxjs/internal/Operator").Operator<any, any>
' is not assignable to type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/rxjs/internal/Operator").Operator<any, any>'.
Types of property 'call' are incompatible.
Type '(subscriber: import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/@codewithdan/observable-store/node_modules/rxjs/internal/Subscriber")
.Subscriber, source: any) => import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/@codewithdan/observable-store/node_modules/rxjs/internal/type...' is n
ot assignable to type '(subscriber: import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/rxjs/internal/Subscriber").Subscriber, source: any) => import("
D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/rxjs/internal/types").TeardownLogic'.
Types of parameters 'subscriber' and 'subscriber' are incompatible.
Property '_parentOrParents' is missing in type 'Subscriber' but required in type 'Subscriber'.
projects/mariage/src/app/modules/widget/navbar/navbar.component.ts(35,7): error TS2345: Argument of type 'OperatorFunction<SettingsState, boolean>' is not assignable to p
arameter of type 'OperatorFunction<any, boolean>'.
Types of parameters 'source' and 'source' are incompatible.
Type 'Observable' is not assignable to type 'Observable'.
Types of property 'source' are incompatible.
Type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/@codewithdan/observable-store/node_modules/rxjs/internal/Observable").Observable'
is not assignable to type 'import("D:/My Data/localdev/workStuff/mariage-serein-web/node_modules/rxjs/internal/Observable").Observable'.

Unable to store an Observable

Hello Dan,

I'm trying to store some observables instead of the object itself so I can make use of the async pipe at the end of the data flow.

Although, I'm getting this error
core.js:6210 ERROR TypeError: Converting circular structure to JSON

I guess the cloning logic is not compatible with an Observable.

Would that be the issue? What workaround is possible while maintaining the Observables?

Thanks!

Question on sharing service

So I have a question and not an issue (I apologize if this is the wrong spot!), but I have a UserPermission service that extends ObservableStore.

My question is, if I am on a roles component, is there a way to hit the StateChanged without adding the customerService to the constructor of the roles service?

Also, is there a way to subscribe to only one of the state changed services instead of all of them? so something like userPermissionService.StateChanged.UserPermissions.subscribe(x={do fancy stuff here})?

Thanks in advance!

Is it possible to subscribe to a single change?

Hi Dan,

First of all: thanks for this Observable Store! I come from a Vue / Vuex background and had some trouble fitting a NGRX / NGXS workflow in my mindset, but Observable Store 'clicked' inmediately.

I'm having trouble subscribing to a single change in Angular. I have the following structure in my (demo) store:

{
  customer: Customer,
  customers: Customer[],
  sortBy: 'id'
}

I'm setting this initial state in the constructor, after calling super. I would like to be able to listen to the sortBy change in a component .ts file. In the template of this component, a user can click on table headers to change the sort. This is the code that is executed in the store:

setSortBy(key: string): void {
  this.setState({ sortBy: key }, actions.changeSort);
}

I have a 'getter' for the current sort property in the store:

getCurrentSort(): Observable<string> {
  return of(this.getState().sortBy);
}

If I use this method in my component HTML with a async pipe, it works great. But if I try to subscribe to it in the .ts file, I only see a change on init, not after change:

this.customerStore.getCurrentSort().subscribe((sort) => {
  console.log('sorting change');
});

Is this intentional and is it always required to track changes in the component .ts via stateChanged and update all properties accordingly? It would be very convenient to be able to subscribe to single props in the state, but maybe I'm missing something or is this considered bad practice :).

Trying to assign stateChanged to a type Subscription is causing an error

Dan,

When I do the following code

import { Observable, forkJoin, SubscriptionLike, Subscription } from 'rxjs';
storeSub: Subscription;

ngOnInit() {
this.wait('Please wait while the group data is loaded.');
this.loadGroups();
this.storeSub = this.groupMeetingsService.stateChanged.subscribe(state => {
console.log('state changed', state);
});
}

this.storeSub is saying

(property) GroupFunctionsComponent.storeSub: Subscription
Type 'Subscription' is missing the following properties from type 'Subscription': _parent, _parents, _addParentts(2739)

I can use SubscriptionLike and the problem goes away.

Thx
jonpfl

How to store n number of propertes (making the state properties dynamic)

I have a situation where I have many pricing scenarios that can be applied to a job. Because of this I was going to create an object that looked like StoreState{PricingCalculationInfo:{ [name: string]: PricingCalculationInfo }}. However, because I am reusing a bunch of components, any time that PricingCalculationInfo object changes, every scenario would then run through all of their subscriptions again, which I don't need to happen.

Because of this, unless there is a way for me to tell which property of PricingCalcInfo changed, I am going to need a list of dynamic store state properties. Each scenario would then need its' own StoreState that it can subscribe to (or at least exclude all changes except when that scenario's state changes).

Is this possible to make the store state properties a little more dynamic?

setState very slow

    public addShowInfo(showInfo: ShowInfo) {
        console.log("start set showinfo state")
        this.setState({ showInfo: showInfo }, ShowInfoStoreActions.AddShowInfo);
        console.log("finish set showinfo state")
    }

I have this simple setState which takes about 3-4 seconds to run. Any idea why?

ObservableStore cannot be found

Hello i have following issue: when i try to extend the class ObservableStore, angular complains that cannot be found.
image
"@codewithdan/observable-store": "^1.0.19", i got this one in my package.json but still cannot be seen by my angular

Super() Expected 1 argument but got 2

Hi,

Looks like an interesting library which I'm investigating for a project.

I get an error on the super() call below when I try to run the sample saying that it

Expected 1 argument but got 2

constructor() {
        const initialState = {
            customers: [],
            customer: null
        }
        super(initialState, { trackStateHistory: true });
    }

not working out of the box with jest

I get the following error when I run a test that uses the store with jest:

SyntaxError: Unexpected token export

  1 | import {Injectable, OnDestroy} from '@angular/core';
> 2 | import {ObservableStore} from '@codewithdan/observable-store';

I tried to add transformIgnorePatterns like:

"transformIgnorePatterns": [
"node_modules/(?!@@CodeWithDan|observable-store)"
]

But it did not help.

Testing capabilities

Hello, Dan was playing around with Observable-Store library in Angular application and now when I come up to the unit testing of service that implements ObservableStore I am not sure what is a proper way to test it.
I have not found any hints in the documentation about the unit testing.
I have tried to track state history in tests, but I would like to have a possibility to reset State in a testing environment without the need to implement it in each service I am going to unit test.
I think there might be some other features that can be included in the 'observable-store/testing' module.

Can't run Angular samples

I've tried all 4 Angular samples and after running 'ng serve -o' i'm getting errors in the terminal.
For example, in the angular-store-edits sample, i'm getting the following:

ERROR in C:/Repos/Observable-Store/modules/observable-store/index.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store'
ERROR in C:/Repos/Observable-Store/modules/observable-store/observable-store.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store'
ERROR in C:/Repos/Observable-Store/modules/observable-store/observable-store-base.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store'
ERROR in C:/Repos/Observable-Store/modules/observable-store-extensions/index.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store-extensions'
ERROR in C:/Repos/Observable-Store/modules/observable-store-extensions/redux-devtools.extension.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store-extensions'
ERROR in C:/Repos/Observable-Store/modules/observable-store-extensions/angular/angular-devtools-extension.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store-extensions\angular'
ERROR in C:/Repos/Observable-Store/modules/observable-store/utilities/cloner.service.ts
Module not found: Error: Can't resolve 'tslib' in 'C:\Repos\Observable-Store\modules\observable-store\utilities'

I've tried deleting the node modules folder and installing the npm packages again, but that didn't make a difference.

However if i hard-reset back to commit dd4a118 then it's working fine.

Handling multiple domain objects being retreived

Hi Dan,
Like the keep it simple approach. If your project has say 8 entities, each of which you wish to keep the state for, and each of which can be retreived separately from your back end as well, then am I correct in thinking you will need 8 different Observable Store instances? JSo that each Observable store tracks and maintains state for one particular entity?

Am asking, because if we were to keep all 8 in a single store, then it would mean all our http get / post etc for all 8 entitities would be in the same class, which would become hard to maintain.

Handling this usecase was not clear from the readme hence wanted to ask. thanks.

Issue in README.md

existing readme:

add(customer: Customer) {
            let state = this.getState();
            state.customers.push(customer);
            this.setState({ customers: state.customers }, 'add_customer');
        }

        remove() {
            let state = this.getState();
            state.customers.splice(state.customers.length - 1, 1);
            this.setState({ customers: state.customers } 'remove_customer');
        }
        
        sort(property: string = 'id') {
            let state = this.getState();
            const sortedState = this.sorterService.sort(state.customers, property);
            this.setState({ customers: sortedState } 'sort_customers');
        }

Expected readme:

add(customer: Customer) {
         let state = this.getState();
         state.customers.push(customer);
         this.setState({ customers: state.customers }, 'add_customer');
     }

     remove() {
         let state = this.getState();
         state.customers.splice(state.customers.length - 1, 1);
         this.setState({ customers: state.customers }, 'remove_customer');
     }
     
     sort(property: string = 'id') {
         let state = this.getState();
         const sortedState = this.sorterService.sort(state.customers, property);
         this.setState({ customers: sortedState }, 'sort_customers');
     }

I may sound silly but just thought you should know, Thanks

Missing properties after setting state

Thank you for this wonderful package.

I am having issues using it in an Angular 10 application using Typescript.

I have setup a store almost identical to the ones provided in the sample application. I have an interface that looks like this:

export interface FileStoreState {
  files: ImportedFile[];
  selectedFiles: SelectionModel<ImportedFile>;
}

I then create a service that extends ObservableStore and give it a type of FileStoreState

@Injectable({
  providedIn: 'root'
})
export class FileService extends ObservableStore<FileStoreState> {
  
  constructor() {
    super({ trackStateHistory: true, logStateChanges: true });
   
    const initialState = {
      files: [],
      selectedFiles: null
    }
    this.setState(initialState, FileStoreActions.Init)
  }

  addFiles(files: Array<ImportedFile>) {
    let state = this.getState();

    files.filter(x => {state.files.push(x)});

    this.setState({ files: state.files}, FileStoreActions.AddFiles)
  }

}

Now my imported File looks like this:

export class ImportedFile {
    electronFileId: string;
    type: string;
    size: string;
    path: string;
    bytes: any;
}

I call the addFiles function in on of my components.

  filesSelected(files: File[]) {    
    let mappedFiles = files
      .filter(x => this.isAcceptedFileType(x))
      .map(x => new ImportedFile(x, FileType.fileSystem));

    **this.fileService.addFiles(mappedFiles);**
  }

When I console log mappedFiles it gives me a result that looks like this:

image

Which is perfect! It has all of the properties and data I am expecting..

When addFiles is called in the service it should push those files into the files state within the store.

This is where it gets weird... In the console log of the STATE CHANGED it looks as if the files are there with their associated data.

image

Yet, when I subscribe to state changes the bytes property disappears completely when I subscribe. Also, checking the state afterward shows that the property is no longer there. This is happening with multiple typescript interfaces I am trying to use.

this.storeSub = this.fileService.stateChanged.subscribe(state => {
      if (state) {
        if (state.files.length > 0) {
          console.log(state.files);
          this.files.data = state.files;
        }
      }
    });

The console log now displays this:

image

Which are the files that I was expecting yet, its missing the bytes property. Also, now when I try and do a this.getState() its also missing from there even though the state change log says it should be there.

None of this makes sense, I'm unsure why this is happening. This is a simple example, it seems to be happening on every single object I am trying to save the state of. For example: SelectionModel ends up losing its functions like toggle and select

Thank you!

setState slow with large array

I have around 70000 objects in an array. I turn off deep cloning but setState is still a bit slow. Is it because deepCloneState isn't passed to getState() in the method?

Awesome library btw.

is it possible to filter by Action?

If say Im manipulating state, but would like to only react to specific actions in other parts of the app.
maybe add the Action type somehow to stateChanged

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.