Skip to content

Commit

Permalink
fix: 馃悰 bean object is not correctly gc after component unmount
Browse files Browse the repository at this point in the history
  • Loading branch information
foreleven committed Oct 15, 2019
1 parent 8fd808a commit 81847a1
Show file tree
Hide file tree
Showing 18 changed files with 2,992 additions and 3,099 deletions.
85 changes: 42 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![codechecks.io](https://raw.githubusercontent.com/codechecks/docs/master/images/badges/badge-default.svg?sanitize=true)](https://codechecks.io)

> A light but scalable state management library with react hooks, inspired by [unstated-next](https://github.com/jamiebuilds/unstated-next).
> It allows you to manage the state data of multiple views together. Make cross-component data transfer simple.
> A light but scalable state management library with react hooks.
>
> It is the **ViewModel** in the MVVM.
## TOC <!-- omit in TOC -->

Expand All @@ -26,7 +27,7 @@
- [Decorators](#decorators)
- [`StatedBean`](#statedbean)
- [`Stated`](#stated)
- [`PostProvided`](#postprovided)
- [`AfterProvided`](#afterprovided)
- [`Effect`](#effect)
- [use Hooks](#use-hooks)
- [`useBean`](#usebean)
Expand Down Expand Up @@ -60,13 +61,43 @@ npm i stated-bean

## Usage

### Plain object StatedBean

```ts
import { useBean } from 'stated-bean';

const CounterModel = {
count: 0,
decrement() {
this.count--;
},
increment() {
this.count++;
},
};

function CounterDisplay() {
const counter = useBean(() => CounterModel);

return (
<div>
<button onClick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
);
}
```

### Class StatedBean

```ts
import { StatedBean, Stated, useBean } from 'stated-bean';
import { StatedBean, Stated useBean } from 'stated-bean';

@StatedBean()
export class Counter {
class CounterModel {
@Stated()
count: number = 0;
count = 0;

increment() {
this.count++;
Expand All @@ -78,14 +109,10 @@ export class Counter {
}

function CounterDisplay() {
const counter = useBean(Counter);
const counter = useBean(CounterModel);

return (
<div>
<button onClick={counter.decrement}>-</button>
<span>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
// ...
);
}
```
Expand All @@ -106,11 +133,11 @@ _Signature_: `@Stated(): PropertyDecorator`

Indicates that an annotated property is `Stated`. Its reassignment will be observed and notified to the container.

#### `PostProvided`
#### `AfterProvided`

_Signature_: `@PostProvided(): MethodDecorator`
_Signature_: `@AfterProvided(): MethodDecorator`

The `PostProvided` decorator is used on a method that needs to be executed after the `StatedBean` be instanced to perform any initialization.
The `AfterProvided` decorator is used on a method that needs to be executed after the `StatedBean` be instanced to perform any initialization.

#### `Effect`

Expand All @@ -132,34 +159,6 @@ _Signature_: `useInject<T>(type: ClassType<T>, option: UseStatedBeanOption<T> =

The `useInject` will get the instance of the stated bean from the `StatedBeanContainer` in the context and listen for its data changes to trigger the re-rendering of the current component.

##### Get the instance from the container in the `React Context`

```tsx
function SampleComponent() {
const model = useInject(UserModel);
// ...
}

function App() {
return (
<StatedBeanProvider types={[UserModel]}>
<SampleComponent />
</StatedBeanProvider>
);
}
```

##### Create the temporary instance for current `Component`

```tsx
function SampleComponent() {
const model = useBean(() => new UserModel());

// pass the model to its children
return <ChildComponent model={model} />;
}
```

##### `UseStatedBeanOption`

```ts
Expand Down
2 changes: 1 addition & 1 deletion example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class InjectionFactory implements IBeanFactory {
let provide;
let provider;
if (beanDefinition.isFactoryBean) {
provide = beanDefinition.getFactoryBeanType();
provide = beanDefinition.factoryBeanType;
provider = { provide: provide, useFactory: beanDefinition.getFactory()! };
} else {
provide = beanDefinition.beanType;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stated-bean",
"version": "0.8.1",
"version": "0.8.2",
"description": "A light but scalable state management library with react hooks",
"repository": "git@github.com:mjolnirjs/stated-bean.git",
"license": "MIT",
Expand Down Expand Up @@ -29,8 +29,8 @@
},
"peerDependencies": {
"react": ">=16.8.0",
"tslib": ">=1.0.0",
"rxjs": ">=6.0.0"
"rxjs": ">=6.0.0",
"tslib": ">=1.0.0"
},
"devDependencies": {
"@1stg/babel-preset": "^0.7.0",
Expand Down
86 changes: 48 additions & 38 deletions src/core/BeanDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,29 @@ import { isFunction, getPropertiesWithoutFunction } from '../utils';
export const UN_NAMED_BEAN = Symbol('UN_NAMED_BEAN');

export class BeanDefinition<T> {
private _target: T | undefined;
private readonly _beanName: symbol;

constructor(private readonly _beanProvider: BeanProvider<T>) {}
private readonly _beanMeta: StatedBeanMeta | undefined;
private _factoryBeanType: ClassType<T> | undefined = undefined;
private _factoryBeanMeta: StatedBeanMeta | undefined = undefined;

getFactory() {
return this._beanProvider.factory;
}
constructor(private readonly _beanProvider: BeanProvider<T>) {
this._beanName = Symbol(`${Date.now()}`);

if (!this.isFactoryBean) {
const beanMeta = getMetadataStorage().getBeanMeta(this.beanType);

getFactoryBeanType() {
return this.target !== undefined
? (((this.target as unknown) as object).constructor as ClassType<T>)
: this.beanType;
if (beanMeta === undefined) {
throw new Error('bean metadata is undefined.');
}
this._beanMeta = beanMeta;
}
}

setTarget(bean: T) {
this._target = bean;
extractFactoryBeanInfo(bean: T) {
this._factoryBeanType = ((bean as unknown) as object)
.constructor as ClassType<T>;

// plain object method binding
if (this.isPlainObject) {
Object.keys(bean).forEach((key: keyof T & string) => {
if (typeof bean[key] === 'function') {
Expand All @@ -37,10 +42,29 @@ export class BeanDefinition<T> {
}
});
}

if (this.isPlainObject) {
this._factoryBeanMeta = {
target: this._factoryBeanType,
name: this._beanProvider.name,
statedFields: (getPropertiesWithoutFunction(bean) || []).map(
property => {
return {
name: property,
target: this._factoryBeanType,
} as StatedFieldMeta;
},
),
} as StatedFieldMeta;
}
}

protected get target() {
return this._target;
getFactory() {
return this._beanProvider.factory;
}

get factoryBeanType() {
return this._factoryBeanType;
}

get beanType() {
Expand All @@ -55,36 +79,22 @@ export class BeanDefinition<T> {
}

get beanName(): string | symbol {
const beanName = this._beanProvider.name || this.beanMeta.name;
const beanName =
this._beanProvider.name ||
(this.beanMeta ? this.beanMeta.name : undefined);
if (this.isSingleton) {
return beanName || this.beanType.name;
} else {
return beanName || UN_NAMED_BEAN;
return beanName || this._beanName;
}
}

get beanMeta(): StatedBeanMeta {
const storage = getMetadataStorage();
const beanType = this.isFactoryBean
? this.getFactoryBeanType()
: this.beanType;
const beanMeta = storage.getBeanMeta(beanType);

if (beanMeta === undefined) {
return {
target: beanType,
name: this._beanProvider.name,
statedFields: (getPropertiesWithoutFunction(this.target) || []).map(
property => {
return {
name: property,
target: beanType,
} as StatedFieldMeta;
},
),
};
if (this.isFactoryBean) {
return this._factoryBeanMeta!;
} else {
return this._beanMeta!;
}
return beanMeta;
}

get isNamedBean() {
Expand All @@ -96,8 +106,8 @@ export class BeanDefinition<T> {
}

get isPlainObject() {
if (this.isFactoryBean) {
return this.getFactoryBeanType().name === 'Object';
if (this.isFactoryBean && this.factoryBeanType) {
return this.factoryBeanType.name === 'Object';
} else {
return this.beanType.name === 'Object';
}
Expand Down
7 changes: 5 additions & 2 deletions src/core/BeanObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PropsFieldMeta,
StateAction,
StatedFieldMeta,
StatedBeanMeta,
} from '../types';
import { getBeanWrapper } from '../utils';

Expand All @@ -25,13 +26,15 @@ export class BeanObserver<T = unknown> {
props$: Subject<unknown> = new Subject();

private readonly _proxyBean: T;
private readonly _beanMeta: StatedBeanMeta;
private readonly _stateSubscription: Subscription | undefined;

constructor(
private readonly _bean: T,
private readonly _container: StatedBeanContainer,
private readonly _beanDefinition: BeanDefinition<T>,
) {
this._beanMeta = this.beanDefinition.beanMeta;
this._proxyBean = (new Proxy(
(this.origin as unknown) as object,
{},
Expand All @@ -58,7 +61,7 @@ export class BeanObserver<T = unknown> {
}

get beanMeta() {
return this.beanDefinition.beanMeta;
return this._beanMeta;
}

destroy() {
Expand Down Expand Up @@ -119,7 +122,7 @@ export class BeanObserver<T = unknown> {
let wrapper = getBeanWrapper(bean);

if (wrapper === undefined) {
wrapper = new BeanWrapper(this);
wrapper = new BeanWrapper(this._container, this.beanDefinition.beanName);
Object.defineProperty(bean, StatedBeanWrapper, {
value: wrapper,
});
Expand Down
15 changes: 9 additions & 6 deletions src/core/BeanWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { StateAction } from '../types';

import { BeanObserver } from './BeanObserver';
import { CountableSubject } from './CountableSubject';
import { StatedBeanContainer } from './StatedBeanContainer';

export class BeanWrapper<T> {
state$: CountableSubject<StateAction<T>> = new CountableSubject();

constructor(private readonly _beanObserver: BeanObserver<T>) {
constructor(
private readonly _container: StatedBeanContainer,
private readonly _beanName: string | symbol,
) {
// this.state$.subscribeCount(count => {
// console.log('bean wrapper sub count', count);
// if (count === 0) {
Expand All @@ -16,11 +19,11 @@ export class BeanWrapper<T> {
}

get beanObserver() {
return this._beanObserver;
return this._container.getNamedObserver<T>(this._beanName);
}

get beanDefinition() {
return this.beanObserver.beanDefinition;
return this.beanObserver!.beanDefinition;
}

get beanMeta() {
Expand All @@ -32,9 +35,9 @@ export class BeanWrapper<T> {
f => f.name === field,
);
if (fieldMeta !== undefined) {
this.beanObserver.publishStateAction(
this.beanObserver!.publishStateAction(
fieldMeta,
this.beanObserver.proxy[field],
this.beanObserver!.proxy[field],
);
}
}
Expand Down
Loading

0 comments on commit 81847a1

Please sign in to comment.