Skip to content

Commit c29d7c4

Browse files
authored
fix: useContext fixes (#495)
1 parent d94740c commit c29d7c4

File tree

11 files changed

+208
-16
lines changed

11 files changed

+208
-16
lines changed

packages/qwik/src/core/object/store.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ELEMENT_ID,
1111
ELEMENT_ID_PREFIX,
1212
QContainerAttr,
13+
QCtxAttr,
1314
QHostAttr,
1415
QObjAttr,
1516
QSeqAttr,
@@ -87,6 +88,7 @@ export function resumeContainer(containerEl: Element) {
8788

8889
const seq = el.getAttribute(QSeqAttr)!;
8990
const host = el.getAttribute(QHostAttr)!;
91+
const contexts = el.getAttribute(QCtxAttr)!;
9092
const ctx = getContext(el);
9193

9294
// Restore captured objets
@@ -109,16 +111,14 @@ export function resumeContainer(containerEl: Element) {
109111
ctx.props = ctx.refMap.get(props);
110112
ctx.renderQrl = ctx.refMap.get(renderQrl);
111113
}
112-
113-
const attributes = el.attributes;
114-
for (let i = 0; i < attributes.length; i++) {
115-
const attr = attributes.item(i)!;
116-
if (attr.name.startsWith('ctx:')) {
114+
if (contexts) {
115+
contexts.split(' ').map((part) => {
116+
const [key, value] = part.split('=');
117117
if (!ctx.contexts) {
118118
ctx.contexts = new Map();
119119
}
120-
ctx.contexts.set(attr.name.slice('ctx:'.length), ctx.refMap.get(strToInt(attr.value)));
121-
}
120+
ctx.contexts.set(key, ctx.refMap.get(strToInt(value)));
121+
});
122122
}
123123
});
124124
containerEl.setAttribute(QContainerAttr, 'resumed');
@@ -160,7 +160,7 @@ export function snapshotState(containerEl: Element): SnapshotResult {
160160
// TODO: improve serialization, get rid of refMap
161161
const hasListeners = ctx.listeners && ctx.listeners.size > 0;
162162
const hasWatch = ctx.refMap.array.some(isWatchCleanup);
163-
const hasContext = ctx.refMap.array.some(isWatchCleanup);
163+
const hasContext = !!ctx.contexts;
164164
if (hasListeners || hasWatch || hasContext) {
165165
collectElement(node, collector);
166166
}
@@ -327,9 +327,11 @@ export function snapshotState(containerEl: Element): SnapshotResult {
327327
}
328328

329329
if (contexts) {
330+
const serializedContexts: string[] = [];
330331
contexts.forEach((value, key) => {
331-
node.setAttribute(`ctx:${key}`, `${ctx.refMap.indexOf(value)}`);
332+
serializedContexts.push(`${key}=${ctx.refMap.indexOf(value)}`);
332333
});
334+
node.setAttribute(QCtxAttr, serializedContexts.join(' '));
333335
}
334336
});
335337

packages/qwik/src/core/use/use-context.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fromCamelToKebabCase } from '../util/case';
55
import { getContext } from '../props/props';
66
import { unwrapSubscriber, wrapSubscriber } from './use-subscriber';
77
import { useHostElement } from './use-host-element.public';
8+
import { QCtxAttr } from '../util/markers';
89

910
/**
1011
* @alpha
@@ -39,7 +40,12 @@ export function useContextProvider<STATE extends object>(context: Context<STATE>
3940
}
4041
newValue = unwrapSubscriber(newValue);
4142
contexts.set(context.id, newValue);
42-
setAttribute(renderCtx, hostElement, `ctx:${context.id}`, '');
43+
44+
const serializedContexts: string[] = [];
45+
contexts.forEach((value, key) => {
46+
serializedContexts.push(`${key}=${ctx.refMap.indexOf(value)}`);
47+
});
48+
setAttribute(renderCtx, hostElement, QCtxAttr, serializedContexts.join(' '));
4349
setValue(newValue);
4450
}
4551
}
@@ -69,7 +75,7 @@ export function _useContext<STATE extends object>(context: Context<STATE>): STAT
6975
}
7076
}
7177
}
72-
const foundEl = hostElement.closest(`[ctx\\:${context.id}]`);
78+
const foundEl = hostElement.closest(`[q\\:ctx*="${context.id}="]`);
7379
if (foundEl) {
7480
const value = getContext(foundEl).contexts!.get(context.id);
7581
if (value) {

packages/qwik/src/core/util/markers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export const QObjAttr = 'q:obj';
6969

7070
export const QSeqAttr = 'q:seq';
7171

72+
export const QCtxAttr = 'q:ctx';
73+
7274
export const QContainerAttr = 'q:container';
7375

7476
export const QObjSelector = '[q\\:obj]';
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
useStore,
3+
component$,
4+
Host,
5+
createContext,
6+
useContextProvider,
7+
useContext,
8+
} from '@builder.io/qwik';
9+
10+
export interface ContextI {
11+
displayName: string;
12+
count: number;
13+
}
14+
15+
export const Context1 = createContext<ContextI>('ctx');
16+
export const Context2 = createContext<ContextI>('ctx1');
17+
export const Context3 = createContext<ContextI>('ctx2');
18+
19+
export const ContextRoot = component$(async () => {
20+
const state1 = useStore({ displayName: 'ROOT / state1', count: 0 });
21+
const state2 = useStore({ displayName: 'ROOT / state2', count: 0 });
22+
23+
useContextProvider(Context1, state1);
24+
useContextProvider(Context2, state2);
25+
26+
return (
27+
<Host>
28+
<button class="root-increment1" onClick$={() => state1.count++}>
29+
Increment State 1
30+
</button>
31+
<button class="root-increment2" onClick$={() => state2.count++}>
32+
Increment State 2
33+
</button>
34+
35+
<Level2 />
36+
<Level2 />
37+
</Host>
38+
);
39+
});
40+
41+
// This code will not work because its async before reading subs
42+
export const Level2 = component$(() => {
43+
const level2State1 = useStore({ displayName: 'Level2 / state1', count: 0 });
44+
useContextProvider(Context1, level2State1);
45+
46+
const state3 = useStore({ displayName: 'Level2 / state3', count: 0 });
47+
useContextProvider(Context3, state3);
48+
49+
const state1 = useContext(Context1);
50+
const state2 = useContext(Context2);
51+
52+
return (
53+
<Host>
54+
<h1>Level2</h1>
55+
<div class="level2-state1">
56+
{state1.displayName} = {state1.count}
57+
</div>
58+
<div class="level2-state2">
59+
{state2.displayName} = {state2.count}
60+
</div>
61+
62+
<button class="level2-increment3" onClick$={() => state3.count++}>
63+
Increment
64+
</button>
65+
66+
{Array.from({ length: state3.count }, () => {
67+
return <Level3></Level3>;
68+
})}
69+
</Host>
70+
);
71+
});
72+
73+
export const Level3 = component$(() => {
74+
const state1 = useContext(Context1);
75+
const state2 = useContext(Context2);
76+
const state3 = useContext(Context3);
77+
78+
return (
79+
<Host>
80+
<h2>Level3</h2>
81+
<div class="level3-state1">
82+
{state1.displayName} = {state1.count}
83+
</div>
84+
<div class="level3-state2">
85+
{state2.displayName} = {state2.count}
86+
</div>
87+
<div class="level3-state3">
88+
{state3.displayName} = {state3.count}
89+
</div>
90+
</Host>
91+
);
92+
});

starters/apps/e2e/src/entry.ssr.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Containers } from './components/containers/container';
1111
import { Factory } from './components/factory/factory';
1212
import { Watch } from './components/watch/watch';
1313
import { EffectClient } from './components/effect-client/effect-client';
14+
import { ContextRoot } from './components/context/context';
1415

1516
/**
1617
* Entry point for server-side pre-rendering.
@@ -32,6 +33,7 @@ export function render(opts: RenderToStringOptions) {
3233
'/e2e/factory': () => <Factory />,
3334
'/e2e/watch': () => <Watch />,
3435
'/e2e/effect-client': () => <EffectClient />,
36+
'/e2e/context': () => <ContextRoot />,
3537
};
3638
const Test = tests[url.pathname];
3739

starters/apps/e2e/src/root.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export const Root = component$(() => {
3535
<p>
3636
<a href="/e2e/effect-client">Client effect</a>
3737
</p>
38+
<p>
39+
<a href="/e2e/context">Context</a>
40+
</p>
3841
</section>
3942
);
4043
});

starters/apps/todo/src/components/body/body.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const Body = component$(() => {
88
<Host class="main">
99
<ul class="todo-list">
1010
{todos.items.filter(FILTERS[todos.filter]).map((key) => (
11-
<Item item={key} todos={todos} />
11+
<Item item={key} />
1212
))}
1313
</ul>
1414
</Host>

starters/apps/todo/src/components/item/item.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { component$, useStore, Host, useRef, useWatch$ } from '@builder.io/qwik';
1+
import { component$, useStore, Host, useRef, useWatch$, useContext } from '@builder.io/qwik';
22

3-
import type { TodoItem, Todos } from '../../state/state';
3+
import { TodoItem, TODOS, Todos } from '../../state/state';
44

55
/**
66
* Individual items of the component.
@@ -10,13 +10,13 @@ import type { TodoItem, Todos } from '../../state/state';
1010

1111
export interface ItemProps {
1212
item: TodoItem;
13-
todos: Todos;
1413
}
1514

1615
export const Item = component$(
1716
(props: ItemProps) => {
1817
const state = useStore({ editing: false });
1918
const editInput = useRef<HTMLInputElement>();
19+
const todos = useContext(TODOS);
2020

2121
useWatch$((track) => {
2222
const current = track(editInput, 'current');
@@ -48,7 +48,7 @@ export const Item = component$(
4848
class="destroy"
4949
onClick$={() => {
5050
const todoItem = props.item;
51-
props.todos.items = props.todos.items.filter((i) => i != todoItem);
51+
todos.items = todos.items.filter((i) => i != todoItem);
5252
}}
5353
/>
5454
</div>

starters/apps/todo/src/state/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { createContext } from '@builder.io/qwik';
66

77
export const TODOS = createContext<Todos>('TodoApp');
8+
89
export interface TodoItem {
910
completed: boolean;
1011
title: string;

0 commit comments

Comments
 (0)