├── src ├── static │ ├── favicon.ico │ └── universal-react.png ├── client │ ├── components │ │ ├── common │ │ │ ├── Loading.js │ │ │ ├── NotFound.js │ │ │ └── Layout.js │ │ ├── blog │ │ │ ├── Blog.js │ │ │ ├── Blogs.js │ │ │ ├── BlogsContainer.js │ │ │ ├── BlogContainer.js │ │ │ ├── api │ │ │ │ ├── state.js │ │ │ │ └── actions.js │ │ │ └── BlogAdd.js │ │ ├── home │ │ │ ├── About.js │ │ │ └── Home.js │ │ └── App.js │ ├── index.js │ └── setup │ │ ├── routes.js │ │ └── store.js └── server │ ├── views │ └── index.js │ └── index.js ├── .gitignore ├── nodemon.json ├── .babelrc ├── webpack.config.babel.js ├── LICENSE ├── package.json └── README.md /src/static/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | src/static/js/bundle.js -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": { 3 | "restart": "npm run build" 4 | } 5 | } -------------------------------------------------------------------------------- /src/static/universal-react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atulmy/universal-react/HEAD/src/static/universal-react.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /src/client/components/common/Loading.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | 4 | const Loading = () => ( 5 |

Loading...

6 | ) 7 | 8 | export default Loading 9 | -------------------------------------------------------------------------------- /src/client/components/common/NotFound.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Link } from 'react-router-dom' 4 | 5 | const NotFound = () => ( 6 |

7 | Page not found. Go Home 8 |

9 | ) 10 | 11 | export default NotFound 12 | -------------------------------------------------------------------------------- /src/client/components/blog/Blog.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Helmet } from 'react-helmet' 4 | 5 | const Blog = ({ blog: { title, body } }) => { 6 | return ( 7 |
8 | 9 | Blog { title } 10 | 11 | 12 |

{ title }

13 | 14 |

{ body }

15 |
16 | ) 17 | } 18 | 19 | export default Blog 20 | -------------------------------------------------------------------------------- /src/client/components/home/About.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Helmet } from 'react-helmet' 4 | 5 | const About = () => ( 6 |
7 | 8 | About Us 9 | 10 | 11 |

About Us

12 | 13 |

Lorem ipsum ut orci quam, ornare sed lorem sed, hendrerit auctor dolor. Nulla viverra, nibh quis ultrices 14 | malesuada.

15 |
16 | ) 17 | 18 | export default About 19 | -------------------------------------------------------------------------------- /src/client/components/blog/Blogs.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Link } from 'react-router-dom' 4 | 5 | // Component 6 | const Blogs = ({ blogs }) => { 7 | // render 8 | return ( 9 | 18 | ) 19 | } 20 | 21 | export default Blogs 22 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | const config = { 4 | entry: { 5 | js: './src/client/index.js' 6 | }, 7 | 8 | output: { 9 | path: path.join(__dirname, 'src', 'static', 'js'), 10 | filename: 'bundle.js' 11 | }, 12 | 13 | module: { 14 | rules: [ 15 | { 16 | test: path.join(__dirname, 'src'), 17 | use: { 18 | loader: 'babel-loader', 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | 25 | export default config 26 | -------------------------------------------------------------------------------- /src/client/components/home/Home.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Helmet } from 'react-helmet' 4 | 5 | const Home = () => ( 6 |
7 | 8 | Home 9 | 10 | 11 |

Home Page

12 | 13 |

This example uses jsonplaceholder.typicode.com API

14 | 15 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et fermentum dui. Ut orci quam, ornare sed lorem 16 | sed, hendrerit.

17 |
18 | ) 19 | 20 | export default Home 21 | -------------------------------------------------------------------------------- /src/client/components/common/Layout.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Layout = ({ children }) => ( 6 |
7 |
8 | Home 9 |   10 | About 11 |   12 | Blogs 13 |   14 | Add Blog 15 |
16 | 17 |
18 | { children } 19 |
20 | 21 | 24 |
25 | ) 26 | 27 | export default Layout 28 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { BrowserRouter as Router } from 'react-router-dom' 5 | import { Provider } from 'react-redux' 6 | 7 | // App Imports 8 | import { store } from './setup/store' 9 | import App from './components/App' 10 | 11 | // Client App 12 | const Client = () => ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | 20 | // Mount client app 21 | window.onload = () => { 22 | hydrate( 23 | , 24 | document.getElementById('app') 25 | ) 26 | } -------------------------------------------------------------------------------- /src/client/components/App.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | import { Route, Switch } from 'react-router-dom' 4 | 5 | // App Imports 6 | import routes from '../setup/routes' 7 | import Layout from './common/Layout' 8 | import NotFound from './common/NotFound' 9 | 10 | const App = () => ( 11 | 12 | 13 | {routes.map((route, index) => ( 14 | // pass in the initialData from the server for this specific route 15 | 16 | ))} 17 | 18 | 19 | 20 | 21 | ) 22 | 23 | export default App 24 | -------------------------------------------------------------------------------- /src/server/views/index.js: -------------------------------------------------------------------------------- 1 | const index = (helmet = {}, appHtml = '', initialState = {}) => ( 2 | ` 3 | 4 | 5 | 6 | 7 | ${helmet.title.toString()} 8 | 9 | 10 | 11 | 12 |
${appHtml}
13 | 14 | 17 | 18 | 19 | 20 | ` 21 | ) 22 | 23 | export default index -------------------------------------------------------------------------------- /src/client/setup/routes.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React from 'react' 3 | 4 | // App Imports 5 | import Home from '../components/home/Home' 6 | import About from '../components/home/About' 7 | import BlogsContainer from '../components/blog/BlogsContainer' 8 | import BlogContainer from '../components/blog/BlogContainer' 9 | import BlogAdd from '../components/blog/BlogAdd' 10 | 11 | // Routes 12 | const routes = [ 13 | { 14 | path: '/', 15 | component: Home, 16 | exact: true 17 | }, 18 | { 19 | path: '/about', 20 | component: About 21 | }, 22 | { 23 | path: '/blogs', 24 | component: BlogsContainer 25 | }, 26 | { 27 | path: '/blog/:id', 28 | component: BlogContainer 29 | }, 30 | { 31 | path: '/blog-add', 32 | component: BlogAdd 33 | } 34 | ] 35 | 36 | export default routes -------------------------------------------------------------------------------- /src/client/setup/store.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import { compose, combineReducers } from 'redux' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import thunk from 'redux-thunk' 5 | 6 | // App Imports 7 | import * as blog from '../components/blog/api/state' 8 | 9 | // App Reducer 10 | const appReducer = combineReducers({ 11 | ...blog 12 | }) 13 | 14 | // Root Reducer 15 | export const rootReducer = (state, action) => { 16 | if (action.type === 'RESET') { 17 | state = undefined 18 | } 19 | 20 | return appReducer(state, action) 21 | } 22 | 23 | // Load initial state from server side 24 | let initialState 25 | if (typeof window !== 'undefined') { 26 | initialState = window.__INITIAL_STATE__ 27 | delete window.__INITIAL_STATE__ 28 | } 29 | 30 | // Store 31 | export const store = createStore( 32 | rootReducer, 33 | initialState, 34 | 35 | compose( 36 | applyMiddleware(thunk) 37 | ) 38 | ) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Atul Yadav 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 | -------------------------------------------------------------------------------- /src/client/components/blog/BlogsContainer.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React, { useEffect } from 'react' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { Helmet } from 'react-helmet' 5 | 6 | // App Imports 7 | import { actionBlogsFetch, actionBlogsFetchIfNeeded } from './api/actions' 8 | import Loading from '../common/Loading' 9 | import Blogs from './Blogs' 10 | 11 | // Component 12 | const BlogsContainer = () => { 13 | // state 14 | const { loading, list } = useSelector(state => state.blogs) 15 | const dispatch = useDispatch() 16 | 17 | // on mount/update 18 | useEffect(() => { 19 | dispatch(actionBlogsFetchIfNeeded()) 20 | }, []) 21 | 22 | const refresh = () => { 23 | dispatch(actionBlogsFetch()) 24 | } 25 | 26 | // render 27 | return ( 28 |
29 | 30 | Blogs 31 | 32 | 33 |

Blogs

34 | 35 |

36 | 37 |

38 | 39 | { 40 | loading 41 | ? 42 | : } 43 | } 44 |
45 | ) 46 | } 47 | 48 | // Static method 49 | BlogsContainer.fetchData = ({ store }) => { 50 | return store.dispatch(actionBlogsFetch()) 51 | } 52 | 53 | export default BlogsContainer 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "export NODE_ENV=development && webpack -d", 8 | "start": "export NODE_ENV=development && npm run build && nodemon --ignore src/static/ --exec babel-node -- src/server/index.js", 9 | "build:prod": "export NODE_ENV=production && webpack -p", 10 | "start:prod": "export NODE_ENV=production && npm run build:prod && nodemon --ignore src/static/ --exec babel-node -- src/server/index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "7.13.1", 17 | "@babel/node": "7.13.0", 18 | "@babel/plugin-transform-runtime": "7.13.7", 19 | "@babel/preset-env": "7.13.5", 20 | "@babel/preset-react": "7.12.13", 21 | "babel-loader": "8.2.2", 22 | "nodemon": "2.0.7", 23 | "webpack": "4.44.2", 24 | "webpack-cli": "3.3.12" 25 | }, 26 | "dependencies": { 27 | "@babel/runtime": "7.13.7", 28 | "axios": "0.21.1", 29 | "express": "5.0.0-alpha.7", 30 | "react": "17.0.1", 31 | "react-dom": "17.0.1", 32 | "react-helmet": "^6.1.0", 33 | "react-redux": "^7.2.2", 34 | "react-router-dom": "^5.2.0", 35 | "redux": "^4.0.5", 36 | "redux-thunk": "^2.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/components/blog/BlogContainer.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React, { useEffect } from 'react' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { Link } from 'react-router-dom' 5 | 6 | // App Imports 7 | import { actionBlogFetch, actionBlogFetchIfNeeded } from './api/actions' 8 | import Loading from '../common/Loading' 9 | import Blog from './Blog' 10 | 11 | // Component 12 | const BlogContainer = ({ match: { params: { id } } }) => { 13 | // state 14 | const { loading, details } = useSelector(state => state.blog) 15 | const dispatch = useDispatch() 16 | 17 | // on mount/update 18 | useEffect(() => { 19 | refresh() 20 | }, []) 21 | 22 | const refresh = () => { 23 | dispatch(actionBlogFetchIfNeeded({ id: parseInt(id) })) 24 | } 25 | 26 | // render 27 | return ( 28 |
29 | { 30 | loading 31 | ? 32 | : details && details[id] && 33 | } 34 | 35 |

36 | 37 |

38 | 39 |

Back to all blogs

40 |
41 | ) 42 | } 43 | 44 | // Static method 45 | BlogContainer.fetchData = ({ store, params: { id } }) => { 46 | return store.dispatch(actionBlogFetch({ id: parseInt(id) })) 47 | } 48 | 49 | export default BlogContainer 50 | -------------------------------------------------------------------------------- /src/client/components/blog/api/state.js: -------------------------------------------------------------------------------- 1 | // App Imports 2 | import { 3 | ACTION_TYPE_BLOGS_FETCH, 4 | ACTION_TYPE_BLOGS_FETCHING, 5 | ACTION_TYPE_BLOG_FETCH, 6 | ACTION_TYPE_BLOG_FETCHING 7 | } from './actions' 8 | 9 | export function blogs(state = { list: [], error: false, loading: false }, action = {}) { 10 | switch (action.type) { 11 | 12 | case ACTION_TYPE_BLOGS_FETCHING: 13 | return Object.assign({}, state, { 14 | list: [], 15 | error: false, 16 | loading: true 17 | }) 18 | 19 | case ACTION_TYPE_BLOGS_FETCH: 20 | return Object.assign({}, state, { 21 | list: action.blogs, 22 | error: action.error, 23 | loading: false 24 | }) 25 | 26 | default: 27 | return state 28 | } 29 | } 30 | 31 | export function blog(state = { details: [], error: false, loading: false }, action = {}) { 32 | switch (action.type) { 33 | 34 | case ACTION_TYPE_BLOG_FETCHING: 35 | return Object.assign({}, state, { 36 | details: state.details, 37 | error: false, 38 | loading: true 39 | }) 40 | 41 | case ACTION_TYPE_BLOG_FETCH: 42 | state.details[action.blog.id] = action.blog; 43 | 44 | return Object.assign({}, state, { 45 | details: state.details, 46 | error: action.error, 47 | loading: false 48 | }) 49 | 50 | default: 51 | return state 52 | } 53 | } -------------------------------------------------------------------------------- /src/client/components/blog/BlogAdd.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import React, { useState } from 'react' 3 | import { Helmet } from "react-helmet" 4 | 5 | // App Imports 6 | import { actionBlogAdd } from './api/actions' 7 | 8 | // Component 9 | const BlogAdd = () => { 10 | // state 11 | const [isSubmitting, isSubmittingToggle] = useState(false) 12 | const [message, setMessage] = useState(false) 13 | const [blog, setBlog] = useState({ 14 | userId: 1, // Example 15 | title: '', 16 | body: '' 17 | }) 18 | 19 | // on submit 20 | const onSubmit = async event => { 21 | event.preventDefault() 22 | 23 | isSubmittingToggle(true) 24 | 25 | try { 26 | const { data } = await actionBlogAdd(blog) 27 | 28 | if(data) { 29 | setMessage(`Blog added successfully with id ${ data.id }`) 30 | } 31 | } catch (error) { 32 | setMessage(`Error ${ error.message }. Please try again.`) 33 | } finally { 34 | isSubmittingToggle(false) 35 | } 36 | } 37 | 38 | // on change 39 | const onChange = event => { 40 | setBlog({ ...blog, [event.target.name]: event.target.value}) 41 | } 42 | 43 | // render 44 | return ( 45 |
46 | 47 | Blog Add 48 | 49 | 50 |

Blog Add

51 | 52 |
53 |

54 | 63 |

64 | 65 |

66 |