Skip to content

Instantly share code, notes, and snippets.

@slorber
Last active August 14, 2016 16:22
Show Gist options
  • Save slorber/18c0116040b56bcc95b8950b200d060b to your computer and use it in GitHub Desktop.
Save slorber/18c0116040b56bcc95b8950b200d060b to your computer and use it in GitHub Desktop.
A useful trick to share more easily React components across different applications

Intro

This pattern is for those of you that maintain multiple applications and need to build components that can be reused across all these apps. It won’t be useful if you build components meant to be reused in the same application.

Your typical app structure

If you use smart/dumb, or container/presentational components, which you should if you want your components to be at least a little reusable, your typical application might look like this:

const AppElement = (
  <App>
    <Container1>
      <Reusable1/>
      <Reusable2/>
      <ComplexReusableComponent>
        <Reusable3>
          <Reusable4>
            <VeryDeeplyNestedReusable/>
          </Reusable4>
        </Reusable3>
      </ComplexReusableComponent>
    </Container1>
    <Container2>
       ...
    <Container2>
  </App>
)

High-level and low-level reusable components

Some components, like ComplexReusableComponent are high-level, and compose multiple lower-level components, like VeryDeeplyNestedReusable.

These 2 kinds of components are supposed to be reused across multiple apps your are building.

Unfortunatly you have to change VeryDeeplyNestedReusable

A new requirement comes in, and you have to change the behavior of VeryDeeplyNestedReusable according to some data you hold in state of one of your app.

This is really annoying us, because this component is very deeply nested and used in multiple high-level components. By using props passing, you will likely need to modify a lot of components just to bring this little change.

To solve this, Redux has connect(), and it avoids you to pass the data your component need into every intermediate component.

But you have multiple applications!

Unfurtunatly, you don't have a single application, but multiple ones. Some of them use Redux, and some don't. Some might even use another legacy Flux framework. Some of them only use VeryDeeplyNestedReusable, and some of them also use high-level components like ComplexReusableComponent

You can't connect VeryDeeplyNestedReusable for all apps because it won't work for apps that don't have a Redux store in context. Even if they all had a store, the state shape might not be the same anyway...

But still, using props passing is really annoying you.

Painless solution

Inject an app identity into React context:

App1 = provide("app1")(App1);
App2 = provide("app2")(App2);
App3 = provide("app3")(App3);

Do component branching according to the context:

VeryDeeplyNestedReusable = branchByAppIdentity({
  "app1": VeryDeeplyNestedReusable,
  "app2": connect(state => state.someData)(VeryDeeplyNestedReusable),
  "app3": OtherFlux.injectData(VeryDeeplyNestedReusable),
});

It can also be useful, if you want the component to look very differently according to the context (can be compared to theming):

const VeryDeeplyNestedReusable = branchByAppIdentity({
  "app1": (props) => <div>Variation1</div>,
  "app2": (props) => <div>Variation2</div>,
  "app3": (props) => <div>Variation3</div>,
});

Not sure but I guess it could also be useful for ReactNative (I don't know RN):

const VeryDeeplyNestedReusable = branchByAppIdentity({
  "web": (props) => <div>Text web</div>,
  "ios": (props) => <View>Text iOS</View>,
  "android": (props) => <View>Text Android</View>,
});

Conclusion

Yes, by using this trick, your component became a bit less reusable.

The difference is that using something like Redux connect() couples the component to a specific app, while this trick only couples the component to your company. You can still use easily the component across multiple apps in the same company. If you are building a real product, and not a react-bootstrap lib, it should be fine!

///////////////////////////////////////////////////////////////////////////////
// LIB CODE
import React, {
Component,
PropTypes,
Children
} from 'react';
import _ from 'lodash';
// Some hardcoded app identities: you will have to bring your own apps here.
export const AppIdentities = [
"appLogged",
"appAnonymous",
"appiOS",
"appAndroid",
];
export const AppIdentityPropType = PropTypes.oneOf(AppIdentities);
export class Provider extends Component {
getChildContext() {
return {appIdentity: this.appIdentity}
}
constructor(props, context) {
super(props, context)
this.appIdentity = props.appIdentity
}
render() {
return Children.only(this.props.children)
}
}
Provider.propTypes = {
appIdentity: AppIdentityPropType.isRequired,
children: PropTypes.element.isRequired
};
Provider.childContextTypes = {
appIdentity: AppIdentityPropType.isRequired,
};
export const provide = appIdentity => Component => {
return (props) => (
<Provider appIdentity={appIdentity}>
<Component {...props}/>
</Provider>
)
};
export const inject = Component => {
return React.createClass({
contextTypes: {
appIdentity: AppIdentityPropType.isRequired,
},
render() {
return <Component {...this.props} appIdentity={this.context.appIdentity}/>
}
});
};
export const branchByAppIdentity = (config,defaultBranch = undefined) => {
const badKeys = _.difference(Object.keys(config),AppIdentities);
if ( badKeys.length > 0 ) {
throw new Error("bad app identities=" + badKeys);
}
if ( defaultBranch && !AppIdentities.includes(defaultBranch) ) {
throw new Error("bad default branch=" + defaultBranch);
}
const finalConfig = AppIdentities.reduce((acc,id) => {
acc[id] = (config[id] || defaultBranch);
return acc;
},{});
const missingKeys = Object.keys(finalConfig).filter(id => typeof finalConfig[id] === "undefined");
if ( missingKeys.length > 0 ) {
throw new Error("No config found for some branches="+missingKeys)
}
// At this point normally all branches have a value
return React.createClass({
contextTypes: {
appIdentity: AppIdentityPropType.isRequired,
},
render() {
const Component = finalConfig[this.context.appIdentity];
if ( !Component ) {
console.error("branch config",finalConfig);
throw new Error("No component could be found for branch="+this.context.appIdentity);
}
return <Component {...this.props} appIdentity={this.context.appIdentity}/>
}
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment