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: contexts can store scalars; nicer logging #5914

Merged
merged 2 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2992,7 +2992,7 @@
}
],
"kind": "Function",
"content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId<TodosStore>('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore<TodosStore>({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return <Items />;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n <ul>\n {todos.items.map((item) => (\n <li>{item}</li>\n ))}\n </ul>\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: <STATE extends object>(context: ContextId<STATE>, newValue: STATE) => void\n```\n\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| context | [ContextId](#contextid)<!-- -->&lt;STATE&gt; | The context to assign a value to. |\n| newValue | STATE | |\n\n**Returns:**\n\nvoid",
"content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId<TodosStore>('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore<TodosStore>({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return <Items />;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n <ul>\n {todos.items.map((item) => (\n <li>{item}</li>\n ))}\n </ul>\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: <STATE>(context: ContextId<STATE>, newValue: STATE) => void\n```\n\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| context | [ContextId](#contextid)<!-- -->&lt;STATE&gt; | The context to assign a value to. |\n| newValue | STATE | |\n\n**Returns:**\n\nvoid",
"editUrl": "https://github.com/BuilderIO/qwik/tree/main/packages/qwik/src/core/use/use-context.ts",
"mdFile": "qwik.usecontextprovider.md"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3628,7 +3628,7 @@ Assign a value to a Context.

Use `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.

Context is a way to pass stores to the child components without prop-drilling.
Context is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.

### Example

Expand Down Expand Up @@ -3667,7 +3667,7 @@ export const Items = component$(() => {
```

```typescript
useContextProvider: <STATE extends object>(context: ContextId<STATE>, newValue: STATE) => void
useContextProvider: <STATE>(context: ContextId<STATE>, newValue: STATE) => void
```

| Parameter | Type | Description |
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1614,7 +1614,7 @@ export const useComputedQrl: ComputedQRL;
export const useContext: UseContext;

// @public
export const useContextProvider: <STATE extends object>(context: ContextId<STATE>, newValue: STATE) => void;
export const useContextProvider: <STATE>(context: ContextId<STATE>, newValue: STATE) => void;

// @public (undocumented)
export const useErrorBoundary: () => Readonly<ErrorBoundaryStore>;
Expand Down
38 changes: 24 additions & 14 deletions packages/qwik/src/core/error/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { logErrorAndStop } from '../util/log';
import { qDev } from '../util/qdev';

export const QError_stringifyClassOrStyle = 0;
export const QError_cannotSerializeNode = 1; // 'Can not serialize a HTML Node that is not an Element'
export const QError_runtimeQrlNoElement = 2; // `Q-ERROR: '${qrl}' is runtime but no instance found on element.`
export const QError_verifySerializable = 3; // 'Only primitive and object literals can be serialized', value,
export const QError_errorWhileRendering = 4; // Crash while rendering
export const QError_cannotRenderOverExistingContainer = 5; //'You can render over a existing q:container. Skipping render().'
export const QError_setProperty = 6; //'Set property'
export const QError_cannotSerializeNode = 1;
export const QError_runtimeQrlNoElement = 2;
export const QError_verifySerializable = 3;
export const QError_errorWhileRendering = 4;
export const QError_cannotRenderOverExistingContainer = 5;
export const QError_setProperty = 6;
export const QError_qrlOrError = 7;
export const QError_onlyObjectWrapped = 8;
export const QError_onlyLiteralWrapped = 9;
Expand Down Expand Up @@ -35,11 +35,11 @@ export const QError_qrlMissingContainer = 30;
export const QError_qrlMissingChunk = 31;
export const QError_invalidRefValue = 32;
export const qError = (code: number, ...parts: any[]): Error => {
const text = codeToText(code);
const text = codeToText(code, ...parts);
return logErrorAndStop(text, ...parts);
};

export const codeToText = (code: number): string => {
export const codeToText = (code: number, ...parts: any[]): string => {
if (qDev) {
const MAP = [
'Error while serializing class attribute', // 0
Expand All @@ -48,14 +48,14 @@ export const codeToText = (code: number): string => {
'Only primitive and object literals can be serialized', // 3
'Crash while rendering', // 4
'You can render over a existing q:container. Skipping render().', // 5
'Set property', // 6
'Set property {{0}}', // 6
"Only function's and 'string's are supported.", // 7
"Only objects can be wrapped in 'QObject'", // 8
`Only objects literals can be wrapped in 'QObject'`, // 9
'QRL is not a function', // 10
'Dynamic import not found', // 11
'Unknown type argument', // 12
'Actual value for useContext() can not be found, make sure some ancestor component has set a value using useContextProvider()', // 13
`Actual value for useContext({{0}}) can not be found, make sure some ancestor component has set a value using useContextProvider(). In the browser make sure that the context was used during SSR so its state was serialized.`, // 13
"Invoking 'use*()' method outside of invocation context.", // 14
'Cant access renderCtx for existing context', // 15
'Cant access document for existing context', // 16
Expand All @@ -68,17 +68,27 @@ For more information see: https://qwik.builder.io/docs/components/tasks/#use-met
'Components using useServerMount() can only be mounted in the server, if you need your component to be mounted in the client, use "useMount$()" instead', // 22
'When rendering directly on top of Document, the root node must be a <html>', // 23
'A <html> node must have 2 children. The first one <head> and the second one a <body>', // 24
'Invalid JSXNode type. It must be either a function or a string. Found:', // 25
'Invalid JSXNode type "{{0}}". It must be either a function or a string. Found:', // 25
'Tracking value changes can only be done to useStore() objects and component props', // 26
'Missing Object ID for captured object', // 27
'The provided Context reference is not a valid context created by createContextId()', // 28
'The provided Context reference "{{0}}" is not a valid context created by createContextId()', // 28
'<html> is the root container, it can not be rendered inside a component', // 29
'QRLs can not be resolved because it does not have an attached container. This means that the QRL does not know where it belongs inside the DOM, so it cant dynamically import() from a relative path.', // 30
'QRLs can not be dynamically resolved, because it does not have a chunk path', // 31
'The JSX ref attribute must be a Signal', // 32
];
return `Code(${code}): ${MAP[code] ?? ''}`;
let text = MAP[code] ?? '';
if (parts.length) {
text = text.replaceAll(/{{(\d+)}}/g, (_, index) => {
let v = parts[index];
if (v && typeof v === 'object' && v.constructor === Object) {
v = JSON.stringify(v).slice(0, 50);
}
return v;
});
}
return `Code(${code}): ${text}`;
} else {
return `Code(${code})`;
return `Code(${code}), see https://github.com/BuilderIO/qwik/blob/main/packages/qwik/src/core/error/error.ts#L44`;
}
};
2 changes: 1 addition & 1 deletion packages/qwik/src/core/render/dom/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const _setProperty = (node: any, key: string, value: any) => {
node.removeAttribute(key);
}
} catch (err) {
logError(codeToText(QError_setProperty), { node, key, value }, err);
logError(codeToText(QError_setProperty), key, { node, value }, err);
}
};

Expand Down
22 changes: 10 additions & 12 deletions packages/qwik/src/core/use/use-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ export const createContextId = <STATE = unknown>(name: string): ContextId<STATE>
* component's function. Once assigned, use `useContext()` in any child component to retrieve the
* value.
*
* Context is a way to pass stores to the child components without prop-drilling.
* Context is a way to pass stores to the child components without prop-drilling. Note that scalar
* values are allowed, but for reactivity you need signals or stores.
*
* ### Example
*
Expand Down Expand Up @@ -191,10 +192,7 @@ export const createContextId = <STATE = unknown>(name: string): ContextId<STATE>
* @public
*/
// </docs>
export const useContextProvider = <STATE extends object>(
context: ContextId<STATE>,
newValue: STATE
) => {
export const useContextProvider = <STATE>(context: ContextId<STATE>, newValue: STATE) => {
const { val, set, elCtx } = useSequentialScope<boolean>();
if (val !== undefined) {
return;
Expand All @@ -211,9 +209,9 @@ export const useContextProvider = <STATE extends object>(
};

export interface UseContext {
<STATE extends object, T>(context: ContextId<STATE>, transformer: (value: STATE) => T): T;
<STATE extends object, T>(context: ContextId<STATE>, defaultValue: T): STATE | T;
<STATE extends object>(context: ContextId<STATE>): STATE;
<STATE, T>(context: ContextId<STATE>, transformer: (value: STATE) => T): T;
<STATE, T>(context: ContextId<STATE>, defaultValue: T): STATE | T;
<STATE>(context: ContextId<STATE>): STATE;
}

// <docs markdown="../readme.md#useContext">
Expand Down Expand Up @@ -266,9 +264,9 @@ export interface UseContext {
* @public
*/
// </docs>
export const useContext: UseContext = <STATE extends object>(
export const useContext: UseContext = <STATE>(
context: ContextId<STATE>,
defaultValue?: any
defaultValue?: STATE | ((current: STATE | undefined) => STATE)
) => {
const { val, set, iCtx, elCtx } = useSequentialScope<STATE>();
if (val !== undefined) {
Expand All @@ -280,7 +278,7 @@ export const useContext: UseContext = <STATE extends object>(

const value = resolveContext(context, elCtx, iCtx.$renderCtx$.$static$.$containerState$);
if (typeof defaultValue === 'function') {
return set(invoke(undefined, defaultValue, value));
return set(invoke(undefined, defaultValue as any, value));
}
if (value !== undefined) {
return set(value);
Expand Down Expand Up @@ -346,7 +344,7 @@ const getParentProvider = (ctx: QContext, containerState: ContainerState): QCont
return ctx.$parentCtx$;
};

export const resolveContext = <STATE extends object>(
export const resolveContext = <STATE>(
context: ContextId<STATE>,
hostCtx: QContext,
containerState: ContainerState
Expand Down
10 changes: 7 additions & 3 deletions packages/qwik/src/core/util/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const STYLE = qDev
: '';

export const logError = (message?: any, ...optionalParams: any[]) => {
return createAndLogError(true, message, ...optionalParams);
return createAndLogError(false, message, ...optionalParams);
};

export const throwErrorAndStop = (message?: any, ...optionalParams: any[]): never => {
Expand Down Expand Up @@ -81,8 +81,12 @@ const printElement = (el: Element) => {

const createAndLogError = (asyncThrow: boolean, message?: any, ...optionalParams: any[]) => {
const err = message instanceof Error ? message : new Error(message);
const messageStr = err.stack || err.message;
console.error('%cQWIK ERROR', STYLE, messageStr, ...printParams(optionalParams));

// display the error message first, then the optional params, and finally the stack trace
// the stack needs to be displayed last because the given params will be lost among large stack traces so it will
// provide a bad developer experience
console.error('%cQWIK ERROR', STYLE, err.message, ...printParams(optionalParams), err.stack);

asyncThrow &&
!qTest &&
setTimeout(() => {
Expand Down
7 changes: 3 additions & 4 deletions packages/qwik/src/optimizer/src/plugins/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
const vendorIds = vendorRoots.map((v) => v.id);
const isDevelopment = buildMode === 'development';
const qDevKey = 'globalThis.qDev';
const qTestKey = 'globalThis.qTest';
const qInspectorKey = 'globalThis.qInspector';
const qSerializeKey = 'globalThis.qSerialize';
const qDev = viteConfig?.define?.[qDevKey] ?? isDevelopment;
Expand Down Expand Up @@ -313,6 +314,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
[qDevKey]: qDev,
[qInspectorKey]: qInspector,
[qSerializeKey]: qSerialize,
[qTestKey]: JSON.stringify(process.env.NODE_ENV === 'test'),
},
};

Expand Down Expand Up @@ -358,10 +360,6 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
updatedViteConfig.build!.minify = false;
} else {
// Test Build
const qDevKey = 'globalThis.qDev';
const qTestKey = 'globalThis.qTest';
const qInspectorKey = 'globalThis.qInspector';

updatedViteConfig.define = {
[qDevKey]: true,
[qTestKey]: true,
Expand All @@ -370,6 +368,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
}

(globalThis as any).qDev = qDev;
(globalThis as any).qTest = true;
(globalThis as any).qInspector = qInspector;
}

Expand Down
12 changes: 12 additions & 0 deletions starters/apps/e2e/src/components/context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const Context2 = createContextId<ContextI>("ctx1");
export const Context3 = createContextId<ContextI>("ctx2");
export const ContextSlot = createContextId<ContextI>("slot");
export const Unset = createContextId<ContextI>("unset");
export const ContextString = createContextId<string>("ctx-string");

export const ContextRoot = component$(() => {
const count = useSignal(0);
Expand Down Expand Up @@ -57,6 +58,7 @@ export const ContextApp = component$(() => {
<Issue2087 />
<Issue2894 />
<Issue5356 />
<Issue5793 />
</div>
);
});
Expand Down Expand Up @@ -310,3 +312,13 @@ export const Issue5356_Child = component$<{ value: number; active: boolean }>(
);
},
);

export const Issue5793 = component$(() => {
useContextProvider(ContextString, "yes");
return <Issue5793_Child />;
});

export const Issue5793_Child = component$(() => {
const s = useContext(ContextString);
return <div id="issue5793-value">{s}</div>;
});
6 changes: 6 additions & 0 deletions starters/e2e/e2e.context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ test.describe("context", () => {
await expect(child1).toContainText("Child 1, active: true");
await expect(child2).toContainText("Child 2, active: false");
});

test("issue 5793 scalar context values", async ({ page }) => {
const value = page.locator("#issue5793-value");

await expect(value).toHaveText("yes");
});
}

test.beforeEach(async ({ page }) => {
Expand Down
Loading