Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
335 changes: 146 additions & 189 deletions packages/core/src/connect.tsx
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);

Expand All @@ -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 =>
Copy link
Collaborator

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 get function in watch.ts. Maybe extract so its only defined once.

path.reduce((x: any, y: any) => x && x[y], obj);

Expand Down Expand Up @@ -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({});
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

BUG: missing !

Copy link
Member Author

Choose a reason for hiding this comment

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

?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh sorry, you mean negation. Good spot!

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure this breaks support for hooks because func(ctx) will be a different function each time.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
// }
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export interface Comp {
export interface Node extends Comp {
pathKey: string;
status: { unmounted: boolean };
setState: (state: any) => void;
forceUpdate: () => void;
unsubscribe?: (comp: Comp) => void;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const submitPatches = (
if (!comps[x.compId]) {
compIds.push(x.compId);
comps[x.compId] = {
setState: x.setState,
forceUpdate: x.forceUpdate,
status: x.status,
name: x.name,
newValues: {},
Expand All @@ -125,11 +125,11 @@ export const submitPatches = (

event.rerender = {};
Object.keys(comps).forEach(compId => {
const { setState, name, newValues, status } = comps[compId];
const { forceUpdate, name, newValues, status } = comps[compId];
if (!status.unmounted) {
event.rerender![comps[compId].name] = true;
logger.info(`[upcoming state update] ${name}`, newValues, status);
setState(newValues);
forceUpdate();
}
});
};
Loading