Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: React Bloc: BlocProvider & BlocConsumer #29

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/react-bloc/lib/react-bloc.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './src/bloc-builder';
export * from './src/bloc-builder';
export * from './src/bloc-provider';
export * from './src/bloc-consumer';
59 changes: 44 additions & 15 deletions packages/react-bloc/lib/src/bloc-builder.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import { Bloc } from '@felangel/bloc'
import * as React from 'react'
import { Subscription } from 'rxjs'
import { BlocProvider } from '../react-bloc'

export type BlocBuilderCondition<S> = (previous: S, current: S) => boolean
export type BlocElementBuilder<S> = (state: S) => JSX.Element

export type BlocBuilderProps<B extends Bloc<any, S>, S> = {
bloc: B
export interface BlocBuilderPropsBase<S> {
builder: BlocElementBuilder<S>
condition?: BlocBuilderCondition<S>
}

type BlocBuilderPropsInternal<B extends Bloc<any, S>, S> = BlocBuilderPropsBase<S> & {
bloc: B
}

export type BlocStateType<S> = {
blocState: S
}

/**
* `BlocBuilder` handles building a component in response to new `states`.
*
* @export
* @class BlocBuilder
* @extends {React.Component<BlocBuilderProps<B, S>, BlocStateType<S>>}
* @template B
* @template S
*/
export class BlocBuilder<B extends Bloc<any, S>, S> extends React.Component<
BlocBuilderProps<B, S>,
class BlocBuilderInternal<B extends Bloc<any, S>, S> extends React.Component<
BlocBuilderPropsInternal<B, S>,
BlocStateType<S>
> {
private bloc: B
Expand All @@ -34,7 +29,7 @@ export class BlocBuilder<B extends Bloc<any, S>, S> extends React.Component<
private condition: BlocBuilderCondition<S> | null
private builder: BlocElementBuilder<S>

constructor(props: BlocBuilderProps<B, S>) {
constructor(props: BlocBuilderPropsInternal<B, S>) {
super(props)
this.bloc = props.bloc
this.builder = props.builder
Expand Down Expand Up @@ -62,7 +57,7 @@ export class BlocBuilder<B extends Bloc<any, S>, S> extends React.Component<
this.subscription.unsubscribe()
}

componentDidUpdate(prevProps: BlocBuilderProps<B, S>): void {
componentDidUpdate(prevProps: BlocBuilderPropsInternal<B, S>):void {
if (prevProps.bloc !== this.props.bloc) {
this.unsubscribe()
this.bloc = this.props.bloc
Expand All @@ -84,3 +79,37 @@ export class BlocBuilder<B extends Bloc<any, S>, S> extends React.Component<
return this.builder(this.state.blocState)
}
}

export type BlocBuilderProps<B extends Bloc<any, S>, S> = BlocBuilderPropsBase<S> & {
type?: string
bloc?: B
}

/**
* `BlocBuilder` handles building a component in response to new `states`.
*
* @export
* @class BlocBuilder
* @extends {React.Component<BlocBuilderProps<B, S>, BlocStateType<S>>}
* @template B
* @template S
*/
export function BlocBuilder<B extends Bloc<any, S>, S>(
props: BlocBuilderProps<B, S>
): JSX.Element {
if (props.bloc) {
return (
<BlocBuilderInternal bloc={props.bloc} builder={props.builder} condition={props.condition} />
)
} else if (props.type) {
const context = BlocProvider.context<B>(props.type)
Copy link
Collaborator

@erickjtorres erickjtorres Sep 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed we are passing in the type manually. Can we get the name of the bloc using something like props.bloc.constructor.name ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

props.bloc.constructor.name will be minified with any good builder, eg Webpack.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, my first iteration used constructor.name, but it doesnt work in following cases:

  1. Minification
  2. Old browsers, <IE9
  3. Classes defined using Prototypical inheritance es5

The new react library, Recoil also uses a unique key method to track a set of state nodes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to note, there is one more possibility, see this implementation https://github.com/cartant/ts-action/blob/master/packages/ts-action/source/action.ts

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we should consider trying to implement something that removes the need to pass in the type. I think someone also mentions using metadata in https://stackoverflow.com/questions/13613524/get-an-objects-class-name-at-runtime.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could merge the PR now and have this as a separate issue to look into. @felangel what do you think?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delayed response! Going to take a closer look at this tomorrow and let y'all know what my thoughts are 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erickjtorres Even if we were to implement an instance property called "type" as suggested by Lonli or use something like constructor.name, they will be available only in the instance context.
We need to be able to access the key in a static context

const context = BlocProvider.context<B>(props.type) 
// No way to access instance properties of B without creating an object first

return (
<context.Consumer>
{bloc => (
<BlocBuilderInternal bloc={bloc} builder={props.builder} condition={props.condition} />
)}
</context.Consumer>
)
}
throw Error('BlocBuilder: Expected either "bloc" or "type" property to be not null.')
}
15 changes: 15 additions & 0 deletions packages/react-bloc/lib/src/bloc-consumer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'
import { Bloc } from '@felangel/bloc'
import { BlocProvider } from '../react-bloc'

export type BlocConsumerBuilder<B> = (bloc: B) => JSX.Element

export type BlocConsumerProps<B extends Bloc<any, any>> = {
type: string
consumer: BlocConsumerBuilder<B>
}

export function BlocConsumer<B extends Bloc<any, any>>(props: BlocConsumerProps<B>): JSX.Element {
const context = BlocProvider.context<B>(props.type)
return <context.Consumer>{props.consumer}</context.Consumer>
}
96 changes: 96 additions & 0 deletions packages/react-bloc/lib/src/bloc-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Bloc } from '@felangel/bloc'
import * as React from 'react'

export type BlocCreator<B extends Bloc<any, any>> = () => B

export interface BlocProviderProps<B extends Bloc<any, any>> {
bloc?: B
create?: BlocCreator<B>
type: string
}

export interface BlocProviderState<B extends Bloc<any, any>> {
bloc: B | null
blocContext: React.Context<B | null>
}

export class BlocProvider<B extends Bloc<any, any>> extends React.Component<
React.PropsWithChildren<BlocProviderProps<B>>,
BlocProviderState<B>
> {
constructor(props: BlocProviderProps<B>) {
super(props)

this.state = this.getStateFromProps()
}

private getStateFromProps(): BlocProviderState<B> {
let bloc: B | null = null

if (this.props.bloc) {
bloc = this.props.bloc
} else if (this.props.create) {
bloc = this.props.create()
} else {
throw Error('BlocProvider: Expected either "bloc" or "create" property to be not null.')
}

let blocContext = BlocProvider.contextTypeMap[this.props.type] as React.Context<B | null>
if (!blocContext) {
blocContext = React.createContext<B | null>(bloc)
blocContext.displayName = this.props.type
BlocProvider.contextTypeMap[this.props.type] = blocContext as React.Context<unknown>
}
return { bloc, blocContext }
}

private subscribe(): void {
const state = this.getStateFromProps()
this.setState(state)
}

private unsubscribe(): void {
if (!this.props.bloc && this.state.bloc) {
// close only if BlocProvider was the creator
this.state.bloc.close()
}
}

componentDidUpdate(prevProps: BlocProviderProps<B>) {
if (
prevProps.bloc !== this.props.bloc ||
prevProps.type !== this.props.type ||
prevProps.create !== this.props.create
) {
this.unsubscribe()
this.subscribe()
}
}

componentWillUnmount() {
this.unsubscribe()
}

render() {
return (
<this.state.blocContext.Provider value={this.state.bloc}>
{this.props.children}
</this.state.blocContext.Provider>
)
}

static clear(): void {
BlocProvider.contextTypeMap = {}
}

static context<B>(type: string): React.Context<B> {
const context = BlocProvider.contextTypeMap[type]
if (context) {
return context as React.Context<B>
}

throw Error('BlocProvider: BlocContext of type ' + type + ' not found!')
}

private static contextTypeMap = {} as Record<string, React.Context<unknown> | null>
}
2 changes: 1 addition & 1 deletion packages/react-bloc/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 24 additions & 2 deletions packages/react-bloc/test/bloc-builder.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BlocBuilder } from '../lib/react-bloc'
import { BlocBuilder, BlocProvider } from '../lib/react-bloc'
import * as React from 'react'
import { Bloc } from '@felangel/bloc'
import { mount } from 'enzyme'
Expand Down Expand Up @@ -63,7 +63,10 @@ class CounterApp extends React.Component<CounterBlocProps<CounterBloc>, any> {
}
}

describe('BlocProvider', () => {
describe('BlocBuilder', () => {
beforeEach(() => {
BlocProvider.clear()
})
it('renders the component properly', () => {
let bloc: CounterBloc = new CounterBloc()
const wrapper = mount(<CounterApp title={'dsad'} bloc={bloc} />)
Expand Down Expand Up @@ -173,4 +176,23 @@ describe('BlocProvider', () => {
done()
})
})

it('throws input error when blocbuilder not provided type or bloc', () => {
const t = () => {
const wrapper = mount(
<div>
<BlocBuilder<CounterBloc, number> builder={(s: number) => <div>{s}</div>} />
</div>
)
}
try {
t()
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(error).toHaveProperty(
'message',
'BlocBuilder: Expected either "bloc" or "type" property to be not null.'
)
}
})
})
Loading