Skip to content

Commit

Permalink
Hacked-up version of Connect and Provider to use new context API
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Mar 9, 2018
1 parent 5d792a2 commit 576f3f2
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 26 deletions.
41 changes: 37 additions & 4 deletions src/components/Provider.js
@@ -1,8 +1,10 @@
import { Component, Children } from 'react'
import React, { Component, Children } from 'react'
import PropTypes from 'prop-types'
import { storeShape, subscriptionShape } from '../utils/PropTypes'
import warning from '../utils/warning'

import {ReactReduxContext} from "./context";

let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
if (didWarnAboutReceivingStore) {
Expand All @@ -20,20 +22,48 @@ function warnAboutReceivingStore() {
}

export function createProvider(storeKey = 'store', subKey) {
const subscriptionKey = subKey || `${storeKey}Subscription`
//const subscriptionKey = subKey || `${storeKey}Subscription`

class Provider extends Component {
/*
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
*/

constructor(props, context) {
super(props, context)
this[storeKey] = props.store;
//this[storeKey] = props.store;

const {store} = props;

if(!store || !store.getState || !store.dispatch) {
throw new Error("Must pass a valid Redux store as a prop to Provider");
}

this.state = {
storeState : store.getState(),
dispatch : store.dispatch,
};
}

componentDidMount() {
const {store} = this.props;

this.unsubscribe = store.subscribe( () => {
console.log("Provider subscription running");
this.setState({storeState : store.getState()});
});
}

render() {
return Children.only(this.props.children)
console.log("Provider re-rendering");

return (
<ReactReduxContext.Provider value={this.state}>
{Children.only(this.props.children)}
</ReactReduxContext.Provider>
);
}
}

Expand All @@ -45,14 +75,17 @@ export function createProvider(storeKey = 'store', subKey) {
}
}


Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired,
}
/*
Provider.childContextTypes = {
[storeKey]: storeShape.isRequired,
[subscriptionKey]: subscriptionShape,
}
*/

return Provider
}
Expand Down
119 changes: 97 additions & 22 deletions src/components/connectAdvanced.js
@@ -1,19 +1,20 @@
import hoistStatics from 'hoist-non-react-statics'
import invariant from 'invariant'
import { Component, createElement } from 'react'
import React, { Component, createElement } from 'react'

import Subscription from '../utils/Subscription'
import {ReactReduxContext} from "./context";
import { storeShape, subscriptionShape } from '../utils/PropTypes'

let hotReloadingVersion = 0
const dummyState = {}
function noop() {}
function makeSelectorStateful(sourceSelector, store) {
function makeSelectorStateful(sourceSelector) {
// wrap the selector in an object that tracks its results between runs.
const selector = {
run: function runComponentSelector(props) {
run: function runComponentSelector(props, storeState) {
try {
const nextProps = sourceSelector(store.getState(), props)
const nextProps = sourceSelector(storeState, props)
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps
Expand Down Expand Up @@ -78,17 +79,19 @@ export default function connectAdvanced(
const subscriptionKey = storeKey + 'Subscription'
const version = hotReloadingVersion++

/*
const contextTypes = {
[storeKey]: storeShape,
[subscriptionKey]: subscriptionShape,
}
const childContextTypes = {
[subscriptionKey]: subscriptionShape,
}
*/

return function wrapWithConnect(WrappedComponent) {
invariant(
typeof WrappedComponent == 'function',
typeof WrappedComponent === 'function',
`You must pass a component to the function returned by ` +
`${methodName}. Instead received ${JSON.stringify(WrappedComponent)}`
)
Expand Down Expand Up @@ -117,22 +120,30 @@ export default function connectAdvanced(
super(props, context)

this.version = version
this.state = {}
//this.state = {}
this.renderCount = 0
this.store = props[storeKey] || context[storeKey]
this.propsMode = Boolean(props[storeKey])
//this.store = props[storeKey] || context[storeKey]
//this.propsMode = Boolean(props[storeKey])

this.storeState = null;


this.setWrappedInstance = this.setWrappedInstance.bind(this)
this.renderChild = this.renderChild.bind(this);

/*
invariant(this.store,
`Could not find "${storeKey}" in either the context or props of ` +
`"${displayName}". Either wrap the root component in a <Provider>, ` +
`or explicitly pass "${storeKey}" as a prop to "${displayName}".`
)
*/

this.initSelector()
this.initSubscription()
//this.initSelector()
//this.initSubscription()
}

/*
getChildContext() {
// If this component received store from props, its subscription should be transparent
// to any descendants receiving store+subscription from context; it passes along
Expand All @@ -141,6 +152,7 @@ export default function connectAdvanced(
const subscription = this.propsMode ? null : this.subscription
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
*/

componentDidMount() {
if (!shouldHandleStateChanges) return
Expand All @@ -151,24 +163,28 @@ export default function connectAdvanced(
// To handle the case where a child component may have triggered a state change by
// dispatching an action in its componentWillMount, we have to re-run the select and maybe
// re-render.
this.subscription.trySubscribe()
this.selector.run(this.props)
//this.subscription.trySubscribe()
this.selector.run(this.props, this.storeState);
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}

componentWillReceiveProps(nextProps) {
this.selector.run(nextProps)

UNSAFE_componentWillReceiveProps(nextProps) {
this.selector.run(nextProps, this.storeState);
}



shouldComponentUpdate() {
return this.selector.shouldComponentUpdate
}


componentWillUnmount() {
if (this.subscription) this.subscription.tryUnsubscribe()
this.subscription = null
//if (this.subscription) this.subscription.tryUnsubscribe()
//this.subscription = null
this.notifyNestedSubs = noop
this.store = null
//this.store = null
this.selector.run = noop
this.selector.shouldComponentUpdate = false
}
Expand All @@ -185,13 +201,22 @@ export default function connectAdvanced(
this.wrappedInstance = ref
}

/*
initSelector() {
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
this.selector = makeSelectorStateful(sourceSelector, this.store)
this.selector.run(this.props)
}
*/

initSelector(dispatch, storeState) {
const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions)
this.selector = makeSelectorStateful(sourceSelector)
this.selector.run(this.props, storeState);
}

initSubscription() {
/*
if (!shouldHandleStateChanges) return
// parentSub's source should match where store came from: props vs. context. A component
Expand All @@ -206,6 +231,7 @@ export default function connectAdvanced(
// listeners logic is changed to not call listeners that have been unsubscribed in the
// middle of the notification loop.
this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
*/
}

onStateChange() {
Expand All @@ -229,23 +255,69 @@ export default function connectAdvanced(
this.notifyNestedSubs()
}

/*
isSubscribed() {
return Boolean(this.subscription) && this.subscription.isSubscribed()
}
*/

addExtraProps(props) {
if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props
//if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props
if (!withRef && !renderCountProp) return props;


// make a shallow copy so that fields added don't leak to the original selector.
// this is especially important for 'ref' since that's a reference back to the component
// instance. a singleton memoized selector would then be holding a reference to the
// instance, preventing the instance from being garbage collected, and that would be bad
const withExtras = { ...props }
if (withRef) withExtras.ref = this.setWrappedInstance
if (renderCountProp) withExtras[renderCountProp] = this.renderCount++
if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription
//if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription
return withExtras
}

renderChild(providerValue) {
const {storeState, dispatch} = providerValue;

this.storeState = storeState;

//console.log(`Running renderChild (${displayName})`, storeState, this.props);

if(this.selector) {
this.selector.run(this.props, storeState);
}
else {
this.initSelector(dispatch, storeState);
}



if (this.selector.error) {
throw this.selector.error
}
else if(this.selector.shouldComponentUpdate) {
console.log(`Re-rendering component (${displayName})`, this.selector.props);
this.selector.shouldComponentUpdate = false;
this.renderedElement = createElement(WrappedComponent, this.addExtraProps(this.selector.props));
}
else {
//console.log(`Returning existing render result (${displayName})`, this.props)
}

return this.renderedElement;
}

render() {
return (
<ReactReduxContext.Consumer>
{this.renderChild}
</ReactReduxContext.Consumer>
)
}

/*
render() {
const selector = this.selector
selector.shouldComponentUpdate = false
Expand All @@ -256,14 +328,16 @@ export default function connectAdvanced(
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
*/
}

Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
Connect.childContextTypes = childContextTypes
Connect.contextTypes = contextTypes
Connect.propTypes = contextTypes
//Connect.childContextTypes = childContextTypes
//Connect.contextTypes = contextTypes
//Connect.propTypes = contextTypes

/*
if (process.env.NODE_ENV !== 'production') {
Connect.prototype.componentWillUpdate = function componentWillUpdate() {
// We are hot reloading!
Expand All @@ -290,6 +364,7 @@ export default function connectAdvanced(
}
}
}
*/

return hoistStatics(Connect, WrappedComponent)
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/context.js
@@ -0,0 +1,3 @@
import React from "react";

export const ReactReduxContext = React.createContext(null);

2 comments on commit 576f3f2

@naholyr
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm scratching my head on this one as I was trying to use new Context API to write redux-like Provider and connect(), but I think it can just not work with server-side rendering :(

The most important thing is that one root component == one store, so you can call createStore for each request and render your component with given store in the tree, without fearing for race conditions between two independent requests which would use the same data.

That was OK with old Context API, as it was attached to the actual tree. Now it's attached to an external object we're doomed: in your current implementation (and I could not find better idea), you have only one global context, so if you have two trees, using <Provider store={store}>…, with two different stores, you will face race conditions as the first tree will suddenly be udpated by the second one.

It will work now, because between createElement() and renderToString() everything is synchronous, but that smells bad :(

Do you have any idea how to solve this?

@timdorr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@naholyr Nothing changes on your end. The same APIs exist for getting your per-request store into the component tree (renderToString(<Provider store={store}/>...). This doesn't affect server rendering at all.

Please sign in to comment.