-
Notifications
You must be signed in to change notification settings - Fork 5
Connect as a functional component #150
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
base: master
Are you sure you want to change the base?
Changes from 2 commits
b8daace
2d3e296
c3928d4
9ac35af
adfa4f7
80ba338
c762e81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| import * as React from "react"; | ||
| import logger from "./logger"; | ||
| import { Comp, Connect, Dispatch, Node, Store, Watch } from "./types"; | ||
| import { Comp, Connect, Node, Store } from "./types"; | ||
| import { joinPath, splitPath } from "./utils"; | ||
| import { subscribe, unsubscribe } from "./watch"; | ||
| import { | ||
| subscribe as subscribeInner, | ||
| unsubscribe as unsubscribeInner, | ||
| } from "./watch"; | ||
|
|
||
| export const ProdoContext = React.createContext<Store<any, any>>(null as any); | ||
|
|
||
|
|
@@ -20,6 +23,15 @@ export const createUniverseWatcher = (universePath: string) => { | |
| return readProxy([universePath]); | ||
| }; | ||
|
|
||
| const state = createUniverseWatcher("state"); | ||
|
|
||
| const useForceUpdate = () => { | ||
| const [, setCounter] = React.useState(0); | ||
| return React.useCallback(() => { | ||
| setCounter(tick => tick + 1); | ||
| }, []); | ||
| }; | ||
|
|
||
| const getValue = (path: string[], obj: any): any => | ||
| path.reduce((x: any, y: any) => x && x[y], obj); | ||
|
|
||
|
|
@@ -75,203 +87,148 @@ export type Func<V, P = {}> = (viewCtx: V) => React.ComponentType<P>; | |
|
|
||
| export const connect: Connect<any> = <P extends {}>( | ||
| func: Func<any, P>, | ||
| name: string = "(anonymous)", | ||
| ): React.ComponentType<P> => | ||
| class ConnectComponent<P> extends React.Component<P, any> { | ||
| public static contextType = ProdoContext; | ||
|
|
||
| public state: any; | ||
| private watched: { [key: string]: any }; | ||
| private prevWatched: { [key: string]: any }; | ||
| private pathNodes: { [key: string]: Node }; | ||
| private compId: number; | ||
| private comp: Comp; | ||
| private name: string; | ||
| private eventIdCnt: number; | ||
| private subscribe: ( | ||
| path: string[], | ||
| unsubscribe?: (comp: Comp) => void, | ||
| ) => void; | ||
| private unsubscribe: (path: string[]) => void; | ||
| private firstTime: boolean; | ||
| private status: { unmounted: boolean }; | ||
| private store: Store<any, any>; | ||
| private _renderFunc: any; | ||
| private _watch: Watch; | ||
| private _dispatch: Dispatch; | ||
| private _state: any; | ||
|
|
||
| private _viewCtx: any; | ||
|
|
||
| constructor(props: P) { | ||
| super(props); | ||
|
|
||
| this.state = {}; | ||
| this.watched = {}; | ||
| this.prevWatched = {}; | ||
| this.pathNodes = {}; | ||
| this.firstTime = true; | ||
| this.compId = _compIdCnt++; | ||
| this.name = name + "." + this.compId; | ||
| this.store = this.context; | ||
|
|
||
| const setState = this.setState.bind(this); | ||
| this.status = { unmounted: false }; | ||
|
|
||
| this.comp = { | ||
| name: this.name, | ||
| compId: this.compId, | ||
| }; | ||
|
|
||
| this.eventIdCnt = 0; | ||
|
|
||
| this.subscribe = (path: string[], unsubscribe?: (comp: Comp) => void) => { | ||
| const pathKey = joinPath(path); | ||
|
|
||
| const node: Node = this.pathNodes[pathKey] || { | ||
| pathKey, | ||
| status: this.status, | ||
| setState, | ||
| unsubscribe, | ||
| ...this.comp, | ||
| }; | ||
|
|
||
| this.pathNodes[pathKey] = node; | ||
| subscribe(this.store, path, node); | ||
| }; | ||
|
|
||
| this.unsubscribe = (path: string[]) => { | ||
| const pathKey = joinPath(path); | ||
| const node = this.pathNodes[pathKey]; | ||
| if (node != null) { | ||
| unsubscribe(this.store, path, node); | ||
| delete this.pathNodes[pathKey]; | ||
| } | ||
| }; | ||
|
|
||
| this._watch = x => x; | ||
|
|
||
| this._dispatch = func => (...args) => | ||
| this.store.exec( | ||
| { | ||
| id: `${this.comp.name}/event.${this.eventIdCnt++}`, | ||
| parentId: this.comp.name, | ||
| }, | ||
| func, | ||
| ...args, | ||
| ); | ||
|
|
||
| this._renderFunc = (props: any): any => { | ||
| return (func as ((args: any) => (props: any) => any))(this._viewCtx)( | ||
| props, | ||
| ); | ||
| }; | ||
|
|
||
| logger.info(`[constructing] ${this.name}`); | ||
| } | ||
|
|
||
| public componentDidMount() { | ||
| logger.info(`[did mount] ${this.name}`); | ||
|
|
||
| Object.keys(this.watched).forEach(pathKey => { | ||
| logger.info(`[start watching] ${this.name}: < ${pathKey} >`); | ||
| this.subscribe(splitPath(pathKey)); | ||
| baseName: string = "(anonymous)", | ||
| ): React.ComponentType<P> => (props: P) => { | ||
| const compId = React.useRef(_compIdCnt++); | ||
| const name = baseName + "." + compId.current; | ||
|
|
||
| logger.info(`[rendering] ${name}`); | ||
| // First render only | ||
| React.useMemo(() => logger.info(`[constructing] ${name}`), []); | ||
|
|
||
| const store = React.useContext(ProdoContext); | ||
| const forceUpdate = useForceUpdate(); | ||
|
|
||
| // Subscribing to part of the state | ||
| const status = React.useRef({ unmounted: true }); | ||
| React.useEffect(() => { | ||
| logger.info(`[did mount] ${name}`); | ||
| status.current.unmounted = false; | ||
| logger.debug("store", store); | ||
| }, []); | ||
|
|
||
| const pathNodes = React.useRef({}); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this by typed? |
||
| const subscribe = (path: string[], unsubscribe?: (comp: Comp) => void) => { | ||
| const pathKey = joinPath(path); | ||
|
|
||
| const node: Node = | ||
| pathNodes.current[pathKey] || | ||
| (pathNodes.current[pathKey] = { | ||
| pathKey, | ||
| status: status.current, | ||
| forceUpdate, | ||
| unsubscribe, | ||
| name, | ||
| compId: compId.current, | ||
| }); | ||
|
|
||
| logger.debug("store", this.store); | ||
|
|
||
| this.prevWatched = { ...this.watched }; | ||
| this.firstTime = true; | ||
| this.setState(this.watched); | ||
| this.watched = {}; | ||
| } | ||
|
|
||
| public shouldComponentUpdate(nextProps: P, nextState: any) { | ||
| const test = | ||
| !shallowEqual(this.props, nextProps) || | ||
| (!this.firstTime && !shallowEqual(this.state, nextState)); | ||
|
|
||
| logger.info(`[should update] ${this.name}`, test); | ||
| this.firstTime = false; | ||
|
|
||
| return test; | ||
| subscribeInner(store, path, node); | ||
| }; | ||
| const unsubscribe = (path: string[]) => { | ||
| const pathKey = joinPath(path); | ||
| const node = pathNodes.current[pathKey]; | ||
| if (node != null) { | ||
| unsubscribeInner(store, path, node); | ||
| delete pathNodes.current[pathKey]; | ||
| } | ||
| }; | ||
|
|
||
| public componentDidUpdate() { | ||
| logger.info(`[did update] ${this.name}`); | ||
|
|
||
| Object.keys(this.watched).forEach(pathKey => { | ||
| const keyExisted = this.prevWatched.hasOwnProperty(pathKey); | ||
| if (!keyExisted) { | ||
| logger.info(`[update] ${this.name}: now watching < ${pathKey} >`); | ||
| this.subscribe(splitPath(pathKey)); | ||
| } | ||
| }); | ||
| const eventIdCnt = React.useRef(0); | ||
| const dispatch = func => (...args) => | ||
| store.exec( | ||
| { | ||
| id: `${name}/event.${eventIdCnt.current++}`, | ||
| parentId: name, | ||
| }, | ||
| func, | ||
| ...args, | ||
| ); | ||
|
|
||
| Object.keys(this.prevWatched).forEach(pathKey => { | ||
| const keyDeleted = !this.watched.hasOwnProperty(pathKey); | ||
| if (keyDeleted) { | ||
| logger.info(`[update] ${this.name}: stop watching < ${pathKey} >`); | ||
| this.unsubscribe(splitPath(pathKey)); | ||
| } | ||
| const watched = React.useRef({}); | ||
| const prevWatched = React.useRef({}); | ||
|
|
||
| // On update, update subscriptions | ||
| React.useEffect(() => { | ||
| logger.info(`[updating watch] ${name}`); | ||
|
|
||
| Object.keys(watched.current).forEach(pathKey => { | ||
| const keyExisted = prevWatched.current.hasOwnProperty(pathKey); | ||
| if (!keyExisted) { | ||
| logger.info(`[update] ${name}: now watching < ${pathKey} >`); | ||
| subscribe(splitPath(pathKey)); | ||
| } | ||
| }); | ||
|
|
||
| Object.keys(prevWatched).forEach(pathKey => { | ||
| const keyDeleted = watched.current.hasOwnProperty(pathKey); | ||
|
||
| if (keyDeleted) { | ||
| logger.info(`[update] ${name}: stop watching < ${pathKey} >`); | ||
| unsubscribe(splitPath(pathKey)); | ||
| } | ||
| }); | ||
|
|
||
| prevWatched.current = { ...watched.current }; | ||
| watched.current = {}; | ||
| }); | ||
|
|
||
| // On unmount, unsubscribe from everything | ||
| React.useEffect(() => { | ||
| return () => { | ||
| logger.info(`[unmounting]: ${name}`, watched.current); | ||
|
|
||
| Object.keys(prevWatched.current).forEach(pathKey => { | ||
| logger.info(`[unmount] ${name}: stop watching < ${pathKey} >`); | ||
| unsubscribe(splitPath(pathKey)); | ||
| }); | ||
| logger.debug("store", store); | ||
| status.current = { unmounted: true }; | ||
| }; | ||
| }, []); | ||
|
|
||
| this.prevWatched = { ...this.watched }; | ||
| this.watched = {}; | ||
| } | ||
|
|
||
| public componentWillUnmount() { | ||
| logger.info(`[will unmount]: ${this.name}`, this.state); | ||
| Object.keys(this.state).forEach(pathKey => { | ||
| logger.info(`[unmount] ${this.name}: stop watching < ${pathKey} >`); | ||
| this.unsubscribe(splitPath(pathKey)); | ||
| }); | ||
| const watch = valueExtractor(store, watched.current); | ||
|
|
||
| logger.debug("store", this.store); | ||
| const _subscribe = (path: string[], unsubscribe?: () => void): void => { | ||
| const pathKey = joinPath(path); | ||
| watched.current[pathKey] = getValue(path, store.universe); | ||
| subscribe(path, unsubscribe); | ||
| }; | ||
|
|
||
| this.status.unmounted = true; | ||
| } | ||
| const ctx = React.useMemo(() => { | ||
| const ctx = { | ||
| dispatch, | ||
| state, | ||
| watch, | ||
| subscribe: _subscribe, | ||
| }; | ||
| store.plugins.forEach(p => { | ||
| if (p._internals.viewCtx) { | ||
| ctx.dispatch = dispatch; | ||
| p._internals.viewCtx( | ||
| { | ||
| ctx, | ||
| universe: store.universe, | ||
| comp: { | ||
| name, | ||
| compId: compId.current, | ||
| }, | ||
| }, | ||
| store.config, | ||
| ); | ||
| } | ||
| }); | ||
| return ctx; | ||
| }, [store.universe]); | ||
|
|
||
| public render() { | ||
| this.createViewCtx(); | ||
| return (func as ((args: any) => (props: any) => any))(ctx)(props); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure this breaks support for hooks because
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've done this before and I think it worked. Will do some testing
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think hooks still work. At least the update-testing example works as expected. |
||
| }; | ||
|
|
||
| const Comp = this._renderFunc; | ||
| return <Comp {...this.props} />; | ||
| } | ||
| // public shouldComponentUpdate(nextProps: P, nextState: any) { | ||
| // const test = | ||
| // !shallowEqual(this.props, nextProps) || | ||
| // (!this.firstTime && !shallowEqual(this.state, nextState)); | ||
|
|
||
| private createViewCtx() { | ||
| this.store = this.context; | ||
| this._state = createUniverseWatcher("state"); | ||
| this._watch = valueExtractor(this.store, this.watched); | ||
|
|
||
| const subscribe = (path: string[], unsubscribe?: () => void): void => { | ||
| const pathKey = joinPath(path); | ||
| this.watched[pathKey] = getValue(path, this.store.universe); | ||
| this.subscribe(path, unsubscribe); | ||
| }; | ||
|
|
||
| const ctx = { | ||
| dispatch: this._dispatch, | ||
| state: this._state, | ||
| watch: this._watch, | ||
| subscribe, | ||
| }; | ||
|
|
||
| this.store.plugins.forEach(p => { | ||
| if (p._internals.viewCtx) { | ||
| (ctx as any).dispatch = this._dispatch; | ||
|
|
||
| p._internals.viewCtx( | ||
| { | ||
| ctx, | ||
| universe: this.store.universe, | ||
| comp: this.comp, | ||
| }, | ||
| this.store.config, | ||
| ); | ||
| } | ||
| }); | ||
| // logger.info(`[should update] ${this.name}`, test); | ||
| // this.firstTime = false; | ||
|
|
||
| this._viewCtx = ctx; | ||
| } | ||
| }; | ||
| // return test; | ||
| // } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same as the
getfunction inwatch.ts. Maybe extract so its only defined once.