{"id":18627092,"url":"https://github.com/markshust/hymn-composer","last_synced_at":"2025-11-04T01:30:31.199Z","repository":{"id":57269900,"uuid":"65516371","full_name":"markshust/hymn-composer","owner":"markshust","description":"Compose React containers and feed data into components.","archived":false,"fork":false,"pushed_at":"2016-08-12T02:58:03.000Z","size":434,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-09T16:39:10.100Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/markshust.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-12T02:23:02.000Z","updated_at":"2017-07-24T21:00:06.000Z","dependencies_parsed_at":"2022-08-26T13:11:51.987Z","dependency_job_id":null,"html_url":"https://github.com/markshust/hymn-composer","commit_stats":null,"previous_names":["markoshust/hymn-composer"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markshust%2Fhymn-composer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markshust%2Fhymn-composer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markshust%2Fhymn-composer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markshust%2Fhymn-composer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markshust","download_url":"https://codeload.github.com/markshust/hymn-composer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239425310,"owners_count":19636346,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T04:40:13.984Z","updated_at":"2025-11-04T01:30:31.133Z","avatar_url":"https://github.com/markshust.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hymn-composer\n\nLet's compose React containers and feed data into components. \u003cbr\u003e\n(supports ReactNative as well)\n\nThe base of this project stems from \u003ca href=\"https://github.com/kadirahq/react-komposer\"\u003ereact-komposer\u003c/a\u003e. It is not intended to be backwards-compatible, as it will introduce breaking updates \u0026 new features.\n\n## Differences from react-komposer\n\n* Few bugfixes\n\n## TOC\n\n* [Why](#why)\n* [Installation](#installation)\n* [Basic Usage](#basic-usage)\n* [API](#api)\n* [Using with XXX](#using-with-xxx)\n    - [Using with Promises](#using-with-promises)\n    - [Using with Meteor](#using-with-meteor)\n    - [Using with Rx.js Observables](#using-with-rxjs-observables)\n    - [Using with Redux](#using-with-redux)\n* [Extending](#extending)\n* [Stubbing](#stubbing)\n* [Caveats](#caveats)\n\n## Why?\n\nLately, in React we try to avoid the use of component state as much as possible and use props to handle the passing of data and actions.\nWe call these stateless components **Dumb Components** or **UI Components.**\n\nThere is another component layer on-top of these **Dumb Components** which handles the **data fetching** logic. We call these components **Containers**. Containers usually do things like:\n\n* Request for data (invoke a subscription or just fetch it).\n* Show a loading screen while the data is fetching.\n* Once data arrives, pass it to the UI Component.\n* If there is an error, show it to the user.\n* It may need to re-fetch or re-subscribe when props change.\n* It needs to cleanup resources (like subscriptions) when the container is unmounting.\n\nIf you want to do these yourself, you have to do a lot of **repetitive tasks**. This commonly leads to **human errors**.\n\n**Meet Hymn Composer**\n\nThat's what we are going to fix with this project. You simply tell it how to get data and clean up resources, then it'll\ndo the hard work for you. This is a universal project and works with **any kind of data source**, whether it's based on\nPromises, Rx.JS observables, a Redux store or even Meteor's Tracker.\n\n## Installation\n\n```\nnpm i --save hymn-composer\n```\n\n## Basic Usage\n\nLet's say we need to build a clock. First let's create a UI Component to show the time.\n\n```js\nconst Time = ({time}) =\u003e (\u003cdiv\u003eTime is: {time}\u003c/div\u003e);\n```\n\nNow let's define how to fetch data for this:\n\n```js\nconst onPropsChange = (props, onData) =\u003e {\n  const handle = setInterval(() =\u003e {\n    const time = (new Date()).toString();\n    onData(null, {time});\n  }, 1000);\n\n  const cleanup = () =\u003e clearInterval(handle);\n  return cleanup;\n};\n```\n\nIn the above function, we get new data every second and send it via the `onData` callback. Additionally, we return a cleanup function from the function to cleanup its resources.\n\nOkay. Now it's time to create the clock Container, wrapped around our Time UI Component, using our `onPropsChange` function:\n\n```js\nimport { compose } from 'hymn-composer';\nconst Clock = compose(onPropsChange)(Time);\n```\n\nThat's it. Now render the clock to the DOM.\n\n```js\nimport ReactDOM from 'react-dom';\nReactDOM.render(\u003cClock /\u003e, document.body);\n```\n\nSee this live: \u003chttps://jsfiddle.net/arunoda/jxse2yw8\u003e\n\n### Additional Benefits\n\nOther than the main benefits, now it's super easy to test our UI code. We can easily do it via a set of unit tests.\n\n* For the UI Components, simply test the plain React component. In this case, `Time` (you can use [enzyme](https://github.com/airbnb/enzyme)).\n* Then test `onPropsChange` for different scenarios.\n\n## API\n\nYou can customize the higher order component created by `compose` in few ways. Let's discuss.\n\n### Handling Errors\n\nRather than showing the data, sometimes you need to deal with errors. Here's how to use `compose` for that:\n\n```js\nconst onPropsChange = (props, onData) =\u003e {\n  // oops some error.\n  onData(new Error('Oops'));\n};\n```\n\nThe error will be rendered to the screen (in place of where the component is rendered).\nYou must provide a [JavaScript Error object](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Error).\n\nYou can clear it by passing some data again like this:\n\n```js\nconst onPropsChange = (props, onData) =\u003e {\n  // oops some error.\n  onData(new Error('Oops'));\n\n  setTimeout(() =\u003e {\n    onData(null, {time: Date.now()});\n  }, 5000);\n};\n```\n\n### Detect props changes\n\nSometimes you can use the props to customize our data fetching logic. Here's how to do it:\n\n```js\nconst onPropsChange = (props, onData) =\u003e {\n  const handle = setInterval(() =\u003e {\n    const time = (props.timestamp)? Date.now() : (new Date()).toString();\n    onData(null, {time});\n  }, 1000);\n\n  const cleanup = () =\u003e clearInterval(handle);\n  return cleanup;\n};\n```\n\nHere we are asking to make the Clock to display timestamp instead of the Date string. See:\n\n```js\nReactDOM.render((\n  \u003cdiv\u003e\n    \u003cClock timestamp={true}/\u003e\n    \u003cClock /\u003e\n  \u003c/div\u003e\n), document.body);\n```\n\nSee this live: \u003chttps://jsfiddle.net/arunoda/7qy1mxc7/\u003e\n\n### Change the Loading Component\n\n```js\nconst MyLoading = () =\u003e (\u003cdiv\u003eHmm...\u003c/div\u003e);\nconst Clock = compose(onPropsChange, MyLoading)(Time);\n```\n\n\u003e This custom loading component receives all the props passed to the UI Component as well.\n\u003e So, based on those props, you can change the behaviour of the loading component also.\n\n### Change the Error Component\n\n```js\nconst MyError = ({error}) =\u003e (\u003cdiv\u003eError: {error.message}\u003c/div\u003e);\nconst Clock = compose(onPropsChange, null, MyError)(Time);\n```\n\n### Compose Multiple Containers\n\nSometimes we need to compose multiple containers at once, in order to use different data sources. Checkout the following examples:\n\n```js\nconst Clock = composeWithObservable(composerFn1)(Time);\nconst MeteorClock = composeWithTracker(composerFn2)(Clock);\n\nexport default MeteorClock;\n```\n\nFor the above case, we've a utility called `composeAll` to make our life easier. To use it:\n\n```js\nexport default composeAll(\n  composeWithObservable(composerFn1),\n  composeWithTracker(composerFn2)\n)(Time)\n```\n\n### Pure Containers\n\n`hymn-composer` checks the purity of payload, error and props, avoiding unnecessary render function calls. That means\nwe've implemented the `shouldComponentUpdate` lifecycle method, which follows something similar to React's [shallowCompare](https://facebook.github.io/react/docs/shallow-compare.html).\n\nIf you need to turn off this functionality, you can do it like this:\n\n```js\n// You can use `composeWithPromise` or any other compose APIs\n// instead of `compose`.\nconst Clock = compose(onPropsChange, null, null, {pure: false})(Time);\n```\n\n### Ref to base component\n\nIn some situations, you need to get a ref to the base component that you pass to `hymn-composer`. You can enable a `ref` with the `withRef` option:\n\n```js\n// You can use `composeWithPromise` or any other compose APIs\n// instead of `compose`.\nconst Clock = compose(onPropsChange, null, null, {withRef: true})(Time);\n```\n\nThe base component will then be accessible with `getWrappedInstance()`. \u003cbr/\u003e\nCheckout this [test case](https://github.com/kadirahq/react-komposer/blob/master/lib/__tests__/compose.js#L90) for a proper example.\n\n### Change Default Components\n\nIt is possible to change default error and loading components globally, so you don't need to set default components in every composer call.\n\nHere's how do it:\n\n```js\nimport {\n  setDefaultErrorComponent,\n  setDefaultLoadingComponent,\n} from 'hymn-composer';\n\nconst ErrorComponent = () =\u003e (\u003cdiv\u003eMy Error\u003c/div\u003e);\nconst LoadingComponent = () =\u003e (\u003cdiv\u003eMy Loading\u003c/div\u003e);\n\nsetDefaultErrorComponent(ErrorComponent);\nsetDefaultLoadingComponent(LoadingComponent);\n```\n\n\u003e This is very important if you are using this in a React Native app,\n\u003e as this project has no default components for React Native.\n\u003e So you can set default components, as shown above, at the very beginning.\n\n## Using with XXX\n\n### Using with Promises\n\nFor use with Promise-based data sources, you can use `composeWithPromise` instead of `compose`.\n\n```js\nimport {composeWithPromise} from 'hymn-composer'\n\n// Create a component to display Time\nconst Time = ({time}) =\u003e (\u003cdiv\u003e{time}\u003c/div\u003e);\n\n// Assume this get's the time from the Server\nconst getServerTime = () =\u003e {\n  return new Promise((resolve) =\u003e {\n    const time = new Date().toString();\n    setTimeout(() =\u003e resolve({time}), 2000);\n  });\n};\n\n// Create the composer function and tell how to fetch data\nconst composerFunction = (props) =\u003e {\n  return getServerTime();\n};\n\n// Compose the container\nconst Clock = composeWithPromise(composerFunction)(Time, Loading);\n\n// Render the container\nReactDOM.render(\u003cClock /\u003e, document.getElementById('react-root'));\n```\n\nSee this live: \u003chttps://jsfiddle.net/arunoda/8wgeLexy/\u003e\n\n### Using with Meteor\n\nFor use with Meteor, you need to use `composeWithTracker` instead of `compose`, from where you can watch any Reactive data.\n\n```js\nimport {composeWithTracker} from 'hymn-composer';\nimport PostList from '../components/post_list.jsx';\n\nfunction composer(props, onData) {\n  if (Meteor.subscribe('posts').ready()) {\n    const posts = Posts.find({}, {sort: {_id: 1}}).fetch();\n    onData(null, {posts});\n  };\n};\n\nexport default composeWithTracker(composer)(PostList);\n```\n\nIn addition to above, you can also return a cleanup function from the composer function. See the following example:\n\n```js\nimport {composeWithTracker} from 'hymn-composer';\nimport PostList from '../components/post_list.jsx';\n\nconst composerFunction = (props, onData) =\u003e {\n  // tracker related code\n  return () =\u003e {console.log('Container disposed!');}\n};\n\n// Note the use of composeWithTracker\nconst Container = composeWithTracker(composerFunction)(PostList);\n```\n\nFor more information, refer this article: [Using Meteor Data and React with Meteor 1.3](https://voice.kadira.io/using-meteor-data-and-react-with-meteor-1-3-13cb0935dedb)\n\n\n### Using with Rx.js Observables\n\n```js\nimport {composeWithObservable} from 'hymn-composer'\n\n// Create a component to display Time\nconst Time = ({time}) =\u003e (\u003cdiv\u003e{time}\u003c/div\u003e);\n\nconst now = Rx.Observable.interval(1000)\n  .map(() =\u003e ({time: new Date().toString()}));\n\n// Create the composer function and tell how to fetch data\nconst composerFunction = (props) =\u003e now;\n\n// Compose the container\nconst Clock = composeWithObservable(composerFunction)(Time);\n\n// Render the container\nReactDOM.render(\u003cClock /\u003e, document.getElementById('react-root'));\n```\n\nTry this live: \u003chttps://jsfiddle.net/arunoda/Lsdekh4y/\u003e\n\n### Using with Redux\n\n```js\n\nconst defaultState = {time: new Date().toString()};\nconst store = Redux.createStore((state = defaultState, action) =\u003e {\n  switch(action.type) {\n    case 'UPDATE_TIME':\n      return {\n        ...state,\n        time: action.time\n      };\n    default:\n      return state;\n  }\n});\n\nsetInterval(() =\u003e {\n  store.dispatch({\n    type: 'UPDATE_TIME',\n    time: new Date().toString()\n  });\n}, 1000);\n\n\nconst Time = ({time}) =\u003e (\u003cdiv\u003e\u003cb\u003eTime is\u003c/b\u003e: {time}\u003c/div\u003e);\n\nconst onPropsChange = (props, onData) =\u003e {\n  onData(null, {time: store.getState().time});\n  return store.subscribe(() =\u003e {\n    const {time} = store.getState();\n    onData(null, {time})\n  });\n};\n\nconst Clock = compose(onPropsChange)(Time);\n\nReactDOM.render(\u003cClock /\u003e, document.getElementById('react'))\n```\n\nTry this live: \u003chttps://jsfiddle.net/arunoda/wm6romh4/\u003e\n\n### Using with MobX\n\n```js\nconst store = mobx.observable({time: new Date().toString()});\n\nsetInterval(() =\u003e store.time = new Date().toString(), 1000);\n\nconst Time = ({time}) =\u003e (\u003cdiv\u003e\u003cb\u003eTime is\u003c/b\u003e: {time}\u003c/div\u003e);\n\nconst onPropsChange = (props, onData) =\u003e {\n  const {time} = store;\n  onData(null, {time});\n};\n\nconst Clock = composeWithMobx(onPropsChange)(Time);\n\nReactDOM.render(\u003cClock /\u003e, document.getElementById('react'));\n```\n\n## Extending\n\nContainers built by Hymn Composer are still React components. This means that they can be extended in the same way\nyou would extend any other component. This is demonstrated in the following example:\n\n\n```js\nconst Tick = compose(onPropsChange)(Time);\nclass Clock extends Tick {\n  componentDidMount() {\n    console.log('Clock started');\n\n    return super();\n  }\n  componentWillUnmount() {\n    console.log('Clock stopped');\n\n    return super();\n  }\n};\nClock.displayName = 'ClockContainer';\n\nexport default Clock;\n```\n\nRemember to call `super` when overriding methods already defined in the Container.\n\n## Stubbing\n\nIt's very important to stub Containers used with `hymn-composer` when we are doing isolated UI testing (especially with\n[react-storybook](https://github.com/kadirahq/react-storybook)). Here's how you can stub composers:\n\n**First of all, this only works if you are using the `composeAll` utility function.**\n\nAt the very beginning of your initial JS file, set the following code:\n\n```js\nimport { setStubbingMode } from 'hymn-composer';\nsetStubbingMode(true);\n```\n\n\u003e In react-storybook, that's in the `.storybook/config.js` file.\n\nThen all your containers will look like this:\n\n![With no stub](docs/with-no-stub.png)\n\nIf you need, you can set a stub composer and pass data to the original component, bypassing the actual composer function.\nYou can do this, before using the component which has the Container.\n\n```js\nimport { setComposerStub } from 'hymn-composer';\nimport CommentList from '../comment_list';\nimport CreateComment from '../../containers/create_comment';\n\n// Create the stub for the composer.\nsetComposerStub(CreateComment, (props) =\u003e {\n  const data = {\n    ...props,\n    create: () =\u003e {},\n  };\n\n  return data;\n});\n```\n\n\u003e In react-storybook, you can do this when you are writing stories.\n\nHere, the `CreateComment` container is being used inside the `CommentList` Container. We simply set a stubComposer,\nwhich returns some data. That data will be passed as props to the original UI Component, wrapped by the `CreateComment` Container.\n\nThis is how it looks after using the stub:\n\n![With stub](docs/with-stub.png)\n\nYou can see a real example in the [Mantra sample blog app](https://github.com/mantrajs/mantra-sample-blog-app).\n\n## Caveats\n\n**SSR**\n\nOn the server, we won't be able to cleanup resources even if you return the cleanup function. That's because there is no\nfunctionality to detect component unmount on the server. So make sure to handle the cleanup logic by yourself, in this case.\n\n**Composer re-run on any prop change**\n\nRight now, the composer function will run again for any prop change. We can fix this by watching props and deciding which\nprop has been changed. See [#4](https://github.com/kadirahq/hymn-composer/issues/4).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkshust%2Fhymn-composer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkshust%2Fhymn-composer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkshust%2Fhymn-composer/lists"}