Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

create-react-app-ssr

Package Overview
Dependencies
Maintainers
1
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

create-react-app-ssr

Server Side Rendering for CRA 2.x (with redux, router, code splitting, ...)

  • 2.0.14
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
10
increased by11.11%
Maintainers
1
Weekly downloads
 
Created
Source

create-react-app-ssr

This package provides a couple of useful tools to take your CRA to the next level and work out a fully fledged modern universal PWA.

  • create-react-app
  • react-scripts-rewired (extend CRA without ejecting it)
  • redux (state management)
  • react router
  • redux events
  • features
  • code splitting
  • server side rendering

NOTE: this is an holistic approach based on years of attempts to find a decent developer experience with the teams I've been workign on.

I try my best to step out the way and write as little custom code as possible, leveraging as much as I can on existing good and widely used libraries, but this is my personal point of view of what a modern Javascript web appication should look like.

You may like it, you may not.

Here I propose some step by step tutorials that take a CRA 2.x end enrich it with the good libraries that we already know, taking away the wiring from you (and myself) because I like to focus on business values instead of the infrastructure.

who am I kidding? I love working on the infrastructure! But at one point I also have to step into some "getting things done" mood, and when I do I would like to have stuff that "just works", this thing "just darn works".

Folder Structure

All the things that we are going to try to do together originates from a CRA 2.x app, so all the folder structures and relative paths that you are going to see in this document are based on you doing:

npx create-react-app my-app

So the way I suggest you to think about your application folder strategy is something like this:

  • /public
  • /src
    • /app
    • /features
    • /commponents
  • /ssr
    • /features
    • /services
    • /models
    • /routes
    • /mutations
    • /queries

Add a redux state to your app

In this tutorial we take the basic CRA structure and add:

  • redux state
  • features support

barebone state

Create /src/app/state.js and write the basics:

import { createAppStateFactory} from "create-react-app-ssr/lib/create-app-state-factory"
export const createState = createAppStateFactory()

here we are using the createAppStateFactory utility that generates a function that has the final resposability to create the state for your app.

Why all this s**t?
But for SSR compatibility of course!

When you will run this app on the server you need a clojure around your state manager so multiple concurrent requests that run async interlaced pieces of logic will not leak data into each other!

render your app with state

In /src/index.js we now need to implement this state with our app:

import { createState } from "./app/state"
createState()
    .then(props => ReactDOM.render(<App {...props} />, document.getElementById('root')))
    .catch(err => console.log(err))

createState will provide our state clojure. It is an asynchronous function because it will boot up all the features that we might like to add to our app (we will get there.)

basic state

The problem, you already know, is that we need to wrap our App with a redux provider so to be able to access the state and do cool stuff. Our next step is to create a /src/app/Root.js file that serves as main entry point and general wrapper of our business logic:

/* eslint react/prop-types: off */

import React from "react"
import { Provider } from "react-redux"
import App from "../App"

const Root = ({ store, ...props }) => (
    <Provider store={store}>
        <App {...props} />
    </Provider>
)

export default Root

and then change our /src/index.js so that it uses Root instead of App:

import { createState } from "./app/state"
import { Root } from "./app/Root"
createState()
    .then(props => ReactDOM.render(<Root {...props} />, document.getElementById('root')))
    .catch(err => console.log(err))

So far so good, if everything is still working you have now a CRA that is also provided with a redux store.

We didn't add any reducer so far, but if you investigate the state with the dev tools (which are set up by default) you may notice a ssr state property. This was injected by create-react-app-ssr and has some useful stuff to help you deal with backend requests and server side rendering of asynchronous components (we will get to it).

Anyway, we can use it to test if our app is correctly connected. Let's modify the basic App.js component and create a super simple connected component (aka container) that shows something from the state:

import React from "react"
import { connect } from "react-redux"

const mapState = ({ ssr }) => ({
    title: "My test app",
    rootUrl: ssr.getRootUrl(""),
})

const App = ({ title, rootUrl }) => (
    <div>
        <h4>{title}</h4>
        <code>rootUrl: {rootUrl}</code>
    </div>
)

export default connect(mapState)(App)

The expected result is something similar to this screenshot:

basic state

Add features to your app

The idea here is to encapsulate parts of your development efforts into isolated folders. You and I should be able to work on 2 different features without fear of stepping onto each other's shoes. It doesn't matter how good we are ar resolving git conflicts, avoiding conflicts saves time.

Let's first create an empty feature and register it into our state manager, just to get the structure of it out of our way:

Create the feature scaffold

  • /src
    • /features
      • index.js
      • /users
        • index.js

In /src/features/users/index.js paste:

export const reducers = {}
export const services = []
export const listeners = []

And in /src/features/index.js paste:

export default [
    require("./users"),
]

Last mechanical step is to add the features into our state manager creator, open /src/app/state.js and change it to:

import { createAppStateFactory} from "create-react-app-ssr/lib/create-app-state-factory"
import features from "../features"
export const createState = createAppStateFactory({}, features)

If everything works... you shouldn't see anything... because we simply registered an empty feature that does nothing. But you should also see no errors in the console. In the next step we are going to add our first reducer to a feature

Add your first feature reducer

Our feature is named "users" so it's likely handling users. Let's add a "users" reducer that help us managing this kind of data.

Create /src/features/users/users.reducer.js:

export const initialState = {
    list: [],
}

/**
 * Actions
 */

export const ADD_USER = "addUser@users"

export const addUser = user => ({
    type: ADD_USER,
    payload: user,
})

/**
 * Handlers
 */

export const actionHandlers = {
    [ADD_USER]: (state, { payload }) => ({
        ...state,
        list: [ ...state.list, payload ],
    })
}

export const reducer = (state = initialState, action) => {
    const handler = actionHandlers[action.type]
    return handler ? handler(state, action) : state
}

export default reducer

And register it into the feature's manifest /src/features/users/index.js:

export const reducers = {
    users: require("./users.reducer").default,
}

If you reload you app (well, it should reload by herself) you should finally be able to see "users" as key of your app's state in the redux dev tools!

basic state

Add your first feature service

A service is just a nice name for asynchronous redux actions. Services are basically collections of thunks, other pieces of the application import those functions and dispatch them (probably from a connected component).

The main thing about a feature's service is that you can export 2 particular functions, init() and start() that allow you to hook into the lifecycle of your application. All the init() functions will be executed and only after that all the start(). This will give you a predictable behaviour that you can use to setup the stage for your services.

IMPORTANT - SSR: I've seen many times people using the module's scope to save variables that are later on used in different functions. This behaviour works great in the client, because I have my browser and you have yours, but it's a very different story when your app is server side rendered. Keep in mind that the only scope that you can play with is the redux state. If you need local data to be persisted across different functions, use a reducer.

Our first service will simulate a backend load action to retrieve a bounch of users. Create /src/features/users/users.service.js:

import { addUser } from "./users.reducer"

// this thunk will be used to retreive stuff from the server
// so far is just dummy data, we will move it to the backend.
export const fetchUsers = () => () => {
    const dummyData = [{
        id: 1,
        username: "Luke Skywalker",
    }, {
        id: 2,
        username: "Ian Solo",
    }]

    return new Promise((resolve) => {
        setTimeout(() => resolve(dummyData), 1000)
    })
}

// fetch data and deals with the reducer
// here we should also take care of error handling
export const loadUsers = () => async (dispatch) => {
    const users = await dispatch(fetchUsers())
    users.map(user => dispatch(addUser(user)))
}

// hook into life cycle and load users when the application boot
export const start = () => (dispatch) => {
    dispatch(loadUsers())
}

At this point we only need to register the service and we will be able to "load users" when the application boot. Edit /src/features/users/index.js with the proper change:

export const services = [
    require("./users.service"),
]

Events and Listeners

Now is the time to complete the data management part of your feature with some pub/sub capabilities. Let's say that when the users are loaded for the first time, we want to notify the rest of the application, so that other features can do something with it.

You know already that we will simply dispatch a redux action, but I suggest you keep good track of this actions in a specific file called /src/features/users/events.js:

export const USERS_FIRST_LOAD = "@@USERS::FIRSTLOAD"

export const usersFirstLoad = (usersList) => ({
    type: USERS_FIRST_LOAD,
    payload: usersList,
})

This file will list all the redux events that your feature is capable of emitting. Consider it just anothe simple manifest file, something that will improve the quality of your codebase and reduce the amount of strings that you need to copy all over the place.

Now let's modify the loadUsers so to identify a first load and emit this event in /src/features/users/users.service.js:

import { usersFirstLoad } from "./events"

...

export const loadUsers = () => async (dispatch, getState) => {
    const isFirstLoad = getState().users.list.length === 0

    const users = await dispatch(fetchUsers())
    users.map(user => dispatch(addUser(user)))
    
    // dispatch an event that is meant for other features to react to
    if (isFirstLoad) dispatch(usersFirstLoad(users))
}

NOTE: a redux event is just a normal redux action to which we choose to give a particular meaning, nothing more than that :-)

Your second feature - a loading curtain

In order to tackle the listening and reacting to events it's more efficient to try to add a new cool feature to our app. A loading curtain has the scope of masquerading the UI while some initial operations are taking place.

We are going to something extremely simple, we will write a reducer, a container and a listener. But in order to start try to use what you learned so far and scaffold a curtain feature.

The curtain reducer

This is going to be trivial. A loading curtain is a piece of UI that need to be visible or not, based on some conditions. So in this reducer we are simply going to store the visibility value. Create /src/features/curtain/curtain.reducer.js and register it to your feature:

export const initialState = true

/**
 * Actions
 */

export const SET_VALUE = "setValue@curtain"

export const setValue = value => ({
    type: SET_VALUE,
    payload: Boolean(value),
})

/**
 * Handlers
 */

export const actionHandlers = {
    [SET_VALUE]: (state, { payload }) => payload
}

export const reducer = (state = initialState, action) => {
    const handler = actionHandlers[action.type]
    return handler ? handler(state, action) : state
}

export default reducer

The curtain container

This piece of UI simply need to cover everything and link it's visibility to the current curtain state value. Create /src/features/curtain/Curtain.js:

import React from "react"
import { connect } from "react-redux"

const mapState = ({ curtain }) => ({
    isVisible: curtain,
})

const style = {
    position: "fixed", top: 0, bottom: 0, left: 0, right: 0,
    display: "flex", justifyContent: "center", alignItems: "center",
    backgroundColor: "#258cf9", color: "#fff",
}

const Curtain = ({ isVisible }) => isVisible
    ? <div style={style}>loading...</div>
    : null

export default connect(mapState)(Curtain)

Now, in order to keep good control over our feature system, I suggest you export every feature capability from the feature's index.js itself. Edit /src/features/curtain/index.js and add:

export { default as Curtain } from "./Curtain"

so that we can import it from the /src/App.js and drop the Curtain UI into the main app:

import { Curtain } from "./features/curtain"

...

const App = ({ title, rootUrl }) => (
    <div>
        <h4>{title}</h4>
        <code>rootUrl: {rootUrl}</code>
        <Curtain />
    </div>
)

add the curtain

The curtain Listener

Finally we are getting to the point of having a listener! What we want to achieve is that after the users first load, the curtain will go away.

Create the listener file in /src/features/curtain/curtain.listener.js:

import { setValue } from "./curtain.reducer"

export default [
    {
        type: "@@USERS::FIRSTLOAD",
        handler: (action) => (dispatch) => {
            console.log("An action was dispatched", action)
            dispatch(setValue(false))
        },
    },
]

And register it into the feature's manifest /src/features/curtain/index.js:

export const listeners = [
    require("./curtain.listener").default,
]

This should be enough to get the Curtain to disappear after the fake loading time of 1s that we set in the users' service.

FAQs

Package last updated on 29 Nov 2018

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc