diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts
index 16e06198eeabaf284b8d0cb524a5592f781c72dc..bb04a1de101777951c6c67e0cdb8b5f88908c3fe 100644
--- a/src/mol-state/context.ts
+++ b/src/mol-state/context.ts
@@ -6,13 +6,13 @@
 
 import { Subject } from 'rxjs'
 import { StateObject } from './object';
-import { Task } from 'mol-task';
 import { Transform } from './tree/transform';
 
 interface StateContext {
     events: {
         object: {
             stateChanged: Subject<{ ref: Transform.Ref }>,
+            propsChanged: Subject<{ ref: Transform.Ref, newProps: unknown }>,
             updated: Subject<{ ref: Transform.Ref }>,
             replaced: Subject<{ ref: Transform.Ref, old?: StateObject }>,
             created: Subject<{ ref: Transform.Ref }>,
@@ -21,15 +21,16 @@ interface StateContext {
         warn: Subject<string>
     },
     globalContext: unknown,
-    runTask<T>(task: T | Task<T>): T | Promise<T>
+    defaultObjectProps: unknown
 }
 
 namespace StateContext {
-    export function create(globalContext?: unknown/* task?: { observer?: Progress.Observer, updateRateMs?: number } */): StateContext {
+    export function create(params: { globalContext: unknown, defaultObjectProps: unknown }): StateContext {
         return {
             events: {
                 object: {
                     stateChanged: new Subject(),
+                    propsChanged: new Subject(),
                     updated: new Subject(),
                     replaced: new Subject(),
                     created: new Subject(),
@@ -37,11 +38,8 @@ namespace StateContext {
                 },
                 warn: new Subject()
             },
-            globalContext,
-            runTask<T>(t: T | Task<T>) {
-                if (typeof (t as any).run === 'function') return (t as Task<T>).run();
-                return t as T;
-            }
+            globalContext: params.globalContext,
+            defaultObjectProps: params.defaultObjectProps
         }
     }
 }
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index 7f9019fc9bc5e03c776c5cc242e6aeb7d254787d..61f9142cf8edb433173664ec844742edc2f5b744 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -46,8 +46,9 @@ export namespace StateObject {
         }
     }
 
-    export interface Wrapped {
+    export interface Node {
         state: StateType,
+        props: unknown,
         errorText?: string,
         obj?: StateObject,
         version: string
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index dfba3eba5b84f9ff8ca355a901e04ea2aff19356..b586e5c2cc4de70b4115bf2bfe505e398f451160 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -7,60 +7,59 @@
 import { StateObject } from './object';
 import { StateTree } from './tree';
 import { Transform } from './tree/transform';
-import { Map as ImmutableMap } from 'immutable';
-// import { StateContext } from './context/context';
 import { ImmutableTree } from './util/immutable-tree';
 import { Transformer } from './transformer';
 import { StateContext } from './context';
 import { UUID } from 'mol-util';
+import { RuntimeContext, Task } from 'mol-task';
 
-export interface State<ObjectProps = unknown> {
-    definition: State.Definition<ObjectProps>,
+export interface State {
+    tree: StateTree,
     objects: State.Objects,
     context: StateContext
 }
 
 export namespace State {
     export type Ref = Transform.Ref
-    export type ObjectProps<P = unknown> = ImmutableMap<Ref, P>
-    export type Objects = Map<Ref, StateObject.Wrapped>
+    export type Objects = Map<Ref, StateObject.Node>
 
-    export interface Definition<P = unknown> {
-        tree: StateTree,
-        // things like object visibility
-        props: ObjectProps<P>
-    }
-
-    export function create(params?: { globalContext?: unknown }): State {
+    export function create(params?: { globalContext?: unknown, defaultObjectProps: unknown }) {
         const tree = StateTree.create();
         const objects: Objects = new Map();
         const root = tree.getValue(tree.rootRef)!;
+        const defaultObjectProps = (params && params.defaultObjectProps) || { }
 
-        objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version });
+        objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version, props: { ...defaultObjectProps } });
 
         return {
-            definition: {
-                tree,
-                props: ImmutableMap()
-            },
+            tree,
             objects,
-            context: StateContext.create(params && params.globalContext)
+            context: StateContext.create({
+                globalContext: params && params.globalContext,
+                defaultObjectProps
+            })
         };
     }
 
-    export async function update<P>(state: State<P>, tree: StateTree): Promise<State<P>> {
-        const ctx: UpdateContext = {
-            stateCtx: state.context,
-            old: state.definition,
-            tree: tree,
-            props: state.definition.props.asMutable(),
-            objects: state.objects
-        };
+    export function update(state: State, tree: StateTree): Task<State> {
+        return Task.create('Update Tree', taskCtx => {
+            const ctx: UpdateContext = {
+                stateCtx: state.context,
+                taskCtx,
+                oldTree: state.tree,
+                tree: tree,
+                objects: state.objects
+            };
+            return _update(ctx);
+        })
+    }
 
-        const roots = findUpdateRoots(state.objects, tree);
+    async function _update(ctx: UpdateContext): Promise<State> {
+        const roots = findUpdateRoots(ctx.objects, ctx.tree);
         const deletes = findDeletes(ctx);
         for (const d of deletes) {
-            state.objects.delete(d);
+            ctx.objects.delete(d);
+            ctx.stateCtx.events.object.removed.next({ ref: d });
         }
 
         initObjectState(ctx, roots);
@@ -70,17 +69,17 @@ export namespace State {
         }
 
         return {
-            definition: { tree, props: ctx.props.asImmutable() as ObjectProps<P> },
-            objects: state.objects,
-            context: state.context
+            tree: ctx.tree,
+            objects: ctx.objects,
+            context: ctx.stateCtx
         };
     }
 
     interface UpdateContext {
         stateCtx: StateContext,
-        old: Definition,
+        taskCtx: RuntimeContext,
+        oldTree: StateTree,
         tree: StateTree,
-        props: ObjectProps,
         objects: Objects
     }
 
@@ -120,15 +119,18 @@ export namespace State {
     }
 
     function setObjectState(ctx: UpdateContext, ref: Ref, state: StateObject.StateType, errorText?: string) {
+        let changed = false;
         if (ctx.objects.has(ref)) {
             const obj = ctx.objects.get(ref)!;
+            changed = obj.state !== state;
             obj.state = state;
             obj.errorText = errorText;
         } else {
-            const obj = { state, version: UUID.create(), errorText };
+            const obj = { state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } };
             ctx.objects.set(ref, obj);
+            changed = true;
         }
-        ctx.stateCtx.events.object.stateChanged.next({ ref });
+        if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref });
     }
 
     function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) {
@@ -170,15 +172,17 @@ export namespace State {
     }
 
     async function updateSubtree(ctx: UpdateContext, root: Ref) {
-        setObjectState(ctx, root, StateObject.StateType.Pending);
+        setObjectState(ctx, root, StateObject.StateType.Processing);
 
         try {
             const update = await updateNode(ctx, root);
             setObjectState(ctx, root, StateObject.StateType.Ok);
-            if (update === 'created') {
+            if (update.action === 'created') {
                 ctx.stateCtx.events.object.created.next({ ref: root });
-            } else if (update === 'updated') {
+            } else if (update.action === 'updated') {
                 ctx.stateCtx.events.object.updated.next({ ref: root });
+            } else if (update.action === 'replaced') {
+                ctx.stateCtx.events.object.replaced.next({ ref: root, old: update.old });
             }
         } catch (e) {
             doError(ctx, root, '' + e);
@@ -194,43 +198,61 @@ export namespace State {
     }
 
     async function updateNode(ctx: UpdateContext, currentRef: Ref) {
-        const { old: { tree: oldTree }, tree, objects } = ctx;
+        const { oldTree, tree, objects } = ctx;
         const transform = tree.getValue(currentRef)!;
         const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from);
-        //console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
+        // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
         if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) {
-            console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
+            // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
             const obj = await createObject(ctx, transform.transformer, parent, transform.params);
             obj.ref = currentRef;
-            objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version });
-            return 'created';
+            objects.set(currentRef, {
+                obj,
+                state: StateObject.StateType.Ok,
+                version: transform.version,
+                props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps }
+            });
+            return { action: 'created' };
         } else {
-            console.log('updating...', transform.transformer.id);
+            // console.log('updating...', transform.transformer.id);
             const current = objects.get(currentRef)!;
             const oldParams = oldTree.getValue(currentRef)!.params;
             switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
                 case Transformer.UpdateResult.Recreate: {
                     const obj = await createObject(ctx, transform.transformer, parent, transform.params);
                     obj.ref = currentRef;
-                    objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version });
-                    ctx.stateCtx.events.object.created.next({ ref: currentRef });
-                    return 'created';
+                    objects.set(currentRef, {
+                        obj,
+                        state: StateObject.StateType.Ok,
+                        version: transform.version,
+                        props: { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps }
+                    });
+                    return { action: 'replaced', old: current.obj! };
                 }
                 case Transformer.UpdateResult.Updated:
                     current.version = transform.version;
-                    return 'updated';
+                    current.props = { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps };
+                    return { action: 'updated' };
+                default:
+                    // TODO check if props need to be updated
+                    return { action: 'none' };
             }
         }
     }
 
+    function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
+        if (typeof (t as any).run === 'function') return (t as Task<T>).runInContext(ctx);
+        return t as T;
+    }
+
     function createObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, params: any) {
-        return ctx.stateCtx.runTask(transformer.definition.apply({ a, params, globalCtx: ctx.stateCtx.globalContext }));
+        return runTask(transformer.definition.apply({ a, params }, ctx.stateCtx.globalContext), ctx.taskCtx);
     }
 
     async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
         if (!transformer.definition.update) {
             return Transformer.UpdateResult.Recreate;
         }
-        return ctx.stateCtx.runTask(transformer.definition.update({ a, oldParams, b, newParams, globalCtx: ctx.stateCtx.globalContext }));
+        return runTask(transformer.definition.update({ a, oldParams, b, newParams }, ctx.stateCtx.globalContext), ctx.taskCtx);
     }
 }
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index 139a1d6107bc58589f5e528018860e653bcb1c7c..701085fff0adf0b6ca0d63357ed78e2fd9f354b0 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -23,16 +23,14 @@ export namespace Transformer {
 
     export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
         a: A,
-        params: P,
-        globalCtx: unknown
+        params: P
     }
 
     export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
         a: A,
         b: B,
         oldParams: P,
-        newParams: P,
-        globalCtx: unknown
+        newParams: P
     }
 
     export enum UpdateResult { Unchanged, Updated, Recreate }
@@ -46,14 +44,14 @@ export namespace Transformer {
          * Apply the actual transformation. It must be pure (i.e. with no side effects).
          * Returns a task that produces the result of the result directly.
          */
-        apply(params: ApplyParams<A, P>): Task<B> | B,
+        apply(params: ApplyParams<A, P>, globalCtx: unknown): Task<B> | B,
 
         /**
          * Attempts to update the entity in a non-destructive way.
          * For example changing a color scheme of a visual does not require computing new geometry.
          * Return/resolve to undefined if the update is not possible.
          */
-        update?(params: UpdateParams<A, B, P>): Task<UpdateResult> | UpdateResult,
+        update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult,
 
         /** Check the parameters and return a list of errors if the are not valid. */
         defaultParams?(a: A, globalCtx: unknown): P,
diff --git a/src/mol-util/object.ts b/src/mol-util/object.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2ee047d59ee226390d30cf68beb09f6f21b7df22
--- /dev/null
+++ b/src/mol-util/object.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+const hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/** Create new object if any property in "update" changes in "source". */
+export function shallowMerge2<T>(source: T, update: Partial<T>): T {
+    // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
+    let changed = false;
+    for (let k of Object.keys(update)) {
+        if (!hasOwnProperty.call(update, k)) continue;
+
+        if ((update as any)[k] !== (source as any)[k]) {
+            changed = true;
+            break;
+        }
+    }
+
+    if (!changed) return source;
+    return Object.assign({}, source, update);
+}
+
+export function shallowEqual<T>(a: T, b: T) {
+    if (!a) {
+        if (!b) return true;
+        return false;
+    }
+    if (!b) return false;
+
+    let keys = Object.keys(a);
+    if (Object.keys(b).length !== keys.length) return false;
+    for (let k of keys) {
+        if (!hasOwnProperty.call(a, k) || (a as any)[k] !== (b as any)[k]) return false;
+    }
+
+    return true;
+}
+
+export function shallowMerge<T>(source: T, ...rest: (Partial<T> | undefined)[]): T {
+    // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
+    let ret: any = source;
+
+    for (let s = 0; s < rest.length; s++) {
+        if (!rest[s]) continue;
+        ret = shallowMerge2(source, rest[s] as T);
+        if (ret !== source) {
+            for (let i = s + 1; i < rest.length; i++) {
+                ret = Object.assign(ret, rest[i]);
+            }
+            break;
+        }
+    }
+    return ret;
+}
\ No newline at end of file
diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts
index 976c329f6db349f9f3cc99e11345f964e7562776..0344b8dc4ff318ab8c32d9cba4d381f06407f31f 100644
--- a/src/perf-tests/state.ts
+++ b/src/perf-tests/state.ts
@@ -64,10 +64,20 @@ export async function runTask<A>(t: A | Task<A>): Promise<A> {
     return t as A;
 }
 
+function hookEvents(state: State) {
+    state.context.events.object.created.subscribe(e => console.log('created:', e.ref));
+    state.context.events.object.removed.subscribe(e => console.log('removed:', e.ref));
+    state.context.events.object.replaced.subscribe(e => console.log('replaced:', e.ref));
+    state.context.events.object.stateChanged.subscribe(e => console.log('stateChanged:', e.ref,
+        StateObject.StateType[state.objects.get(e.ref)!.state]));
+    state.context.events.object.updated.subscribe(e => console.log('updated:', e.ref));
+}
+
 export async function testState() {
     const state = State.create();
+    hookEvents(state);
 
-    const tree = state.definition.tree;
+    const tree = state.tree;
     const builder = StateTree.build(tree);
     builder.toRoot<Root>()
         .apply(CreateSquare, { a: 10 }, { ref: 'square' })
@@ -80,7 +90,7 @@ export async function testState() {
     printTTree(tree1);
     printTTree(tree2);
 
-    const state1 = await State.update(state, tree1);
+    const state1 = await State.update(state, tree1).run();
     console.log('----------------');
     console.log(util.inspect(state1.objects, true, 3, true));
 
@@ -93,7 +103,7 @@ export async function testState() {
     printTTree(treeFromJson);
 
     console.log('----------------');
-    const state2 = await State.update(state1, treeFromJson);
+    const state2 = await State.update(state1, treeFromJson).run();
     console.log(util.inspect(state2.objects, true, 3, true));
 }