├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── nodemon.json
├── package-lock.json
├── package.json
├── src
├── client
│ ├── components
│ │ ├── App.js
│ │ ├── blog
│ │ │ ├── Blog.js
│ │ │ ├── BlogAdd.js
│ │ │ ├── BlogContainer.js
│ │ │ ├── Blogs.js
│ │ │ ├── BlogsContainer.js
│ │ │ └── api
│ │ │ │ ├── actions.js
│ │ │ │ └── state.js
│ │ ├── common
│ │ │ ├── Layout.js
│ │ │ ├── Loading.js
│ │ │ └── NotFound.js
│ │ └── home
│ │ │ ├── About.js
│ │ │ └── Home.js
│ ├── index.js
│ └── setup
│ │ ├── routes.js
│ │ └── store.js
├── server
│ ├── index.js
│ └── views
│ │ └── index.js
└── static
│ ├── favicon.ico
│ └── universal-react.png
└── webpack.config.babel.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": ["@babel/plugin-transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | src/static/js/bundle.js
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌐 Universal React
2 | Simple universal React application with server side rendering.
3 |
4 | Built using latest version of React (v16), React Router (v5+), Redux (v7+), Express (v5+), Webpack (v4+), Babel Preset ES6
5 |
6 | ## 📝 Features
7 | - [x] List blogs (async API call using `axios`)
8 | - [x] View single blog
9 | - [x] Add blog
10 | - [x] Container Components ([read here](https://medium.com/@learnreact/container-components-c0e67432e005))
11 | - [x] Server Side Rendering
12 | - [x] Cache data in client `state` to prevent re-fetch
13 |
14 | ## ▶️ Running
15 | - Clone repo `git clone git@github.com:atulmy/universal-react.git universal-react`
16 | - Install NPM modules `cd universal-react` and `npm install`
17 | - Run `npm run start`
18 |
19 | ## 📦 Packages Used
20 |
21 | ### dependencies
22 | - **react** (Library for building user interfaces)
23 | - **react-dom** (React package for working with the DOM)
24 | - **react-router-dom** (A complete routing library for React)
25 | - **redux** (Predictable state container for JavaScript apps)
26 | - **redux-thunk** (Thunk middleware for Redux)
27 | - **react-redux** (Official React bindings for Redux)
28 | - **react-helmet** (Manage all of your changes to the document head)
29 | - **express** (Fast, unopinionated, minimalist web framework)
30 | - **axios** (Promise based HTTP client for the browser and node.js)
31 |
32 | ## 📚 Resources
33 | - Universal JavaScript Web Applications with React - Luciano Mammino ([YouTube](https://t.co/HVXd0HMOlC))
34 | - Container Components - ([Medium Post](https://medium.com/@learnreact/container-components-c0e67432e005))
35 | - React Router 4 SSR example - Ryan Florence ([Gist](https://gist.github.com/ryanflorence/efbe562332d4f1cc9331202669763741/))
36 | - Start learning by looking at sample codes: [#LearnByExamples](https://github.com/topics/learn-by-examples)
37 |
38 | ## ⭐ Showcase
39 | Following projects have been built with or inspired from [universal-react](https://github.com/atulmy/universal-react/)
40 | - Crate - Get monthly subscription of trendy clothes and accessories - [GitHub](https://github.com/atulmy/crate)
41 | - HIRESMART - Application to streamline hiring process - [GitHub](https://github.com/atulmy/hire-smart)
42 | - Would really appreciate if you add your project to this list by sending a PR
43 |
44 | ## 🎩 Author
45 | Atul Yadav - [GitHub](https://github.com/atulmy) • [Twitter](https://twitter.com/atulmy)
46 |
47 | ## 📜 License
48 | Copyright (c) 2017 Atul Yadav http://github.com/atulmy
49 |
50 | The MIT License (http://www.opensource.org/licenses/mit-license.php)
51 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "events": {
3 | "restart": "npm run build"
4 | }
5 | }
--------------------------------------------------------------------------------
/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/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/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/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 |
84 |
85 | )
86 | }
87 |
88 | export default BlogAdd
--------------------------------------------------------------------------------
/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/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 |
10 | {
11 | blogs.map((blog) => (
12 | -
13 | {blog.title}
14 |
15 | ))
16 | }
17 |
18 | )
19 | }
20 |
21 | export default Blogs
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 |
--------------------------------------------------------------------------------
/src/client/components/blog/api/actions.js:
--------------------------------------------------------------------------------
1 | // Imports
2 | import axios from 'axios'
3 |
4 | export const ACTION_TYPE_BLOGS_FETCH = 'ACTION_TYPE_BLOGS_FETCH'
5 | export const ACTION_TYPE_BLOGS_FETCHING = 'ACTION_TYPE_BLOGS_FETCHING'
6 | export const ACTION_TYPE_BLOG_FETCH = 'ACTION_TYPE_BLOG_FETCH'
7 | export const ACTION_TYPE_BLOG_FETCHING = 'ACTION_TYPE_BLOG_FETCHING'
8 |
9 | export function actionBlogsFetch() {
10 | return (dispatch) => {
11 | dispatch({
12 | type: ACTION_TYPE_BLOGS_FETCHING
13 | })
14 |
15 | return axios.get('https://jsonplaceholder.typicode.com/posts')
16 | .then((response) => {
17 | if (response.status === 200) {
18 | dispatch({
19 | type: ACTION_TYPE_BLOGS_FETCH,
20 | blogs: response.data
21 | })
22 | } else {
23 | console.error(response)
24 | }
25 | })
26 | .catch(function (error) {
27 | console.error(error)
28 | })
29 | }
30 | }
31 |
32 | export const actionBlogsFetchIfNeeded = () => {
33 | return (dispatch, getState) => {
34 | let state = getState()
35 |
36 | if (state.blogs.list.length === 0) {
37 | return dispatch(actionBlogsFetch())
38 | }
39 | }
40 | }
41 |
42 | export function actionBlogFetch({ id }) {
43 | return (dispatch) => {
44 | dispatch({
45 | type: ACTION_TYPE_BLOG_FETCHING
46 | })
47 |
48 | return axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
49 | .then((response) => {
50 | if (response.status === 200) {
51 | dispatch({
52 | type: ACTION_TYPE_BLOG_FETCH,
53 | blog: response.data
54 | })
55 | } else {
56 | console.error(response)
57 | }
58 | })
59 | .catch(function (error) {
60 | console.error(error)
61 | })
62 | }
63 | }
64 |
65 | export const actionBlogFetchIfNeeded = ({ id }) => {
66 | return (dispatch, getState) => {
67 | let state = getState()
68 |
69 | if (typeof state.blog.details[id] === 'undefined') {
70 | return dispatch(actionBlogFetch({ id }))
71 | }
72 | }
73 | }
74 |
75 | export const actionBlogAdd = blog => {
76 | return axios.post(`https://jsonplaceholder.typicode.com/posts`, blog)
77 | }
--------------------------------------------------------------------------------
/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/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 |
20 |
21 |
24 |
25 | )
26 |
27 | export default Layout
28 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | // Imports
2 | import path from 'path'
3 | import { Server } from 'http'
4 | import Express from 'express'
5 |
6 | import React from 'react'
7 | import { renderToString } from 'react-dom/server'
8 | import { matchPath } from 'react-router'
9 | import { StaticRouter } from 'react-router-dom'
10 | import { Helmet } from "react-helmet"
11 | import { createStore, applyMiddleware } from 'redux'
12 | import { Provider } from 'react-redux'
13 | import thunk from 'redux-thunk'
14 |
15 | // App Imports
16 | import { rootReducer } from '../client/setup/store'
17 | import routes from '../client/setup/routes'
18 | import App from '../client/components/App'
19 | import index from './views/index'
20 |
21 | // Create new server
22 | const app = new Express()
23 | const server = new Server(app)
24 |
25 | // Static files folder
26 | app.use(Express.static(path.join(__dirname, '../', 'static')))
27 |
28 | // Store (new store for each request)
29 | const store = createStore(
30 | rootReducer,
31 | applyMiddleware(thunk)
32 | )
33 |
34 | // Match any Route
35 | app.get('*', (request, response) => {
36 |
37 | let status = 200
38 |
39 | const matches = routes.reduce((matches, route) => {
40 | const match = matchPath(request.url, route.path, route)
41 | if (match && match.isExact) {
42 | matches.push({
43 | route,
44 | match,
45 | promise: route.component.fetchData ? route.component.fetchData({
46 | store,
47 | params: match.params
48 | }) : Promise.resolve(null)
49 | })
50 | }
51 | return matches
52 | }, [])
53 |
54 | // No such route, send 404 status
55 | if (matches.length === 0) {
56 | status = 404
57 | }
58 |
59 | // Any AJAX calls inside components
60 | const promises = matches.map((match) => {
61 | return match.promise
62 | })
63 |
64 | // Resolve the AJAX calls and render
65 | Promise.all(promises).then((...data) => {
66 |
67 | const initialState = store.getState()
68 | const context = {}
69 |
70 | const appHtml = renderToString(
71 |
72 |
73 |
74 |
75 |
76 | )
77 |
78 | if (context.url) {
79 | response.redirect(context.url)
80 | } else {
81 | // Get Meta header tags
82 | const helmet = Helmet.renderStatic()
83 |
84 | let html = index(helmet, appHtml, initialState)
85 |
86 | // Reset the state on server
87 | store.dispatch({
88 | type: 'RESET'
89 | })
90 |
91 | // Finally send generated HTML with initial data to the client
92 | return response.status(status).send(html)
93 | }
94 | }, (error) => {
95 | console.error(response, error)
96 | })
97 | })
98 |
99 | // Start Server
100 | const port = process.env.PORT || 3000
101 | const env = process.env.NODE_ENV || 'production'
102 | server.listen(port, (error) => {
103 | if (error) {
104 | return console.error(error)
105 | } else {
106 | return console.info(`Server running on http://localhost:${port} [${env}]`)
107 | }
108 | })
--------------------------------------------------------------------------------
/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/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atulmy/universal-react/1c513581219b948721bdec5c1222a4cfd074fc6d/src/static/favicon.ico
--------------------------------------------------------------------------------
/src/static/universal-react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atulmy/universal-react/1c513581219b948721bdec5c1222a4cfd074fc6d/src/static/universal-react.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------