Intended outcome:
I'm using the new React Context API to keep a "tree path" and that value is used to choose which components will be rendered. Some of them may have graphql queries that should run on server side. That was working as intended using the old React Context API.
Actual outcome:
getDataFromTree is messing up context values and that is impacting directly on my server side rendering.
I think it is related to #2135.
How to reproduce the issue:
https://github.com/brunoabreu/getDataFromTree-context-bug
This repo has this Node component that is a context consumer and provider at same time.
const App = (
<Node id='root'>
<Node id='one'>
<Node id='a' />
<Node id='b' />
</Node>
<Node id='two'>
<Node id='a' />
<Node id='b' />
</Node>
</Node>
)
It works as expected with ReactDom.renderToStaticMarkup, but not with getDataFromTree:
without getDataFromTree:
ID: root | PARENT:
ID: one | PARENT: root
ID: a | PARENT: root/one
ID: b | PARENT: root/one
ID: two | PARENT: root
ID: a | PARENT: root/two
ID: b | PARENT: root/two
during getDataFromTree:
ID: root | PARENT:
ID: one | PARENT: root
ID: a | PARENT: root/one
ID: b | PARENT: root/one/a
ID: two | PARENT: root/one/a/b
ID: a | PARENT: root/one/a/b/two
ID: b | PARENT: root/one/a/b/two/a
Version
React Context API creates special VNodes, which does not provide children, coz of context variables only available at runtime. Please use context api legacy code:
import * as React from 'react';
import * as PropTypes from 'prop-types';
// import uuid from 'some-super-uuid-lib';
interface IProviderMiddlewareProps {
value: any;
}
interface IConsumerMiddlewareProps {
value: any;
children(context: any): React.ReactNode;
}
function createProviderMiddleware(id: string, key: string): React.ComponentClass<IProviderMiddlewareProps> {
return class ProviderMiddleware extends React.Component<IProviderMiddlewareProps> {
public static dislpayName = `ProviderMiddleware#${id}`;
public static childContextTypes = {
[key]: PropTypes.any,
}
getChildContext() {
return {
[key]: this.props.value,
};
}
public render() {
return this.props.children;
}
}
}
function createConsumerMiddleware(id: string, key: string): React.ComponentClass<IConsumerMiddlewareProps> {
return class ConsumerMiddleware extends React.Component<IConsumerMiddlewareProps> {
public static dislpayName = `ConsumerMiddleware#${id}`;
public static contextTypes = {
[key]: PropTypes.any,
}
context: {
[key: string]: any;
} | undefined;
public render() {
const context = this.context;
if (typeof context === 'object' && null != context) {
if (context.hasOwnProperty(key)) {
return this.props.children(context[key]);
}
}
return this.props.children(this.props.value);
}
}
}
export interface IProviderProps<T> {
value: T;
}
export interface IConsumerProps<T> {
children(value: T): React.ReactNode;
}
export function createContext<T>(defaultValue: T) {
const id = uuid();
const key = `context-${id}-${uuid()}`;
const ProviderMiddleware = createProviderMiddleware(id, key);
// protect Provider childContextTypes
const Provider: React.ComponentType<IProviderProps<T>> = props => (
<ProviderMiddleware value={props.value}>
{props.children}
</ProviderMiddleware>
);
Provider.displayName = `Provider#${id}`;
// protect Consumer contextTypes
const ConsumerMiddleware = createConsumerMiddleware(id, key);
const Consumer: React.ComponentType<IConsumerProps<T>> = props => (
<ConsumerMiddleware value={defaultValue}>
{props.children}
</ConsumerMiddleware>
);
Consumer.displayName = `Consumer#${id}`;
return Object.freeze({
Provider,
Consumer,
});
}
export default createContext;
````
```typescript
import createContext from './path-to-create-context-api';
const { Provider, Consumer } = createContext('this is a default value');
function App() {
return (
<Provider value={'new value'}>
<Consumer>
{value => value}
</Consumer>
</Provider>
);
} // will render 'new value'
I hope it will help you. Good luck!
I'm doing that right now (relying on old context API just for getDataFromTree).
<TreePathContext.Consumer>
{
context => {
// get treePath from old react context during apollo's getDataFromTree
// it is buggy when dealing with contexts that are overwritten, like treePath
const treePath = __APOLLO_SSR__ ? this.context.treePath : context.treePath
return <Component {...this.props} treePath={treePath}/>
}
}
</TreePathContext.Consumer>
But I was looking forward to remove this hack and use just the new context API.
I'm also worried because the old context API will be removed eventually. I hope getDatafromTree will be working properly when it happens.
In my case react-apollo walkTree function (https://github.com/apollographql/react-apollo/blob/85e8b3fde7a7e4f6c8ef2d55b12f7a9525476a2c/src/getDataFromTree.ts#L46) doesn't visit any node that wrapped with <SomeContext.Consumer>.
I found out that walkTree function doesn't visit nodes wrapped with React.forwardRef(...) too.
@hwillson https://github.com/apollographql/react-apollo/issues/2139#issuecomment-403987580 is a separate issue, right? I don't see any new logic in #2304 to handle React.forwardRef
@hwillson Any updates on React.forwardRef?
I just created separated issue for bug related to React.forwardRef: #2512
Most helpful comment
I found out that
walkTreefunction doesn't visit nodes wrapped withReact.forwardRef(...)too.