├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── ComponentData.js ├── Resolver.js ├── resolveRecursive.js ├── resolveSimple.js └── script.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | lib 5 | index.js 6 | recursive.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gragland/react-component-data/788397f133e4d327e074689f17b3f8e47abdaf2e/.npmignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Gabe Ragland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍯 React Component Data 2 | 3 | Data fetching for server-rendered React applications. Components declare the data they'd like to receive as props. Plays nicely with React Router. 4 | 5 | ## Usage 6 | 7 | Install the package: 8 | 9 | ```bash 10 | $ npm install react-component-data --save 11 | ``` 12 | 13 | ### Component 14 | 15 | The only change you need to make to your component is to give it a `getData()` static method which returns an object containing the props that it would like to receive. On server-side render (and subsequent client-side render/re-hydration) your component will get those props on its initial mount. If a component is not rendered server-side its props will be fetched asyncronously and you can manage your component's loading state however you wish. 16 | 17 | 18 | ```jsx 19 | import React from 'react'; 20 | 21 | class Component extends React.Component { 22 | 23 | static async getData(){ 24 | const response = await fetchGithubRepos('gragland'); 25 | return { projects: response.data }; 26 | } 27 | 28 | static defaultProps = { projects: null }; 29 | 30 | render() { 31 | const projects = this.props.projects; 32 | 33 | { projects ? 34 | 35 | : 36 | 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | 43 | If you don't use async/await just return a Promise. 44 | ```jsx 45 | static getData(){ 46 | return new Promise((resolve, reject) => { 47 | fetchGithubRepos('gragland') 48 | .then((response) => { 49 | resolve({ projects: response.data }); 50 | }); 51 | }); 52 | } 53 | ``` 54 | We'll continue to use async/await in our examples but keep in mind that you can use standard Promises instead. 55 | 56 | 57 | ### Server 58 | On the server-side you use `resolve()` to fetch your component's data and then pass the resulting data object as a prop to ``. 59 | 60 | ```jsx 61 | import React from 'react'; 62 | import { renderToStaticMarkup, renderToString } from 'react-dom/server'; 63 | import ComponentData, { resolve } from 'react-component-data'; 64 | import Layout from './Layout.js'; 65 | import App from './App.js'; 66 | 67 | express() 68 | .get('/', async function (req, res) { 69 | 70 | const data = await resolve(App); 71 | 72 | const body = renderToString( 73 | 74 | 75 | 76 | ); 77 | 78 | res.send( 79 | renderToStaticMarkup( ) 80 | ); 81 | 82 | }).listen( ... ); 83 | ``` 84 | ### Server (w/ React Router) 85 | 86 | ```jsx 87 | import React from 'react'; 88 | import { renderToStaticMarkup, renderToString } from 'react-dom/server'; 89 | import { match, RouterContext } from 'react-router'; 90 | import ComponentData, { resolve } from 'react-component-data'; 91 | import routes from './routes.js'; 92 | import Layout from './Layout.js'; 93 | 94 | express() 95 | .use((req, res, next) => { 96 | match( 97 | { routes: routes, location: req.url }, 98 | async (error, redirectLocation, renderProps) => { 99 | 100 | const data = await resolve(RouterContext, renderProps); 101 | 102 | const body = renderToString( 103 | 104 | 105 | 106 | ); 107 | 108 | res.send( 109 | renderToStaticMarkup( ) 110 | ); 111 | } 112 | ); 113 | }).listen( ... ); 114 | ``` 115 | ### Entry 116 | ```jsx 117 | import React from 'react'; 118 | import ReactDOM from 'react-dom'; 119 | import ComponentData from 'react-component-data'; 120 | import App from './App.js'; 121 | 122 | ReactDOM.render( 123 | 124 | 125 | , 126 | document.getElementById('app') 127 | ); 128 | ``` 129 | 130 | 131 | 132 | ### Entry (w/ React Router) 133 | ```jsx 134 | import React from 'react'; 135 | import ReactDOM from 'react-dom'; 136 | import { Router, browserHistory } from 'react-router'; 137 | import ComponentData from 'react-component-data'; 138 | import routes from './routes'; 139 | 140 | ReactDOM.render( 141 | 142 | 143 | , 144 | document.getElementById('app') 145 | ); 146 | ``` 147 | 148 | 149 | 150 | ### 🌀 Recursive Resolve (experimental) 151 | If you have nested components with data dependencies then you can use the recursive resolve method. Rather then just resolve data for the top level component (or route component when used with React Router), it will recursively iterate through your entire component tree, resolving each component's data dependency and calling its componentWillMount lifecycle method before moving farther down the tree. 152 | ```jsx 153 | import ComponentData, { resolve } from 'react-component-data/recursive'; 154 | ``` 155 | 156 | Usage is exactly the same as in the examples above, except that you'll also need to wrap each nested component that expects data with our `withData() `Higher Order Component like so: 157 | ```jsx 158 | import { withData } from 'react-component-data'; 159 | 160 | class NestedComponent extends React.Component { 161 | ... 162 | } 163 | 164 | export default withData(NestedComponent); 165 | ``` 166 | 167 | ## Demo 168 | unsplash.now.sh ([source](https://github.com/gragland/unsplash-demo)) 169 | 170 | ## Acknowledgements 171 | 172 | - Inspired by the very awesome [Next.js](https://github.com/zeit/next.js) framework by [▲ZEIT](https://zeit.co/). 173 | - Much of the recursive resolver code was gratiously copied from the [React Apollo](https://github.com/apollostack/react-apollo) project. 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-component-data", 3 | "version": "0.1.2", 4 | "description": "React Component Data", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm run build-library && npm run build-library-recursive", 8 | "build-library": "NODE_ENV=production TYPE=library webpack --config webpack.config.js", 9 | "build-library-recursive": "NODE_ENV=production TYPE=library RECURSIVE=1 webpack --config webpack.config.js", 10 | "build-script": "NODE_ENV=production TYPE=script webpack --config webpack.config.js", 11 | "prepublish": "npm run build" 12 | }, 13 | "peerDependencies": { 14 | "react": ">=0.12.0 <16.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "6.18.2", 18 | "babel-loader": "6.2.8", 19 | "babel-preset-es2015": "6.18.0", 20 | "babel-preset-react": "6.16.0", 21 | "webpack": "^1.14.0" 22 | }, 23 | "dependencies": { 24 | "lodash": "^4.17.2", 25 | "object-assign": "^4.1.0", 26 | "promise-polyfill": "^6.0.2" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/gragland/react-component-data" 31 | }, 32 | "author": "Gabe Ragland ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/gragland/react-component-data/issues" 36 | }, 37 | "keywords": [], 38 | "homepage": "https://github.com/gragland/react-component-data" 39 | } 40 | -------------------------------------------------------------------------------- /src/ComponentData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Resolver } from './Resolver.js'; 3 | import { getScript, getScriptData, isClient } from './script.js'; 4 | 5 | /* 6 | - Handles passing of data down the tree and re-hydration between server and client 7 | - Wraps child with which mediates getting data from this component via context or fetching it directly if not available ... 8 | - Or if child is React Router it adds the createElement hook to Router so that route components always get wrapped with 9 | - When rendered on the server: 10 | - Saves props.data to state and makes it available via context for components 11 | - Renders a ); 9 | } 10 | 11 | export function getScriptData(){ 12 | const payloadElement = document.getElementById('COMPONENT_DATA_PAYLOAD'); 13 | return (payloadElement ? JSON.parse(payloadElement.innerHTML) : null); 14 | } 15 | 16 | function safeStringify(obj){ 17 | return (obj ? JSON.stringify(obj).replace(/<\/script/g, '<\\/script').replace(/