Giter VIP home page Giter VIP logo

resub's Introduction

ReSub

GitHub license npm version npm downloads Build Status David David

A library for writing better React components and data stores. Uses automatic subscriptions to reduce code and avoid common data flow pitfalls. Scales for projects of all sizes and works great with TypeScript.

ReSub v2

For the 2.x+ ReSub releases, to get in line with the apparent future of React, we have changed how ReSub uses the React lifecycle functions to instead use the getDerivedStateFromProps path. See this link for more details on the new lifecycle. This means that, for the standard happy path of just using _buildState, everything should keep working just like it used to. However, if you were using the old React lifecycle functions (componentWillMount, componentWillReceiveProps, etc.) in any way, you will likely find those functions not being called anymore. This change should future-proof ReSub for a while, and play more nicely with the future async rendering path that React is moving toward.

As a small secondary note, for 2.x+, we also removed support for the old-school manual subscriptions. We strongly suggest moving to either autosubscriptions (where ReSub's true differentiating value is) or, if you prefer manual subscriptions, directly subscribe to the stores in your component using whatever lifecycle you prefer.

All in all, for more complicated projects especially, these may be substantial changes to your usage of ReSub, and upgrading may be hard. You're obviously welcome to keep using the 1.x branch of ReSub indefinitely, and if there are bugs, please let us know and we will attempt to fix them, but we won't be putting any more energy into features/examples/etc. on 1.x and should consider it to be on LTS at this point, since it doesn't work with versions of React newer than 16.8.6.

Overview

In React’s early days, Flux gave us guidance on how to manage data flow in our apps. At its core, data would be placed into stores and React components would fetch it from them. When a store’s data was updated, it would notify all concerned components and give them the opportunity to rebuild their states.

While Flux works well, it can also be cumbersome and error prone. Separate actions, action creators, and stores can result in a great deal of boilerplate code. Developers can fetch data from a store but fail to subscribe to changes, or components can oversubscribe and cause performance issues. Furthermore, developers are left to implement these patterns from scratch.

ReSub aims to eliminate these limitations (and more) through the use of automatic data binding between stores and components called autosubscriptions. By using TypeScript’s method decorators, ReSub components can subscribe to only the data they need on only the stores that provide it, all without writing any code. ReSub works with both traditional class components as well as function components, which is the direction that React seems to be heading for most usage.

Basic Example

The easiest way to understand ReSub is to see it in action. Let’s make a simple todo app.

The heavy lifting in ReSub is done mostly within two classes, ComponentBase and StoreBase. It’s from these that we make subclasses and implement the appropriate virtual functions.

First, we create a store to hold todos:

import { StoreBase, AutoSubscribeStore, autoSubscribe } from 'resub';

@AutoSubscribeStore
class TodosStore extends StoreBase {
    private _todos: string[] = [];

    addTodo(todo: string) {
        // Don't use .push here, we need a new array since the old _todos array was passed to the component by reference value
        this._todos = this._todos.concat(todo);
        this.trigger();
    }

    @autoSubscribe
    getTodos() {
        return this._todos;
    }
}

export = new TodosStore();

Next, we create a component to display the todos:

import * as React from 'react';
import { ComponentBase } from 'resub';

import TodosStore = require('./TodosStore');

interface TodoListState {
    todos?: string[];
}

class TodoList extends ComponentBase<{}, TodoListState> {
    protected _buildState(props: {}, initialBuild: boolean, incomingState: {} | TodoListState): TodoListState {
        return {
            todos: TodosStore.getTodos()
        }
    }

    render() {
        return (
            <ul className="todos">
                { this.state.todos.map(todo => <li>{ todo }</li> ) }
            </ul>
        );
    }
}

export = TodoList;

That’s it. Done!

When future todos are added to the TodoStore, TodoList will automatically fetch them and re-render. This is achieved because TodoList._buildState makes a call to TodosStore.getTodos() which is decorated as an @autoSubscribe method.

Subscriptions and Scaling

ReSub is built with scalability in mind; it works for apps of all sizes with all scales of data traffic. But this doesn’t mean scalability should be the top concern for every developer. Instead, ReSub encourages developers to create the simplest code possible and to only add complexity and tune performance when it becomes an issue. Follow these guidelines for best results:

  1. Start by doing all your work in _buildState and rebuilding the state from scratch using autosubscriptions. Tracking deltas and only rebuilding partial state at this stage is unnecessary for the vast majority of components.
  2. If you find that components are re-rendering too often, introduce subscriptions keys. For more information, see the “Subscriptions by key” and “Subscriptions by props” sections below.
  3. If components are still re-rendering too often, consider using trigger throttling and trigger blocks to cut down on the number of callbacks. For more information, see the “Trigger throttling” and “Trigger blocks” sections below.
  4. If rebuilding state completely from scratch is still too expensive, manual subscriptions to stores (store.subscribe()) with callbacks where you manage your own state changes may help.

A Deep Dive on ReSub Features

Subscriptions and Triggering

Subscriptions by key:

By default, a store will notify all of its subscriptions any time new data is available. This is the simplest approach and useful for many scenarios, however, stores that have heavy data traffic may result in performance bottlenecks. ReSub overcomes this by allowing subscribers to specify a string key that limit the scope in which they will trigger.

Consider an example where our Todo app differentiates between high and low priority todo items. Perhaps we want to show a list of all high priority todo items in a HighPriorityTodoItems component. This component could subscribe to all changes on the TodosStore, but this means it’d re-render even when a new low priority todo was created. That’s wasted effort!

Let’s make TodosStore smarter. When a new high priority todo item is added, it should trigger with a special key TodosStore.Key_HighPriorityTodoAdded instead of using the default StoreBase.Key_All key. Our HighPriorityTodoItems component can now subscribe to just this key, and its subscription will trigger whenever TodosStore triggers with either TodosStore.Key_HighPriorityTodoAdded or StoreBase.Key_All, but not for TodosStore.Key_LowPriorityTodoAdded.

All of this can still be accomplished using method decorators and autosubscriptions. Let’s create a new method in TodosStore:

class TodosStore extends StoreBase {
    ...

    static Key_HighPriorityTodoAdded = "Key_HighPriorityTodoAdded";

    @autoSubscribeWithKey(TodosStore.Key_HighPriorityTodoAdded)
    getHighPriorityTodos() {
        return this._highPriorityTodos;
    }
}

Note: Of course it’s possible to separate high and low priority todo items into separate stores, but sometimes similar data is simultaneously divided on different axes and is therefore difficult to separate into stores without duplicating. Using custom keys is an elegant solution to this problem.

Autosubscriptions using @key:

Key-based subscriptions are very powerful, but they can be even more powerful and can reduce more boilerplate code when combined with autosubscriptions. Let’s update our TodosStore to add the @key decorator:

class TodosStore extends StoreBase {
    ...

    @autoSubscribe
    getTodosForUser(@key username: string) {
        return this._todosByUser[username];
    }
}

Now, we can establish the autosubscription for this user in _buildState:

class TodoList extends ComponentBase<TodoListProps, TodoListState> {
    ...

    protected _buildState(props: {}, initialBuild: boolean, incomingState: {} | TodoListState): TodoListState {
        return {
            todos: TodosStore.getTodosForUser(this.props.username)
        }
    }
}

_buildState will be called when TodoStore triggers any changes for the specified username, but not for any other usernames.

Compound-key subscriptions/triggering

Sometimes, either when a single store contains hierarchical data, or when you have more than one parameter to a function that you'd like to have key-based subscriptions to (i.e. a user and a name of an object that the user has), the single @key mechanism isn't good enough. We've added the ability to put @key on multiple parameters to a function, and ReSub concatenates them with the formCompoundKey function (also exported by ReSub) to form the actual subscription key. You can also combine this with @autoSubscribeWithKey to have even more hierarchy on your data. Note that the @autoSubscribeWithKey value always goes on the end of the compound key, since it should be the most selective part of your hierarchy.

To trigger these compound keys, you execute this.trigger(ReSub.formCompoundKey('key1val', 'key2val', 'autoSubscribeWithKeyval')) and it will trigger the key to match the autosubscription of your function.

NOTE: Compound keys themselves don't actually support any sort of hierarchy. If you don't trigger EXACTLY the correct key, your subscriptions will not update. If you have a key of ['a', 'b', 'c'], and you trigger ['a', 'b'], you will be disappointed to find that none of your subscribed components update. Compound keys are designed to help you provide discrete updates within a hierarchy of data, but are not designed to allow for updating wide swaths of that hierarchy.

Example of correct usage:

enum TriggerKeys {
    BoxA = 'a',
    BoxB = 'b',
}
class UserStuffStore extends StoreBase {
    private _stuffByUser: {[userCategory: string]: {[username: string]: {boxA: string; boxB: string;}}}

    @autoSubscribeWithKey(TriggerKeys.BoxA)
    getBoxAForUser(@key userCategory: string, @key username: string) {
        return this._stuffByUser[userCategory][username].boxA;
    }

    @disableWarnings
    setBoxAForUser(userCategory: string, username: string, boxAValue: string): void {
        this._stuffByUser[userCategory][username].boxA = boxAValue;
        this.trigger(ReSub.formCompoundKey(userCategory, username, TriggerKeys.BoxA));
    }
}

class UserBoxADisplay extends ComponentBase<SomeProps, SomeState> {
    ...

    protected _buildState(props: SomeProps, initialBuild: boolean, incomingState: {} | SomeState): TodoListState {
        return {
            boxA: UserStuffStore.getBoxAForUser(props.userCategory, props.username),
        };
    }
}

withResubAutoSubscriptions

We've added a new hook-like mechanism to ReSub in 2.3. It uses hooks under the covers, so treat them like hooks for ordering purposes and not using them inside conditional blocks. It basically uses the same autosubscribe logic from ComponentBase, but it does so from inside function components that have been wrapped in the withResubAutoSubscriptions HOC wrapper function. Let's just jump straight into an example, it'll be more clear. Let's adapt the initial example from earlier in the doc to the new format. The TodosStore is the same as above, but we can now make the TodoList function much simpler.

import * as React from 'react';
import { withResubAutoSubscriptions } from 'resub';

import TodosStore = require('./TodosStore');

function TodoList() {
    const todos = TodosStore.getTodos();

    return (
        <ul className="todos">
            { this.state.todos.map(todo => <li>{ todo }</li> ) }
        </ul>
    );
}

export default withResubAutoSubscriptions(TodoList);

Much simpler, right? The call to getTodos is intercepted just like it were being called from a _buildState class method, and a subscription is automatically generated back to the TodosStore, which causes a useState mutation internally if the subscription gets triggered, which then makes the function component re-render again. Super easy. Just remember that these methods need to be treated like hooks for all intents and purposes when used like this, and that you need to wrap your function component in the withResubAutoSubscriptions call, or else autosubscriptions won't work and you'll be very confused. In development mode, there's a check that should warn you if you get this wrong, but in production mode (Options.development === false), the check disappears, so you're on your own!

ComponentBase

To get the most out of ReSub, your components should inherit ComponentBase and should implement some or all of the methods below.

Callable methods:

isComponentMounted(): boolean

Returns true if the component is currently mounted, false otherwise. Subclasses should not override this method.

shouldComponentUpdate(nextProps: P, nextState: S): boolean

ReSub’s implementation of this method always returns true, which is inline with React's guidance. If you wish to apply optimizations using shouldComponentUpdate we provide a few different methods to do this:

  1. Provide a shouldComponentUpdateComparator to the ReSub Options payload. This is the default comparator that is used in shouldComponentUpdate for components that extend ComponentBase. This is a good way to apply custom shouldComponentUpdate logic to all your components.
  2. Override shouldComponentUpdate in specific components and don't call super
  3. Apply a decorator to specific component classes to apply default or custom shouldComponentUpdate comparators. Examples:
    • @CustomEqualityShouldComponentUpdate(myComparatorFunction) - This will call your custom comparator function (for Props, State and Context), returning true from shouldComponentUpdate if your comparator returns false. This is built into ReSub's public module export.
    • @DeepEqualityShouldComponentUpdate - This will do a deep equality check (_.isEqual) on Props, State & Context and return true from shouldComponentUpdate if any of the values have changed. As of 2.1.0+ of ReSub, this is no longer included in the public module export, to avoid including lodash in your bundles. The implementation, if you want to still use it:
import isEqual from 'lodash/isEqual';

import ComponentBase from './ComponentBase';

export function DeepEqualityShouldComponentUpdate<T extends { new(props: any): ComponentBase<any, any> }>(constructor: T): T {
    return CustomEqualityShouldComponentUpdate<any, any>(deepEqualityComparator)(constructor);
}

function deepEqualityComparator<P extends React.Props<any>, S = {}>(
        this: ComponentBase<P, S>, nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
    return isEqual(this.state, nextState) || isEqual(this.props, nextProps) || isEqual(this.context, nextContext);
}

Note: _.isEqual is a deep comparison operator, and hence can cause performance issues with deep data structures. Weigh the pros/cons of using this deep equality comparator carefully.

Subclassing:

Subclasses should implement some or all of the following methods:

protected _buildState(props: P, initialBuild: boolean, incomingState: {} | S): Partial<S> | undefined

This method is called to rebuild the module’s state. All but the simplest of components should implement this method. It is called on three occurrences:

  1. During initial component construction, initialBuild will be true. This is where you should set all initial state for your component. This case rarely needs special treatment because the component always rebuilds all of its state from its props, whether it's an initial build or a new props received event.
  2. In the React lifecycle, during a getDerivedStateFromProps, this is called before every render.
  3. When this component subscribes to any stores, this will be called whenever the subscription is triggered. This is the most common usage of subscriptions, and the usage created by autosubscriptions.

Any calls from this method to store methods decorated with @autoSubscribe will establish an autosubscription.

React’s setState method should not be called directly in this function. Instead, the new state should be returned and ComponentBase will handle the update of the state.

Note: If your _buildState ever relies on component state, utilize the incomingState argument, otherwise you risk using an old snapshot of the state (See #131 for more details)

protected _componentDidRender()

This method is automatically called from componentDidMount and componentDidUpdate, as both of these methods typically do the same work.

React lifecycle methods

Methods include:

  • constructor(props: P)
  • componentDidMount()
  • componentWillUnmount()
  • componentDidUpdate(prevProps: P, prevState: S)
  • static getDerivedStateFromProps(prevProps: P, prevState: S)

Many of these methods are unnecessary in simple components thanks to _componentDidRender and _buildState, but may be overridden if needed. Implementations in subclasses must call super. If using getDerivedState in a Subclass, Implementations must return ComponentBase.getDerivedStateFromProps(props, state), but can add additional changes to the return value.

StoreBase

ReSub’s true power is realized when creating subclasses of StoreBase. Several features are exposed as public methods on StoreBase, and subclasses should also implement some or all of the virtual methods below.

In addition to providing useful patterns for store creation, StoreBase also provides features to squeeze out additional performance through heavy data traffic.

Trigger throttling:

By default, a store will instantly (and synchronously) notify all of its subscriptions when trigger is called. For stores that have heavy data traffic, this may cause components to re-render far more often than needed.

To solve this issue, stores may specify a throttle time limit by specifying throttleMs = X (X being a number of milliseconds) during construction. Any triggers within the time limit will be collected, de-duped, and callbacks will be called after the time is elapsed.

Trigger blocks:

In applications with heavy data traffic, especially on mobile browsers, frequent component re-rendering can cause major performance bottlenecks. Trigger throttling (see above) helps this problem, but sometimes this isn’t enough. For example, if the developer wants to show an animation at full 60-fps, it is important that there is little to no other work happening at the same time.

StoreBase allows developers to block all subscription triggers on all stores until the block is lifted. All calls to trigger in this time will be queued and will be released once the block is lifted.

Because certain stores may be critical to the app, StoreBase allows stores to opt out of (and completely ignore) trigger blocks by passing bypassTriggerBlocks = true to the constructor.

Multiple stores or components might want to block triggers simultaneously, but for different durations, so StoreBase counts the number of blocks in effect and only releases triggers once the block count reaches 0.

Callable methods:

subscribe(callback: SubscriptionCallbackFunction, key = StoreBase.Key_All): number

Manually subscribe to this store. By default, the callback method will be called when the store calls trigger with any key, but this can be reduced by passing a specific key. For more information, see the “Subscriptions and Triggering” section.

subscribe returns a token that can be passed to unsubscribe.

unsubscribe(subToken: number)

Removes a subscription from the store.

trigger(keyOrKeys?: string|string[])

Trigger all subscriptions that match the provided keyOrKeys to be called back. If no key is specified, StoreBase.Key_All will be used and all subscriptions will be triggered. For more information, see the “Subscriptions and Triggering” section.

protected _getSubscriptionKeys(): string[]

This method returns a de-duped list of all keys on which subscribers have subscribed.

protected _isTrackingKey(key: string)

Returns true if a subscription has been made on the specified key, or false otherwise. This also responds to the Key_All key to check for global subscriptions.

static pushTriggerBlock()

Calling StoreBase.pushTriggerBlock() will halt all triggers on all stores until the trigger block is lifted by a subsequent call to StoreBase.popTriggerBlock(). For more information, see the “Trigger blocks” section.

static popTriggerBlock()

Lifts the trigger block on all stores and releases any queued triggers. If more than one trigger block is in effect (because more than one store or component wants to block triggers simultaneously), popTriggerBlock will decrement the block count but not release the triggers. For more information, see the “Trigger blocks” section.

Subclassing:

constructor(throttleMs: number = 0, bypassTriggerBlocks = false)

Subclass constructors should call super. throttleMs refers to the throttle time (see “Trigger throttling” section). bypassTriggerBlocks refers to the trigger blocking system (see “Trigger blocks” section).

_startedTrackingSub(key?: string)

StoreBase uses reference counting on subscriptions. This method is called whenever a subscription is first created, either as a global subscription (key = undefined in this function) or with a key.

Subclasses do not need to call super.

_stoppedTrackingSub(key?: string)

StoreBase uses reference counting on subscriptions. This method is called whenever a subscription is last removed, either as a global subscription (key = undefined in this function) or with a key.

Subclasses do not need to call super.

Extensions

A small ecosystem is starting up around ReSub, so if you bring up an extension based on it, let us know and we'll add it to a list here:

  • ReSub-entity: This project eases the use of ReSub by providing a simple function to create a Store for an entity

Data Flow

ReSub avoids taking a strong opinion on data flow in your project.

While it’s not encouraged, it’s fine for components to make calls to modify store data, for components and stores to make AJAX and other asynchronous calls, and for stores to subscribe to one another. Action creators may be used to organize data flow, but they’re not required and often not necessary.

Whether using ReSub or not, your app will likely scale best if it follows these guidelines:

  1. Components should remain pure, and as such, should only get data from props and stores.
  2. Store data should never be modified on the same cycle as component data fetching and rendering. Race conditions and update cycles can form when a component modifies store data while building its state.

Performance Analysis

To assist with performance analysis of your store and component state-building/-triggering, there is a performance module built into ReSub that marks durations for buildState functions and store trigger callbacks. If you want to enable it, call the setPerformanceMarkingEnabled(true) function available on the root ReSub module export.

Using ReSub Without TypeScript

It is fine to use ReSub without TypeScript, but without access to TypeScript’s method decorators, stores and components cannot leverage autosubscriptions, and as such, lose a lot of their value.

At the very least, developers can still leverage the organizational patterns of ComponentBase and StoreBase, and any virtual functions that subclasses implement will still be called.

Using ReSub with Babel

ReSub relies heavily on typescript decorators, which are not supported out of the box when transpiling typescript via babel. If you choose to transpile your project with Babel, be sure to add the following to your babel config:

  plugins: [
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    "babel-plugin-parameter-decorator"
  ],

You'll also need to install babel-plugin-parameter-decorator@^1.0.8 and @babel/plugin-proposal-decorators

TSLint rules

We have couple of tslint rules to automate search of common problems in ReSub usage. They are located at the ./dist/tslint folder of the package. add following rules to your tslint.json in order to use them.

incorrect-state-access rule doesn't check abstract methods called from UNSAFE_componentWillMount, but you could enforce check of your methods by passing them to the rule as an argument.

"incorrect-state-access": [
    true
],

"override-calls-super": [
    true,
    "_buildInitialState",
    "UNSAFE_componentWillMount",
    "componentDidMount",
    "UNSAFE_componentWillReceiveProps",
    "UNSAFE_componentWillUpdate",
    "componentDidUpdate",
    "componentWillUnmount"
],

ESLint rules

TSLint will be deprecated some time in 2019

If you plan to migrate your projects from TSLint to ESlint and want to continue using the rules to automate search common problems in ReSub usage, you can use eslint-plugin-resub.

resub's People

Contributors

a-tarasyuk avatar berickson1 avatar bigga94 avatar btraut avatar dependabot[bot] avatar deregtd avatar dryganets avatar erictraut avatar isnotgood avatar kant avatar magom001 avatar microsoft-github-policy-service[bot] avatar mikehardy avatar ms-deregtd avatar ms-markda 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

resub's Issues

Remove "catch in render -> null" behavior

PR #42 puts this behind the option preventTryCatchInRender but we don't need this logic in the long term. React error boundaries will make this redundant. However, not everyone has updated to the latest React.

Make this change when we update our minimum supported React dependency.

TS2304: Cannot find name 'RX'.

LOVE Resub, want to use it a lot.....but am having compilation errors....

I am using ReactXP, which does use typescript.

My imports are like this

import {  ComponentBase } from 'resub';
import * as React from 'react'
import { TextInput, View, Text , TouchableHighlight, StyleSheet } from 'react-native'
import TodosStore = require('./TodosStore');

It complains about the View, TextInput, TouchableHighlight tags

<View>
<TextInput/>
</View>
 <TouchableHighlight>
   <Text></Text>
 </TouchableHighlight>
</View>
</View>

Non Singleton Usage of Store breaks the subscription keys

The following test shows the error.

import * as React from 'react';
import ComponentBase from '../src/ComponentBase';
import {StoreBase} from '../src/StoreBase';
import {formCompoundKey} from '../src/utils';
import {mount} from 'enzyme';
import {AutoSubscribeStore, autoSubscribeWithKey, key} from '../src/AutoSubscriptions';

interface TestParameters {
    uniqueId: String;
    testStore: SimpleStore;
    propertyKey: string;
}

interface TestState {
    testObject: number;
}

class TestComponent extends ComponentBase<TestParameters, TestState> {

    render(): React.ReactElement<any> | string | number | {} | React.ReactNodeArray | React.ReactPortal | boolean | null | undefined {
        if (!this.state.testObject) {
            return null;
        }
        return this.state.testObject;
    }

    protected _buildState(props: TestParameters, initialBuild: boolean): Partial<TestState> | undefined {
        return {
            testObject: this.props.testStore.getVal(this.props.propertyKey)
        };
    }
}

@AutoSubscribeStore
class SimpleStore extends StoreBase {
    protected value: number = 0;

    constructor() {
        super();
        key(this, 'getVal', 0);
    }

    public setVal(value: number) {
        this.value = value;
        let trigger = formCompoundKey('value', 'TEST');
        this.trigger(trigger);
    }

    protected trigger(keyOrKeys?: string | number | (string | number)[]): void {
        super.trigger(keyOrKeys);
        console.log('triggered:', keyOrKeys);
        console.log(this._getSubscriptionKeys());
    }

    @autoSubscribeWithKey('TEST')
    // @autoSubscribe
    public getVal(key: string): number {
        // @ts-ignore
        return this[key];
    }
}

describe('SimpleStore', () => {
    function testSimpleStore() {
        const testStore1 = new SimpleStore();
        testStore1.setVal(1);
        const testComponent = mount(
            <TestComponent propertyKey={'value'} testStore={testStore1} uniqueId={new Date().getTime() + '1'}/>
        );

        const testStore2 = new SimpleStore();
        testStore2.setVal(2);

        const testComponent2 = mount(
            <TestComponent propertyKey={'value'} testStore={testStore2} uniqueId={new Date().getTime() + '2'}/>
        );
        expect(testComponent.contains('1')).toEqual(true);
        expect(testComponent2.contains('2')).toEqual(true);

        testStore1.setVal(2);
        testComponent.update();
        expect(testComponent.contains('2')).toEqual(true);
        expect(testComponent2.contains('2')).toEqual(true);

        testStore2.setVal(1);
        testComponent2.update();
        expect(testComponent.contains('2')).toEqual(true);
        expect(testComponent2.contains('1')).toEqual(true);

        testStore1.setVal(1);
        testComponent.update();
        expect(testComponent.contains('1')).toEqual(true);
        expect(testComponent2.contains('1')).toEqual(true);

        testStore2.setVal(2);
        testComponent2.update();
        expect(testComponent.contains('1')).toEqual(true);
        expect(testComponent2.contains('2')).toEqual(true);
    }

    describe('should contain the correct values', () => {
        it('while using separate simple stores', testSimpleStore);
    });

    describe('should contain the correct values', () => {
        it('while running the second time..', testSimpleStore);
    });
});

If you run it, then it logs something like the following:

LOG: 'triggered:', 'value%&TEST'
LOG: []
LOG: 'triggered:', 'value%&TEST'
LOG: []
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&TEST']
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&TEST']
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&TEST']
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&TEST']

and the second test gives the following:

LOG: 'triggered:', 'value%&TEST'
LOG: []
LOG: 'triggered:', 'value%&TEST'
LOG: []
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&value%&TEST']
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&value%&value%&TEST']
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&value%&TEST']
LOG: 'triggered:', 'value%&TEST'
LOG: ['value%&value%&value%&value%&TEST']

While the Log in the square brackets '[ ]' are the subscription keys of the store, that is triggering, and the Logs with the 'triggered:' show the trigger key.

As you can see, there seems to be a problem with creating the subscription keys.

Update:
I think, the Problem lies in using key(this, 'getVal', 0); in the constructor.
If using @key, this works.

Typescript declarations missing

It seems that the typescript declarations have gone missing from ReSub.
I'm trying to use with ReactXP example and both the compiler and VSCode are complaining about missing definitions.
Version 1.0.1 gives this issue while version 1.0.0 doesn't.

Thanks for your work 👍

ComponentBase

short:
Is there any reason, that P in
https://github.com/Microsoft/ReSub/blob/467399dcbbd14fd264b9ef5a0a10f9d3acbf60c1/src/ComponentBase.ts#L44
extends React.Props instead of just {}, as the Component in the Typedefinition of React does?
See:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/cc6de05e3347a9ad3651df02f4b87603758e4398/types/react/index.d.ts#L397

Long form:
I want to user Material-UI together with ReSub.
Material-UI uses augmenting functions for the Properties.
E.g.

import { WithStyles, createStyles } from '@material-ui/core';

const styles = (theme: Theme) => createStyles({
  root: { /* ... */ },
  paper: { /* ... */ },
  button: { /* ... */ },
});

interface Props extends WithStyles<typeof styles> {
  foo: number;
  bar: boolean;
}

So the Component would extend ComponentBase<Props, State>, but this gives a
Compilation error, as in
#22.
Extending React.props does only work, if on would leave the WithStyles augmentation.
It adds the following:

interface Props  {
  foo: number;
  bar: boolean;
  classes: {
    foo: string,
    bar: string
  }
}

Here, one would loose this convenience method..

The Official Types definition for React.Component (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/cc6de05e3347a9ad3651df02f4b87603758e4398/types/react/index.d.ts#L397)
defines the following:

 interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }

Is there a particular Reason for having

ComponentBase<P extends React.Props<any>, ...

instead of just

ComponentBase<P extends {}, ...

in
https://github.com/Microsoft/ReSub/blob/467399dcbbd14fd264b9ef5a0a10f9d3acbf60c1/src/ComponentBase.ts#L44
?

Problem with _buildState + componentDidMount in same component

I just ported a small project to the latest versions of ReactXP and ReSub. The project dependencies are:

"@react-native-community/netinfo": "^3.2.1", "react": "16.12.0", "react-dom": "16.12.0", "react-native": "0.59.10", "react-native-windows": "0.59.0-rc.3", "reactxp": "2.0.0", "reactxp-netinfo": "^2.0.0", "reactxp-virtuallistview": "^2.1.0", "reactxp-webview": "^2.0.0", "resub": "^2.0.0-rc.2", "simplerestclients": "^0.2.12", "synctasks": "^0.3.3"

There seems to be some sort of problem when using _buildState and the React lifecycle function componentDidMount in the same component.

When componentDidMount is not present, _buildState runs right after constructor with the initialBuild parameter being true. Then comes a render, and a second _buildState (with initialBuild being false) triggered by a @autoSubscribeWithKey decorator in a data store.

When I add componentDidMount in the component, the second _buildState never gets triggered by the subscription key from the data store.

When I add componentDidMount with a setState call inside it, the _buildState gets called a second time (probably because of the setState call, not because of the subscription key), but its initialBuild parameter remains set to true.

There was no such problem using older versions of ReactXP and ReSub.

Is it not allowed anymore to use _buildState together with componentDidMount in the latest version of ReSub?

Proposal: Automatic Release

Currently there is no Autorelease, or pushing builds to npm automatically.
Last version on npm is currently 1.0.13 but the current version is 1.

There should be an automatic release.

In order to do this, the follwing must be accomplished:

  • automatic versioning (e.g. semantic-release)
  • automatic push to npmjs.com

Alternative, if manual triggering is desired:
Automatic release, if the version number is bumped manually.

Create subscribeToKey method

As a developer
I want to listen only to a particular key 
So that I will not get notified when `trigger()` was called.

Solution

Create subscribeWithKey() method that will be called only when someone triggers with the specified key.

Example

// a.js
this.store.subscribeWithKey(myCallbackFunc, 'myKey');

// store.js

startup() {
  this.trigger('myKey'); // this will announce a change on that key and myCallbackFunc will be called
  this.trigger() // this will announce a change but myCallbackFunc will not be called.
}

Minor bug in the example

I think I've found a bug in the example code. To make it work, in the second file I had to change line 6 to:

todos?: String[];

With a capital S.

I also had to add a line for the import of React.

Compatibility with Preact

Preact calls render with arguments. This is an iconic feature which allows destructuring props, state, and context in the parameter list. Currently ReSub breaks that pattern when Options.preventTryCatchInRender is falsy (which is the default).

Restoring compatibility should be as easy as forwarding the arguments list here.

Apparently I also have to manually set Options.development in order to get the TypeError thrown from destructuring undefined since my pipeline replaces the literal process.env.NODE_ENV at build time whereas you carefully test it for existence. I'll give that a shot tomorrow.

As a result my Components just rendered nothing and my coffee got cold.

Can we have this fixed?
If you want a PR it will have to wait till after the holidays.

ComponentBase: Prevent _buildState re-entry

If _buildState causes a store to trigger, that might call back into the component's _buildState before the first one has finished/returned. That can really mess up state updates since the setState will arrive in the wrong order*. Also, both _buildState will probably see the same this.state as the "previous state" since setState is not guaranteed to update this.state synchronously. Note: This is only a problem if _buildState depends on the previous state, or causes a change in the outside world (discouraged). Otherwise, both _buildState should return the same thing.

Is the solution to just use setState(updater) in #69? Or do we need to track this problem explicitly at runtime? Our current solution is to forbid store triggering from getters. That is manually enforced and can be error-prone. We could add runtime checks for that.

*
Call stack: FooStore.trigger -> MyComponent._buildStateWithAutoSubscriptions (first) -> MyComponent._buildState -> FooStore.getFoo -> FooStore.trigger -> MyComponent._buildStateWithAutoSubscriptions (second) -> MyComponent._buildState

The MyComponent._buildStateWithAutoSubscriptions (second) will finish and call setState, and then the stack will unwind to allow the MyComponent._buildStateWithAutoSubscriptions (first) to finish and call setState.

Compiler error using @key decorator

I have tested the simpler subscription examples successfully, and now I am trying autosubscriptions using the @key decorator, such as in your example:

@autoSubscribe
    getTodosForUser(@key username: string) {
        return this._todosByUser[username];
    }

Unfortunately, I get a compiler error:

Error: Module parse failed: Unexpected character '@' (1:20609)
You may need an appropriate loader to handle this file type.

It seems like Babel has a problem with the @key decorator being used within a function, and not with other decorators like @autosubscribe and @AutoSubscribeStore.

I've checked my babel.config.js and everything seems OK, using:

  const plugins = [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true}]
  ];

Did someone else come across this problem? Any recommendation on how to solve this?

React Native

Should this work out of the box with React Native?

Async Action Demo

Hi there,

Loving the simplicity of ReSub so far, but like with many other projects in this ecosystem I am at a loss until I can wrap my head around an async action example to say hit an API, get some data, and update store/state unidirectionally.

Any guidance or a demo would be greatly appreciated.

enabledPropertyName not working if target prop was ever falsy/empty

Known issue: ComponentBase does not properly track the enabledPropertyName when the target prop transitions from falsy to truthy. If ever falsy then the subscription is removed. If the target prop becomes truthy then we do not enable the subscription because we removed it.

I have not needed these Components perf tuning features of manual subscriptions, even in very large projects, so this issue is mainly to document the problem. Currently there are no plans to fix this.

If anyone needs proper enabledPropertyName behavior then leave a comment below.

React 17 / state of maintenance

Hey there, would it be a good idea to submit a PR that migrates ReSub to use React v17 or is ReSub EOL?
Reading through #203 it seems possible, most likely the enzyme tests would need to support React v17, I guess. But before putting work into that I want to ask if that would make sense / if a PR would get merged.
Thanks!

How to define Props

I'm gonna using resub in react-native, but I have no idea to define the properties of a component. Code as follows:

interface Props {
   navigation?;
}
interface State {
   value: string;
}
class TodoList extends ComponentBase<Props, State> {
    ...
    
}

But Typescript always show a red line under Props:
[ts] Type 'Props' has no properties in common with type 'Props<any>'.
Any idea to fix this?

React 18+ in StrictMode: _buildState is not triggered on state change

I have just made a simple project to learn RESUB with React + Typescript.

Store

@AutoSubscribeStore
class PermissionsStore extends StoreBase {

    private _permissions: string[] = [];

    updatePermissions(permissions: string) {
        this._permissions = this._permissions.concat(permissions);
        this.trigger();
    }


    @autoSubscribe
    getPermissions() {
        return this._permissions;
    }

}

export default new PermissionsStore();

Component

interface AppState {
    permissions: string[],
}

export default class PermissionsComponent extends ComponentBase<any, AppState> {



    getPermissions = async () => {
        PermissionsStore.updatePermissions("loading")
    }

    render() {
        return <React.Fragment>
            {this.state.permissions?.length ? <h4>Loading</h4> : <h4>Not Loading</h4>}
            <button onClick={this.getPermissions}>get Permissons</button>
        </React.Fragment>;
    }

    protected _buildState(props: {}, initialBuild: boolean, incomingState: Readonly<AppState> | undefined): Partial<AppState> | undefined {
        console.log('in build State')
        return {permissions: PermissionsStore.getPermissions()}
    }
}

Component not rerendering in React Native

Sorry for the multiple issues, but I'm working on a blog post for this with React Native and I want to make sure I get this right.

So, I've got the example working with React Native, as in it's loading and rendering the todos if I populate the in the store.

The problem I'm having now is that when I add a todo, the component utilizing _buildState is not rerendering.

When I inspect the state, I see that the todos are being added (I log the array on initial render, and it gets populated as I add more) but the component is not rerendering.

App.tsx

import { ComponentBase } from 'resub';
import * as React from 'react'
import { View, Text , TouchableHighlight} from 'react-native'

import TodosStore = require('./TodosStore');

interface TodoListState {
    todos?: string[];
}

class TodoList extends ComponentBase<{}, TodoListState> {
    protected _buildState(props: {}, initialBuild: boolean): TodoListState {
        return {
            todos: TodosStore.getTodos()
        }
    }

    addTodo = () => {
      TodosStore.addTodo('yo')
    }

    render() {
      console.log('state:', this.state)
        return (
            <View style={{marginTop: 30}}>
                { this.state.todos.map((x, i) => {
                  return <Text key={i}>asfd</Text>
                } ) }
                <TouchableHighlight onPress={this.addTodo}>
                  <Text>add</Text>
                </TouchableHighlight>
            </View>
        );
    }
}

export = TodoList

TodoStore.tsx


import { StoreBase, AutoSubscribeStore, autoSubscribe } from 'resub';

@AutoSubscribeStore
class TodosStore extends StoreBase {
    private _todos: String[] = ['yo', 'one', 'two'];

    addTodo(todo: String) {
        this._todos.push(todo);
        this.trigger();
        console.log('t_todos:', this._todos)
    }

    @autoSubscribe
    getTodos() {
        return this._todos;
    }
}

export = new TodosStore();

Efficiency of Deep Equality Checks in Prop Updates?

I notice that by default ReSub does a deep equality check when properties are updated, rather than preferring immutability and shallow checks. Wouldn't it be more efficient to do the deep equality check in the store, and push immutable structures to PureComponents (i.e. deep check once at source, rather than in each consumer?).

[discussion] ReSub Hook API

I was wondering whether there are any plans to officialy support React Hooks.
I only found a discussion regarding lifecycle changes (#78) but could not find a mention of the now introduced Hooks.

It is definitely possible to use them, I have made an example repository as a proof of concept here:
https://github.com/Hizoul/ReSubHooksTest

Ideally I imagine the API to work the same as _buildState so like this:

  const state = useResub(() => {
    value: FormStore.getValue("My key")
  })

(Where FormStore is an autosubscribe score that has an @key decorator on its first argument)
Unfortunately due to my lack of understanding of decorators and how automatic subscriptions work I was only able to come up with a solution that requires the key to be specified explicitly.

  const state = useResub(FormStore, "My key", {name: "getValue", arguments: ["My key"]})

The hook implementation with annotations for parts that need to be improved can be found here:
https://github.com/Hizoul/ReSubHooksTest/blob/master/src/resubHook.js

ReSub ComponentBase "swallowing" context updates

It seems that ReSub swallows the context update on some specific use-case.
I'm having trouble using ReSub and React-Router, but I was able to reproduce it with just React.

The repro is the following:

import * as React from "react";
import { ComponentBase } from "resub";
import * as PropTypes from "prop-types";

class NotWorking extends ComponentBase<{}, {}> {
  render() {
    return <div>{this.props.children}</div>;
  }
}

class Working extends React.Component {
  render() {
    return <div>{this.props.children}</div>;
  }
}

class RenderFromContext extends React.Component {
  static contextTypes = { text: PropTypes.string };
  render() {
    return this.context.text;
  }
}

class MakeContext extends React.Component<
  {},
  { text: string }
> {
  state = {
    text: "",
  };
  static childContextTypes = { text: PropTypes.string };

  getChildContext() {
    return { text: this.state.text };
  }

  componentDidMount() {
    this.setState({ text: "Works" });
  }

  render() {
    return <div>{this.props.children}</div>;
  }
}

class App extends React.Component {
  render() {
    return (
      <MakeContext>
        Working:
        <Working>
          <RenderFromContext />
        </Working>
        Not working:
        <NotWorking>
          <RenderFromContext />
        </NotWorking>
      </MakeContext>
    );
  }
}

export default App;

If you'd like to run it on your browser I've made a create-react-app with this code and it is hosted here: https://github.com/degroote22/resubctxbug2

Just clone then npm install then npm start and you should see the following on your browser:
Working: Works Not working:

Thanks for your time 👍

ComponentBase: Consider setState(updater) instead of setState(object)

I don't have evidence that this is hurting us, but the React docs are quite clear that if building state depends on previous state then we should use the updater version instead of the object version. At the ReSub layer, we don't know if the derived class will need this should perhaps we should always opt-in to be safe?

Also, we could forbid using this.state for the previous state since the updater is given prevState (and props). Although that would involve changing the _buildState signature to forward it like the props.

One possible issue: how does the props argument correspond with nextProps in componentWillReceiveProps? Which should we use in there?

https://reactjs.org/docs/react-component.html#setstate

[discussion] Update ReSub to support the latest React

The latest version of React (starting from 16.3.*) includes public API changes which will affect on the ReSub in the future.

The following methods will be removed:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps - has new pseudo analog, static method getDerivedStateFromProps

In my opinion, the most significant change related to componentWillMount, because ReSub uses it to set the initial state., correct me if I'm wrong.

In order to use the latest version of React with ReSub would be great to follow new API changes.

Is there any plan/ideas on how to "smoothly" update ReSub? Maybe make sense to make all changes and publish major (for instance 2.0) version in order to do not break applications which use the current version of ReSub.

What do you think @deregtd @ms-markda @berickson1?

ReSub v2 setState not working?

Hey you all - I'm testing out the 2.0.0-rc.1 and vs 1.2.2 I have a component that fails to setState correctly.

  private _onNextPage = () => {
    console.log(
      'TIPI::_onNextPage - was on page ' + this.state.pageIndex + ' (max is ' + this.MAX_PAGE + ')'
    );
   let nextIndex = this.state.pageIndex + 1;
    this.setState({ pageIndex: nextIndex >= this.MAX_PAGE ? this.MAX_PAGE : nextIndex });
    console.log('TIPI::_onNextPage - will be on page ' + this.state.pageIndex);
  };

It's a simple pager, and I get 'will be on page 0' (the initial state) every click with 2.0.0-rc.1 but it increments nicely with 1.2.2.

That's a vague report in and of itself but it is a clear regression with this code between those two versions.

I'm trying to get a repro together but in the meantime I also see this on an npm test execution

mike@isabela:~/work/react-random/resub (master) % npm test

> [email protected] test /home/mike/work/react-random/resub
> run-s clean:* karma:single-run


> [email protected] clean:dist /home/mike/work/react-random/resub
> rimraf dist*


> [email protected] karma:single-run /home/mike/work/react-random/resub
> karma start --singleRun

Starting type checking service...
Using 1 worker with 2048MB memory limit
Type checking in progress...
ℹ 「wdm」: Compiled successfully.
ℹ 「wdm」: Compiling...
ERROR in /home/mike/work/react-random/resub/test/AutoSubscribe.spec.tsx(128,7):
TS2417: Class static side 'typeof OverriddenComponent' incorrectly extends base class static side 'typeof SimpleComponent'.
  Types of property 'getDerivedStateFromProps' are incompatible.
    Type 'GetDerivedStateFromProps<SimpleProps, SimpleState>' is not assignable to type 'GetDerivedStateFromProps<unknown, unknown>'.
      Types of parameters 'nextProps' and 'nextProps' are incompatible.
        Property 'ids' is missing in type 'Readonly<unknown>' but required in type 'Readonly<SimpleProps>'.
Version: typescript 3.5.3
Time: 2315ms
ℹ 「wdm」: Compiled successfully.

Types Error in AutoSubscribe.spec.tsx

ERROR in /test/AutoSubscribe.spec.tsx(128,7):
TS2417: Class static side 'typeof OverriddenComponent' incorrectly extends base class static side 'typeof SimpleComponent'.
  Types of property 'getDerivedStateFromProps' are incompatible.
    Type 'GetDerivedStateFromProps<SimpleProps, SimpleState>' is not assignable to type 'GetDerivedStateFromProps<unknown, unknown>'.
      Types of parameters 'nextProps' and 'nextProps' are incompatible.
        Property 'ids' is missing in type 'Readonly<unknown>' but required in type 'Readonly<SimpleProps>'.

_buildState called on unmounted component

I have the following issue:

I'm using the navigator in conjonction with a side menu and a Resub store.
I have a root component that is the orchestrator for the menu and the navigator component.
I have several screens.
The root component and the various screens components all extends ComponentBase, thus implements _buildState().

When the navigator is first mounted, I reset the stack to a welcome screen.
Basically, here is the timeline of events:
*App Start
buidState() is called in the following order for the following components:
Root -> Menu -> Welcome

Now let's say that the user chooses a menu item that directs to a new screen not extending ComponentBase. I do not push the new route on the navigator stack but replace the current top route.

I have the following events that comes in:

NewScreen::ComponentDidMount
Welcome::ComponentWillUnmount

So far so good. In this new screen I click a button that calls the resub store to set a value.

  • This has the effect of calling _buildState on Root/Menu and Welcome components

The problem here is that the Welcome component has been unmounted. This triggers the warning : 'Can't perform a React state update on an unmounted component...'

Am i doing something wrong or is this a bug?

Consider allowing Generic Trigger Keys

Something like

const enum TriggerAreas {
    Foo,
    Bar,
    Baz,
    Cat
};

type TriggerKeyType = string|MyObject|TriggerAreas;
export class MyStore extends ReSub.StoreBase<TriggerKeyType>{
...
    // Trigger for enum, string and a complex object are fully typesafe
    this.trigger([TriggerAreas.Foo, 'fooitemKey', fooObject]);
...
}

This would allow passing anything a developer wants through keys and allows more documentation context to be gathered from the specified trigger keys.

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.