GithubHelp home page GithubHelp logo

jcoreio / redux-features Goto Github PK

View Code? Open in Web Editor NEW
14.0 3.0 0.0 772 KB

a powerful feature-oriented programming framework for redux

License: MIT License

JavaScript 97.37% TypeScript 2.63%
redux fop features feature-oriented-programming code-splitting

redux-features's Introduction

redux-features

npm Build Status Coverage Status semantic-release Commitizen friendly

A feature-oriented programming and code-splitting framework for Redux (rewrite of redux-plugins-immutable). Asynchronously and atomically load features containing Redux reducers and middleware, React Components, react-router routes, etc. using Webpack code splitting. Supports server-side rendering.

Huh?

When a web app gets big enough, initial page load will be too slow unless you split your code into bundles and avoid loading various features until they're needed. For instance, in a React/Redux app, you may have a UserProfileView and userProfileReducer that you don't want to load until the user visits your /user/profile route.

Webpack provides a foundation for this; you can System.import those modules when needed and Webpack will put them in a separate code bundle. And react-router versions 2 and 3 support getComponent hooks on routes in which you can System.import the component to render.

But these are bare-bones tools: they don't automatically show any kind of spinner and tell the user what's happening while a bundle is loading, and more importantly, they don't automatically show the user an error if it fails to load. Also, if you want to load reducers or redux middleware from a separate bundle, you need some way to install them in your redux store once they've loaded. None of these are incredibly difficult problems, and you may have basic solutions to them, but you may not have time to design a systematic, well-organized, DRY solution.

This is where redux-features comes in! It allows you to define features, each of which may contain a reducer, middleware, React components, or anything else you want. All you have to do is dispatch a loadFeature action, and it takes care of loading them and hooking in your reducer and middleware when it's finished. It stores their loading status and load errors (if any) in Redux state, which you can then connect into a component and display to the user. Together with react-redux-features, you can create a component with just a few lines of code that automatically starts loading a feature when it mounts, displays its loading or error status to the user, and renders a React component from the feature once it's loaded.

redux-features is also a great plugin framework for your app. A third-party developer can write a feature, following the contract of this package, and by simply adding the feature definition via redux-features, it can extend or modify your app's behavior.

Ecosystem

TOC

Overview

Each feature is an object you put into Redux state that groups together everything needed for a feature to function. Typically this will include one ore more React components, a reducer, and maybe even middleware. By grouping these together and atomically adding them to the redux state, you can atomically activate entire parts of your application.

When you combine this with Webpack code splitting, you get a nice, clean way to dynamically load application features when they are needed, for instance when the user visits a certain route.

This can also be a great foundation for people to develop plugins for your app.

But Redux state shouldn't contain functions, React components, etc.!!

People have good reasons for advocating this, primarily because you can't serialize functions or React components and send them over the wire. But that doesn't mean you can't send everything else, and reconstruct the features from it.

For awhile, I thought I should store the functions, React components, etc. for features outside of Redux state, but I came to the conclusion that it's best to keep them in the Redux state. Here's why:

  • The alternative would be a separate store and provider of some sort to pass down features' functions and components through React context, which would be very similar to the Redux store and provider in form.
  • All of the non-serializable feature functions and components are organized in a single subtree of your state that is easy to strip out before serializing.
  • The client can reconstruct the same state as the server by adding all of the same features, then loading all of the features marked as loaded in the serialized state from the server.

Feature lifecycle

redux-features keeps track of the state of each feature in a separate subtree from the features themselves. Here is what the feature lifecycle looks like:

  • You dispatch addFeature(...)
  • The feature's state is set to NOT_LOADED. redux-features middleware calls its init method, if it has one.
  • You dispatch loadFeature(...)
  • The feature's state is set to LOADING. redux-features middleware calls its load method, if it has one.
  • The promise returned by load resolves to the full feature, which redux-features middleware dispatches in an installFeature(...) action.
  • The feature's state is set to LOADED.
  • Or the promise returned by load rejects, in which case redux-features middleware dispatches a setFeatureStatus(...) action with the rejection reason.
  • The feature's state is set to the rejection reason.

If you add a feature with a reducer, its reducer will be active, even if its state is NOT_LOADED -- whatever is in the redux state is active. Indeed, you may sometimes want to add a feature without a load method at all. You only need to load a feature if you want to fetch code for it asynchronously or perform some other long-running initialization in the background.

Feature API

The following optional properties on features are handled by redux-features. However, you may add any other properties you want.

  • reducer: (state, action) => state: a reducer to apply to the top-level redux state for each action
  • middleware: store => next => action => any: middleware to apply for each action
  • init: (store) => any: called when the feature is added by dispatching an addFeature action
  • load: (store) => Promise<Feature>: called when you dispatch a loadFeature action for the feature. It should return a promise that will resolve to the full feature after loading. The full feature will replace the current feature in the redux state, so initial properties of the feature will be blown away unless you merge them into the full feature yourself.
  • dependencies: Array<string>: an array of feature ids to load when this feature is loaded. All dependencies will be loaded in parallel with this feature, so they may not be available during the load method. Circular feature dependencies are not supported, and the behavior is undefined. As an alternative to dependencies, you can load other features (by dispatching loadFeature actions) in your feature's load method, in which case it's easy to wait for those features to load and do something with them before resolving your feature's load promise.

Quick start

Creating the redux store

import {createStore, applyMiddleware, combineReducers} from 'redux'
import {
  featureStatesReducer,
  featuresReducer,
  featureReducersReducer,
  loadFeatureMiddleware,
  featureMiddlewaresMiddleware,
  composeReducers,
  addFeature,
  loadFeature,
} from 'redux-features'

const reducer = composeReducers(
  combineReducers({
    featureStates: featureStatesReducer(),
    features: featuresReducer(),
    ...
    // your own app reducers here
  }),
  featureReducersReducer(),
)

const store = createStore(reducer, applyMiddleware(
  featureMiddlewaresMiddleware(),
  loadFeatureMiddleware(),
  // more middleware you want here
))

Adding a feature

const counter = {
  load(store) {
    return System.import('./counter/reducer').then(reducer => ({...this, reducer})
  }
}
store.dispatch(addFeature('counter', counter))

// state now looks like this:
/*
 * {
 *   featureStates: {
 *     counter: 'NOT_LOADED'
 *   }
 *   features: {
 *     counter: {
 *       load: load(store)
 *     }
 *   }
 * }
 */

Loading the feature

Now let's say ./counter/reducer.js contains this:

export default function counterReducer(state, action) {
  switch (action.type) {
    case 'COUNTER.INCREMENT':
      return { ...state, counter: (state.counter || 0) + 1 }
    case 'COUNTER.DECREMENT':
      return { ...state, counter: (state.counter || 0) - 1 }
    default:
      return state
  }
}

Once the 'counter' feature is loaded, featureReducersReducer will call counterReducer:

store.dispatch({ type: 'COUNTER.INCREMENT' }) // does nothing yet

store.dispatch(loadFeature('counter')).then(() => {
  // state now looks like this:
  /*
   * {
   *   featureStates: {
   *     counter: 'LOADED'
   *   }
   *   features: {
   *     counter: {
   *       load: load(store),
   *       reducer: counterReducer(state, action)
   *     }
   *   }
   * }
   */

  store.dispatch({ type: 'COUNTER.INCREMENT' })
  // state.counter is now 1
})

Custom start

You may be setting up your reducer and middleware in a different way than in the examples above; you may be using a different type for your state, like an Immutable.js Map; or you may want to mount the features and feature states at non-default locations. To support that, redux-features exports a modular collection of reducers and middleware you can hook into your app however you need:

  • featuresReducer - puts the features into the state
  • featureStatesReducer - controls state specifying whether features are loaded or not
  • featureReducersReducer - calls features' reducers
  • loadFeatureMiddleware - handles loadFeature actions by calling a feature's load method and dispatching installFeature when it resolves (or setFeatureStatus with an error if it rejects).
  • featureMiddlewaresMiddleware - calls features' middleware

Technically you could use only featuresReducer if all you want to do is add features that provide React components to your app at startup, and not do any dynamic loading.

If you want to dynamically load features, you'll need to use featureStatesReducer and loadFeatureMiddleware. And if you want to support reducers or middleware in features, you'll need to use featureReducersReducer and featureMiddlewaresMiddleware, respectively.

featuresReducer and featureStatesReducer

If you're using combineReducers, you can hook featuresReducer and featureStatesReducer into it with any keys you want:

import {combineReducers} from 'redux'
import {featureStatesReducer, featuresReducer} from 'redux-features'

const appReducer = combineReducers({
  ...
  myFeatureStates: featureStatesReducer(),
  myFeatures: featuresReducer(),
})

Which would make your state look like

{
  ...
  myFeatureStates: {
    feature1: 'NOT_LOADED',
    feature2: 'LOADING',
  },
  myFeatures: {
    feature1: {...},
    feature2: {...},
  },
}

However, you can hook in featuresReducer and featureStatesReducer however you want, so long as you call them with the features subtree and feature states subtree, respectively.

loadFeatureMiddleware

If you mount the features and their states anywhere other than state.features and state.featureStates, you have to tell loadFeatureMiddleware how to get them out of the state, like this:

import { createStore, applyMiddleware } from 'redux'
import { loadFeatureMiddleware } from 'redux-features'
import reducer from './reducer'
import initialState from './initalState'

const store = createStore(
  reducer,
  initialState,
  applyMiddleware(
    loadFeatureMiddleware({
      getFeatureStates: (state) => state.myFeatureStates,
      getFeatures: (state) => state.myFeatures,
    })
  )
)

featureReducersReducer

If any feature has a reducer property, then featureReducersReducer will call it for each action dispatched to the store. It must be composed with your top-level reducer so that it can pass the top-level state to the feature reducers. You may use the composeReducers function from redux-features to do this. Again, if you mount the features anywhere other than state.features, you have to tell featureReducersReducer where to find them:

import {combineReducers} from 'redux'
import {featureStatesReducer, featuresReducer, featureReducersReducer, composeReducers} from 'redux-features'

const baseReducer = combineReducers({
  ...
  myFeatureStates: featureStatesReducer(),
  myFeatures: featuresReducer(),
})

const appReducer = composeReducers(
  baseReducer,
  featureReducersReducer({getFeatures: state => state.myFeatures})
)

featureMiddlewaresMiddleware

Just like featureReducersReducer, if any feature has a middleware property, then featureMiddlewaresMiddleware will call it for each action dispatched to the store. If you mount the features anywhere other than state.features, you have to tell featureMiddlewaresMiddleware where to find them:

import {createStore, applyMiddleware, combineReducers} from 'redux'
import {
  featureStatesReducer,
  featuresReducer,
  loadFeatureMiddleware,
  featureMiddlewaresMiddleware,
} from 'redux-features'

const reducer = combineReducers({
  myFeatureStates: featureStatesReducer(),
  myFeatures: featuresReducer(),
  ...
  // your own app reducers here
})

const store = createStore(reducer, applyMiddleware(
  featureMiddlewaresMiddleware({
    getFeatures: state => state.myFeatures,
  }),
  loadFeatureMiddleware({
    getFeatureStates: state => state.myFeatureStates,
    getFeatures: state => state.myFeatures,
  }),
  // more middleware you want here
))

Using custom createReducers, composeReducers, createMiddleware, and composeMiddleware

You can pass in your own implementations of createReducer, composeReducers, createMiddleware, and composeMiddleware. For instance, you can use the implementations from mindfront-redux-utils, which can compose reducers from its createReducer and middleware from its createMiddleware more efficiently than a naive implementation, for high-action-throughput apps:

import {createStore, applyMiddleware, combineReducers} from 'redux'
import {
  featureStatesReducer,
  featuresReducer,
  featureReducersReducer,
  loadFeatureMiddleware,
  featureMiddlewaresMiddleware,
  addFeature,
  loadFeature,
} from 'redux-features'
import {createReducer, composeReducers, createMiddleware, composeMiddleware} from 'mindfront-redux-utils'

const reducer = composeReducers(
  combineReducers({
    featureStates: featureStatesReducer({createReducer}),
    features: featuresReducer({createReducer}),
    ...
    // your own app reducers here
  }),
  featureReducersReducer({composeReducers}),
)

const store = createStore(reducer, applyMiddleware(
  featureMiddlewaresMiddleware({composeMiddleware}),
  loadFeatureMiddleware({createMiddleware}),
  // more middleware you want here
))

Server-side rendering

Loading features during SSR, and then again on the client, when bootstrapping, takes a bit of extra effort. There are various options:

  • One-pass rendering:
    • Manually dispatch loadFeature actions applicable to the requested route.
    • Dispatch loadFeature actions in react-router v2/v3 hooks like getComponent, getIndexRoute, etc.
  • Two-pass rendering:
    • Create the store with a middleware that keeps track of loadFeature promises
    • Render the app in your first pass with components that dispatch the applicable loadFeature events (e.g. featureLoaders from react-redux-features
    • Wait for the loadFeature promises to resolve
    • Render again, which will now include the loaded feature components, and send it to the client

On the client, after you create your store from the initial server-side redux state, dispatch the loadInitialFeatures action, which will load all the features that were loaded on the server side. Once it resolves, you are ready to mount your app.

One-pass rendering example

TODO

Two-pass rendering example

(using Webpack 1x)

reducer.js

import {
  composeReducers,
  featuresReducer,
  featureStatesReducer,
  featureReducersReducer,
} from 'redux-features'

export default composeReducers(
  combineReducers({
    features: featuresReducer(),
    featureStates: featureStatesReducer(),
  }),
  featureReducersReducer()
)

counterFeature.js

export default {
  load: (store) =>
    new Promise((resolve) =>
      require.ensure(
        ['./counterFeatureImpl'],
        (require) => require('./counterFeatureImpl').default
      )
    ),
}

counterFeatureImpl.js

// polyfill require.ensure for the server side
if (__SERVER__) require.ensure = (modules, callback) => callback(require)

export default {
  reducer: (state, action) =>
    action.type === 'INCREMENT'
      ? { ...state, count: (state.count || 0) + 1 }
      : state,
  Counter: connect(({ count }) => ({ count }))(({ count }) => (
    <div>{`Counter: ${count || 0}`}</div>
  )),
}

Counter.js

import React from 'react'
import { featureLoader } from 'react-redux-features'

const Counter = featureLoader({
  featureId: 'counter',
  render({ featureState, feature, props }) {
    const Comp = feature && feature.Counter
    if (featureState instanceof Error) {
      return (
        <div className="alert alert-danger">
          Failed to load counter: {featureState.message}
        </div>
      )
    } else if (!Comp) {
      return <div className="alert alert-info">Loading counter...</div>
    }
    return <Comp {...props} />
  },
})

serverSideRender.js

import React from 'react'
import { renderToString } from 'react-dom/server'
import { combineReducers, createStore, applyMiddleware } from 'redux'
import { connect, Provider } from 'react-redux'
import {
  loadFeatureMiddleware,
  featureMiddlewaresMiddleware,
  addFeature,
  LOAD_FEATURE,
} from 'redux-features'
import reducer from './reducer'
import counterFeature from './counterFeature'
import Counter from './Counter'

async function serverSideRender(request, response) {
  const featurePromises = []

  const store = createStore(
    reducer,
    applyMiddleware(
      (store) => (next) => (action) => {
        const result = next(action)
        if (action.type === LOAD_FEATURE) featurePromises.push(result)
        return result
      },
      loadFeatureMiddleware(),
      featureMiddlewaresMiddleware()
    )
  )

  store.dispatch(addFeature('counter', counterFeature))

  const app = (
    <Provider store={store}>
      <Counter />
    </Provider>
  )
  renderToString(app)
  try {
    await Promise.all(featurePromises)
  } catch (error) {
    response.status(500).send(error.stack)
  }

  const body = renderToString(app)

  // get all state except features
  const { features, ...initialState } = store.getState()

  response.status(200).send(`<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript">
      window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
    </script>
    <script src="/client.js">
  </head>
  <body>
    <div id="root">${body}</div>
  </body>
</html>`)
}

client.js

import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import reducer from './reducer'
import Counter from './Counter'
import counterFeature from './counterFeature'
import {
  loadFeatureMiddleware,
  featureMiddlewaresMiddleware,
  addFeature,
  loadInitialFeatures,
} from 'redux-features'

const store = createStore(
  reducer,
  window.__INITIAL_STATE__,
  applyMiddleware(loadFeatureMiddleware(), featureMiddlewaresMiddleware())
)
store.dispatch(addFeature(counterFeature))
store
  .dispatch(loadInitialFeatures())
  .then(() =>
    render(
      <Provider store={store}>
        <Counter />
      </Provider>,
      document.getElementById('root')
    )
  )
  .catch((error) =>
    render(
      <div className="alert alert-danger">
        Failed to load some features: {error.message}
      </div>,
      document.getElementById('root')
    )
  )

redux-features's People

Contributors

greenkeeper[bot] avatar jedwards1211 avatar renovate-bot avatar renovate[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

redux-features's Issues

The automated release is failing 🚨

🚨 The automated release from the master branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this πŸ’ͺ.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit the master branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


Invalid npm token.

The npm token configured in the NPM_TOKEN environment variable must be a valid token allowing to publish to the registry https://registry.npmjs.org/.

If you are using Two-Factor Authentication, make configure the auth-only level is supported. semantic-release cannot publish with the default auth-and-writes level.

Please make sure to set the NPM_TOKEN environment variable in your CI with the exact value of the npm token.


Good luck with your project ✨

Your semantic-release bot πŸ“¦πŸš€

Dependency deprecation warning: travis-deploy-once (npm)

On registry https://registry.npmjs.org/, the "latest" version (v5.0.11) of dependency travis-deploy-once has the following deprecation notice:

We recommend to use Travis Build Stages instead

Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.

Affected package file(s): package.json

If you don't care about this, you can close this issue and not be warned about travis-deploy-once's deprecation again. If you would like to completely disable all future deprecation warnings then add the following to your config:

"suppressNotifications": ["deprecationWarningIssues"]

Dependency deprecation warning: validate-commit-msg (npm)

On registry https://registry.npmjs.org/, the "latest" version (v2.14.0) of dependency validate-commit-msg has the following deprecation notice:

Check out CommitLint which provides the same functionality with a more user-focused experience.

Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.

Affected package file(s): package.json

If you don't care about this, you can close this issue and not be warned about validate-commit-msg's deprecation again. If you would like to completely disable all future deprecation warnings then add the following to your config:

"suppressNotifications": ["deprecationWarningIssues"]

Possible writeup of all this work?

Hi. I do regular searches for any new or recently-updated Redux-related libraries. I've seen your repos pop up in that list pretty frequently. Skimming the readmes, it sounds like you're building some interesting things, but I'm not clear on how things fit together and what all you'd use them for. Any chance of you putting together a long-form article that describes what problems you're trying to solve and how these libraries might be useful for people?

Dependency deprecation warning: babel-preset-es2015 (npm)

On registry https://registry.npmjs.org, the "latest" version (v6.24.1) of dependency babel-preset-es2015 has the following deprecation notice:

πŸ™Œ Thanks for using Babel: we recommend using babel-preset-env now: please read babeljs.io/env to update!

Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake.

Affected package file(s): package.json

If you don't care about this, you can close this issue and not be warned about babel-preset-es2015's deprecation again. If you would like to completely disable all future deprecation warnings then add the following to your config:

"suppressNotifications": ["deprecationWarningIssues"]

An in-range update of eslint-plugin-flowtype is breaking the build 🚨

Version 2.30.4 of eslint-plugin-flowtype just got published.

Branch Build failing 🚨
Dependency eslint-plugin-flowtype
Current Version 2.30.3
Type devDependency

This version is covered by your current version range and after updating it in your project the build failed.

As eslint-plugin-flowtype is β€œonly” a devDependency of this project it might not break production or downstream projects, but β€œonly” your build or test tools – preventing new deploys or publishes.

I recommend you give this issue a high priority. I’m sure you can resolve this πŸ’ͺ


Status Details
  • βœ… coverage/coveralls First build on greenkeeper/eslint-plugin-flowtype-2.30.4 at 100.0% Details

  • ❌ continuous-integration/travis-ci/push The Travis CI build failed Details

Release Notes v2.30.4

<a name"2.30.4">

2.30.4 (2017-03-21)

Bug Fixes

Commits

The new version differs by 1 commits .

  • 636cead fix: no-dupe-keys with legal duplicated keys (#148)

See the full diff.

Not sure how things should work exactly?

There is a collection of frequently asked questions and of course you may always ask my humans.


Your Greenkeeper Bot 🌴

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.