{"id":17239114,"url":"https://github.com/binarymuse/isomorphic-fluxxor-experiment","last_synced_at":"2025-04-14T02:30:48.172Z","repository":{"id":25848439,"uuid":"29288078","full_name":"BinaryMuse/isomorphic-fluxxor-experiment","owner":"BinaryMuse","description":"Testing an isomorphic Fluxxor app","archived":false,"fork":false,"pushed_at":"2015-01-15T09:06:16.000Z","size":124,"stargazers_count":20,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-07T02:42:33.511Z","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":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BinaryMuse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-01-15T08:33:45.000Z","updated_at":"2021-02-02T06:20:00.000Z","dependencies_parsed_at":"2022-08-24T14:15:10.935Z","dependency_job_id":null,"html_url":"https://github.com/BinaryMuse/isomorphic-fluxxor-experiment","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BinaryMuse%2Fisomorphic-fluxxor-experiment","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BinaryMuse%2Fisomorphic-fluxxor-experiment/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BinaryMuse%2Fisomorphic-fluxxor-experiment/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BinaryMuse%2Fisomorphic-fluxxor-experiment/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BinaryMuse","download_url":"https://codeload.github.com/BinaryMuse/isomorphic-fluxxor-experiment/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248810875,"owners_count":21165191,"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-10-15T05:47:46.228Z","updated_at":"2025-04-14T02:30:48.130Z","avatar_url":"https://github.com/BinaryMuse.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Isomorphic Fluxxor\n==================\n\nThis is an application designed to test rendering an isomorphic React app powered by [Fluxxor](http://fluxxor.com/). It fetches some basic information from Reddit.\n\nRunning\n-------\n\nRequires Node.js installed.\n\n```\n$ npm install\n$ npm start\n```\n\nYou can set a different port with the `PORT` environment variable.\n\nOverview\n--------\n\nHere's how it works from a high level.\n\n\u003e Note: As a proof-of-concept, this app cuts some corners, skips some error checking, and short-circuits some best practices. Consult the docs for [React](http://facebook.github.io/react/), [react-router](https://github.com/rackt/react-router), and [Fluxxor](http://fluxxor.com/) for more information on the proper use of each.\n\n1. Components don't fetch data by dispatching actions; instead, they ask for it directly from the appropriate store using a getter on that store.\n\n    ```javascript\n    getStateFromFlux() {\n      return {\n        subredditData: subredditStore.getSubreddit(subreddit)\n      };\n    },\n    ```\n\n2. If the store has the data cached, it returns it immediately. Otherwise, it returns a \"loading token\" and starts an async fetch for the data.\n\n    ```javascript\n    getSubreddit(subreddit) {\n      if (this.state.subreddits[subreddit]) {\n        return this.state.subreddits[subreddit];\n      } else {\n        this.state.subreddits[subreddit] = LOADING_TOKEN;\n        this.reddit.getSubreddit(subreddit, (err, data) =\u003e {\n          // ...\n        });\n        return LOADING_TOKEN;\n      }\n    },\n    ```\n\n3. When the async fetch is complete, the store dispatches a \"success\" action to inform the system that the data is ready. It uses this action to update itself.\n\n    ```javascript\n    getSubreddit(subreddit) {\n      // ...\n\n        this.reddit.getSubreddit(subreddit, (err, data) =\u003e {\n          if (err) dispatch(FETCH_FAILURE, {subreddit: subreddit, err: err});\n          else     dispatch(FETCH_SUCCESS, {subreddit: subreddit, data: data});\n        });\n\n      // ...\n    },\n\n    handleFetchSuccess(payload) {\n      this.state.subreddits[payload.subreddit] = payload.data;\n      this.emit(\"change\");\n    }\n    ```\n\n4. The component that originally requested the data uses the loading token to determine if the data is ready or not.\n\n    ```javascript```\n    render() {\n      return (\n        \u003cdiv\u003e\n          \u003ch2\u003e/r/{this.state.name}\u003c/h2\u003e\n          {\n            this.state.subredditData === SubredditStore.LOADING_TOKEN ?\n            this.renderLoadingMessage() :\n            this.renderSubredditData()\n          }\n        \u003c/div\u003e\n      );\n    },\n    ```\n\n5. On the server, we inject a server-side version of the Reddit API into our store.\n\n    ```javascript\n    var reddit = new Reddit(ServerRedditFetcher);\n    var flux = Flux(reddit);\n    ```\n\n    This object emits events whenever it starts to fetch or finishes fetching a request from the Reddit API. We can use this information to know when we're done loading data asynchronously (because the the number of requests started minus the number of requests finished will be zero).\n\n    ```javascript\n    reddit.on(\"reqs\", (num) =\u003e {\n      if (num === 0) {\n        reddit.removeAllListeners();\n        render();\n      }\n    });\n    ```\n\n6. We do an *initial* server-side render to kick off the `getInitialState` calls, which start the async data requests flowing. Note that we don't do anything with the return value; we're only interested in the side effects that rendering the app has on our stores.\n\n    ```javascript\n    Router.run(Routes, req.url, (Handler, state) =\u003e {\n      React.renderToString(\n        \u003cHandler key={state.path} flux={flux} /\u003e\n      );\n    });\n    ```\n\n7. Once the Reddit API finishes the last request, we call `render()` (see above); here, we render the app *again*, and this time we send the React HTML and the serialized Fluxxor store data to the client.\n\n    ```javascript\n    var render = () =\u003e {\n      var serializedFlux = flux.serialize();\n      Router.run(Routes, req.url, (Handler, state) =\u003e {\n        var content = React.renderToString(\n          \u003cHandler key={state.path} flux={flux} /\u003e\n        );\n\n        res.render(\"index\", {\n          reactMarkup: content,\n          serializedFlux: serializedFlux\n        });\n      });\n    };\n    ```\n\n8. Our server-side view injects the HTML and serialized data into the page.\n\n    ```ejs\n    \u003cscript\u003e\n    window.fluxData = \u003c%- serializedFlux %\u003e;\n    \u003c/script\u003e\n    \u003cdiv id=\"app-container\"\u003e\u003c%- reactMarkup %\u003e\u003c/div\u003e\n    ```\n\n9. When the client-side application boots, we use the serialized store data to reconstruct the state from the server. We also use a different version of the Reddit API that works on the client.\n\n    ```javascript\n    var reddit = new Reddit(ClientRedditFetcher);\n\n    var flux = Flux(reddit);\n    if (window.fluxData) {\n      flux.hydrate(window.fluxData);\n    }\n\n    Router.run(Routes, Router.HistoryLocation, (Handler, state) =\u003e {\n      React.render(\n        \u003cHandler key={state.path} flux={flux} /\u003e,\n        document.getElementById(\"app-container\")\n      );\n    });\n    ```\n\n10. Since the flux state is the same on the client as it was on the server, React transparently \"upgrades\" our static markup into a proper single-page app. Any future asynchronous fetches are handled the same way they were on the server--by calling the getter on the store, which triggers an async fetch and an action as a result.\n\nConsiderations\n--------------\n\nThis seems to work well, and I like the declarative approach to getting appropriate component state from the stores. I haven't investigated the performance characteristics of rendering twice on the server. Additional care needs to be taken to ensure we don't leak if something goes wrong.\n\nOne situation where this naive approach will not work is when the availability of asynchronous data causes the second server-side render to include new components not included in the first render, and these new components also request async data from the stores. In such a case, you would need to repeat the loop that checks for outstanding requests until there are no more. An alternative approach, which I think I like, is to limit the fetching of async data to components that will be used by react-router as route handlers (thus ensuring they are triggered on the first server-side render).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinarymuse%2Fisomorphic-fluxxor-experiment","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbinarymuse%2Fisomorphic-fluxxor-experiment","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinarymuse%2Fisomorphic-fluxxor-experiment/lists"}