Skip to content
12 changes: 9 additions & 3 deletions compat/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,21 @@ declare namespace React {
): T;

// Preact Defaults
export import Context = preact.Context;
export import ContextType = preact.ContextType;
export interface Context<T> extends preact.Provider<T> {
Consumer: preact.Consumer<T>;
Provider: preact.Provider<T>;
displayName?: string;
}
export function createContext<T>(defaultValue: T): Context<T>;
export type ContextType<C extends Context<any>> = C extends Context<infer T>
? T
: never;
export import RefObject = preact.RefObject;
export import Component = preact.Component;
export import FunctionComponent = preact.FunctionComponent;
export import ComponentType = preact.ComponentType;
export import ComponentClass = preact.ComponentClass;
export import FC = preact.FunctionComponent;
export import createContext = preact.createContext;
export import Ref = preact.Ref;
export import createRef = preact.createRef;
export import Fragment = preact.Fragment;
Expand Down
10 changes: 10 additions & 0 deletions compat/src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ function handleDomVNode(vnode) {

let oldVNodeHook = options.vnode;
options.vnode = vnode => {
// @ts-expect-error type can't be null, however TS is confused
if (
vnode.type != null &&
typeof vnode.type === 'object' &&
'Provider' in vnode.type
) {
// @ts-expect-error
vnode.type = vnode.type.Provider;
}

// only normalize props on Element nodes
if (typeof vnode.type === 'string') {
handleDomVNode(vnode);
Expand Down
34 changes: 34 additions & 0 deletions compat/test/browser/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>{props.a}</div>;
}
}

sinon.spy(Inner.prototype, 'render');

render(
<Context value={CONTEXT}>
<div>
<Context.Consumer>
{data => {
receivedContext = data;
return <Inner {...data} />;
}}
</Context.Consumer>
</div>
</Context>,
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('<div><div>a</div></div>');
});

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) {
Expand Down
23 changes: 23 additions & 0 deletions compat/test/ts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,26 @@ React.unmountComponentAtNode(document.body.shadowRoot!);
React.createPortal(<div />, document.createElement('div'));
React.createPortal(<div />, document.createDocumentFragment());
React.createPortal(<div />, 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 (
<Ctx value={{ contextValue: 'value' }}>
<div>
{/* Ensure context still works */}
<Ctx.Consumer>
{({ contextValue }) => contextValue.toLowerCase()}
</Ctx.Consumer>
</div>
</Ctx>
);
}
}

React.render(
<SimpleComponentWithContextAsProvider />,
document.createElement('div')
);
3 changes: 2 additions & 1 deletion test/browser/createContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,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' };

Expand Down
10 changes: 5 additions & 5 deletions test/ts/custom-elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -50,7 +50,7 @@ class SimpleComponent extends Component {
render() {
// Render inside div to ensure standard JSX elements still work
return (
<Provider value={{ contextValue: 'value' }}>
<Ctx.Provider value={{ contextValue: 'value' }}>
<div>
<clickable-ce
onClick={e => {
Expand All @@ -73,11 +73,11 @@ class SimpleComponent extends Component {
></custom-whatever>

{/* Ensure context still works */}
<Consumer>
<Ctx.Consumer>
{({ contextValue }) => contextValue.toLowerCase()}
</Consumer>
</Ctx.Consumer>
</div>
</Provider>
</Ctx.Provider>
);
}
}
Expand Down
Loading