diff --git a/compat/test/browser/render.test.js b/compat/test/browser/render.test.js
index 6245fc39fa..7781acb6d8 100644
--- a/compat/test/browser/render.test.js
+++ b/compat/test/browser/render.test.js
@@ -545,6 +545,40 @@ describe('compat render', () => {
expect(scratch.textContent).to.equal('foo');
});
+ it('should allow context as a component', () => {
+ const Context = createContext(null);
+ const CONTEXT = { a: 'a' };
+
+ let receivedContext;
+
+ class Inner extends Component {
+ render(props) {
+ return
{props.a}
;
+ }
+ }
+
+ sinon.spy(Inner.prototype, 'render');
+
+ render(
+
+
+
+ {data => {
+ receivedContext = data;
+ return ;
+ }}
+
+
+ ,
+ scratch
+ );
+
+ // initial render does not invoke anything but render():
+ expect(Inner.prototype.render).to.have.been.calledWithMatch(CONTEXT);
+ expect(receivedContext).to.equal(CONTEXT);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
it("should support recoils's usage of __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", () => {
// Simplified version of: https://github.com/facebookexperimental/Recoil/blob/c1b97f3a0117cad76cbc6ab3cb06d89a9ce717af/packages/recoil/core/Recoil_ReactMode.js#L36-L44
function useStateWrapper(init) {
diff --git a/compat/test/ts/index.tsx b/compat/test/ts/index.tsx
index f636ce6d5f..2e322931d9 100644
--- a/compat/test/ts/index.tsx
+++ b/compat/test/ts/index.tsx
@@ -15,3 +15,26 @@ React.unmountComponentAtNode(document.body.shadowRoot!);
React.createPortal(, document.createElement('div'));
React.createPortal(, document.createDocumentFragment());
React.createPortal(, document.body.shadowRoot!);
+
+const Ctx = React.createContext({ contextValue: '' });
+class SimpleComponentWithContextAsProvider extends React.Component {
+ componentProp = 'componentProp';
+ render() {
+ // Render inside div to ensure standard JSX elements still work
+ return (
+
+
+ {/* Ensure context still works */}
+
+ {({ contextValue }) => contextValue.toLowerCase()}
+
+
+
+ );
+ }
+}
+
+React.render(
+ ,
+ document.createElement('div')
+);
diff --git a/hooks/test/browser/useContext.test.js b/hooks/test/browser/useContext.test.js
index 67b7851406..6a47b107d8 100644
--- a/hooks/test/browser/useContext.test.js
+++ b/hooks/test/browser/useContext.test.js
@@ -206,6 +206,38 @@ describe('useContext', () => {
expect(values).to.deep.equal([13, 42, 69]);
});
+ it('should only subscribe a component once (non-provider)', () => {
+ const values = [];
+ const Context = createContext(13);
+ let provider, subSpy;
+
+ function Comp() {
+ const value = useContext(Context);
+ values.push(value);
+ return null;
+ }
+
+ render(, scratch);
+
+ render(
+ (provider = p)} value={42}>
+
+ ,
+ scratch
+ );
+ subSpy = sinon.spy(provider, 'sub');
+
+ render(
+
+
+ ,
+ scratch
+ );
+ expect(subSpy).to.not.have.been.called;
+
+ expect(values).to.deep.equal([13, 42, 69]);
+ });
+
it('should maintain context', done => {
const context = createContext(null);
const { Provider } = context;
diff --git a/mangle.json b/mangle.json
index dfbca39c3d..8c518f89fe 100644
--- a/mangle.json
+++ b/mangle.json
@@ -62,8 +62,8 @@
"$_globalContext": "__n",
"$_context": "c",
"$_defaultValue": "__",
- "$_id": "__c",
- "$_contextRef": "__",
+ "$_id": "__l",
+ "$_contextRef": "__c",
"$_parentDom": "__P",
"$_originalParentDom": "__O",
"$_prevState": "__u",
diff --git a/src/create-context.js b/src/create-context.js
index 1bb9868f6d..8ecaec9ea5 100644
--- a/src/create-context.js
+++ b/src/create-context.js
@@ -2,64 +2,58 @@ import { enqueueRender } from './component';
export let i = 0;
-export function createContext(defaultValue, contextId) {
- contextId = '__cC' + i++;
-
- const context = {
- _id: contextId,
- _defaultValue: defaultValue,
- /** @type {import('./internal').FunctionComponent} */
- Consumer(props, contextValue) {
- // return props.children(
- // context[contextId] ? context[contextId].props.value : defaultValue
- // );
- return props.children(contextValue);
- },
- /** @type {import('./internal').FunctionComponent} */
- Provider(props) {
- if (!this.getChildContext) {
- /** @type {Set | null} */
- let subs = new Set();
- let ctx = {};
- ctx[contextId] = this;
-
- this.getChildContext = () => ctx;
-
- this.componentWillUnmount = () => {
- subs = null;
- };
-
- this.shouldComponentUpdate = function (_props) {
- if (this.props.value !== _props.value) {
- subs.forEach(c => {
- c._force = true;
- enqueueRender(c);
- });
+export function createContext(defaultValue) {
+ function Context(props) {
+ if (!this.getChildContext) {
+ /** @type {Set | null} */
+ let subs = new Set();
+ let ctx = {};
+ ctx[Context._id] = this;
+
+ this.getChildContext = () => ctx;
+
+ this.componentWillUnmount = () => {
+ subs = null;
+ };
+
+ this.shouldComponentUpdate = function (_props) {
+ // @ts-expect-error even
+ if (this.props.value !== _props.value) {
+ subs.forEach(c => {
+ c._force = true;
+ enqueueRender(c);
+ });
+ }
+ };
+
+ this.sub = c => {
+ subs.add(c);
+ let old = c.componentWillUnmount;
+ c.componentWillUnmount = () => {
+ if (subs) {
+ subs.delete(c);
}
+ if (old) old.call(c);
};
+ };
+ }
- this.sub = c => {
- subs.add(c);
- let old = c.componentWillUnmount;
- c.componentWillUnmount = () => {
- if (subs) {
- subs.delete(c);
- }
- if (old) old.call(c);
- };
- };
- }
+ return props.children;
+ }
- return props.children;
- }
+ Context._id = '__cC' + i++;
+ Context._defaultValue = defaultValue;
+
+ /** @type {import('./internal').FunctionComponent} */
+ Context.Consumer = (props, contextValue) => {
+ return props.children(contextValue);
};
- // Devtools needs access to the context object when it
- // encounters a Provider. This is necessary to support
- // setting `displayName` on the context object instead
- // of on the component itself. See:
- // https://reactjs.org/docs/context.html#contextdisplayname
+ // we could also get rid of _contextRef entirely
+ Context.Provider =
+ Context._contextRef =
+ Context.Consumer.contextType =
+ Context;
- return (context.Provider._contextRef = context.Consumer.contextType =
- context);
+ return Context;
}
diff --git a/src/index.d.ts b/src/index.d.ts
index 279af93ab2..8ab4570dc5 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -388,11 +388,12 @@ export type ContextType> = C extends Context
? T
: never;
-export interface Context {
- Consumer: Consumer;
- Provider: Provider;
+export interface Context extends preact.Provider {
+ Consumer: preact.Consumer;
+ Provider: preact.Provider;
displayName?: string;
}
+
export interface PreactContext extends Context {}
export function createContext(defaultValue: T): Context;
diff --git a/test/browser/createContext.test.js b/test/browser/createContext.test.js
index f5af75db58..41850aad90 100644
--- a/test/browser/createContext.test.js
+++ b/test/browser/createContext.test.js
@@ -57,6 +57,40 @@ describe('createContext', () => {
expect(scratch.innerHTML).to.equal('');
});
+ it('should pass context to a consumer (non-provider)', () => {
+ const Ctx = createContext(null);
+ const CONTEXT = { a: 'a' };
+
+ let receivedContext;
+
+ class Inner extends Component {
+ render(props) {
+ return {props.a}
;
+ }
+ }
+
+ sinon.spy(Inner.prototype, 'render');
+
+ render(
+
+
+
+ {data => {
+ receivedContext = data;
+ return ;
+ }}
+
+
+ ,
+ scratch
+ );
+
+ // initial render does not invoke anything but render():
+ expect(Inner.prototype.render).to.have.been.calledWithMatch(CONTEXT);
+ expect(receivedContext).to.equal(CONTEXT);
+ expect(scratch.innerHTML).to.equal('');
+ });
+
// This optimization helps
// to prevent a Provider from rerendering the children, this means
// we only propagate to children.
@@ -152,7 +186,8 @@ describe('createContext', () => {
it('should preserve provider context between different providers', () => {
const { Provider: ThemeProvider, Consumer: ThemeConsumer } =
createContext(null);
- const { Provider: DataProvider, Consumer: DataConsumer } = createContext(null);
+ const { Provider: DataProvider, Consumer: DataConsumer } =
+ createContext(null);
const THEME_CONTEXT = { theme: 'black' };
const DATA_CONTEXT = { global: 'a' };
diff --git a/test/ts/custom-elements.tsx b/test/ts/custom-elements.tsx
index c606d6a7f8..f943b7c930 100644
--- a/test/ts/custom-elements.tsx
+++ b/test/ts/custom-elements.tsx
@@ -41,7 +41,7 @@ interface WhateveElAttributes extends createElement.JSX.HTMLAttributes {
}
// Ensure context still works
-const { Provider, Consumer } = createContext({ contextValue: '' });
+const Ctx = createContext({ contextValue: '' });
// Sample component that uses custom elements
@@ -50,7 +50,7 @@ class SimpleComponent extends Component {
render() {
// Render inside div to ensure standard JSX elements still work
return (
-
+
{
@@ -73,13 +73,30 @@ class SimpleComponent extends Component {
>
{/* Ensure context still works */}
-
+
{({ contextValue }) => contextValue.toLowerCase()}
-
+
-
+
);
}
}
const component = ;
+class SimpleComponentWithContextAsProvider extends Component {
+ componentProp = 'componentProp';
+ render() {
+ // Render inside div to ensure standard JSX elements still work
+ return (
+
+
+ {/* Ensure context still works */}
+
+ {({ contextValue }) => contextValue.toLowerCase()}
+
+
+
+ );
+ }
+}
+const component2 = ;