Giter Club home page Giter Club logo

react-redux-controller's Introduction

react-redux-controller

react-redux-controller is a library that adds some opinion to the react-redux binding of React components to the Redux store. It creates the entity of a Controller, which is intended to be the single point of integration between React and Redux. The controller passes data and callbacks to the UI components via the React context. It's one solution to the question of how to get data and controller-like methods (e.g. event handlers) to the React UI components.

Note Although react-redux-controller continues to be used in production at Artsy, it is not actively being developed. We may or may not continue to develop it. The issues on the future of the architecture should be considered a doucment on lessons learned, rather than an intention to actually release a future version. Anyone should feel free use this code as-is or develop their own fork.

Philosophy

This library takes the opinion that React components should solely be focused on the job of rendering and capturing user input, and that Redux actions and reducers should be soley focused on the job of managing the store and providing a view of the state of the store in the form of selectors. The plumbing of distributing data to components, as well as deciding what to fetch, when to fetch, how to manage latency, and what to do with error handling, should be vested in an explicit controller layer.

This differs from alternative methods in a number of ways:

  • The ancestors of a component are not responsible for conveying dependencies to via props -- particularly when it comes to dependencies the ancestors don't use themselves.
  • The components are not coupled to Redux in any way -- no connect distributed throughout the component tree.
  • There are no smart components. Well there's one, but it's hidden inside the Controller.
  • Action creators do not perform any fetching. They are only responsible for constructing action objects, as is the case in vanilla Redux, with no middleware needed.

Usage

The controller factory requires 3 parameters:

  • The root component of the UI component tree.
  • An object that holds controller generator functions.
  • Any number of selector bundles, which are likely simply imported selector modules, each selector annotated a propType that indicates what kind of data it provides.

The functionality of the controller layer is implemented using generator functions. Within these functions, yield may be used await the results of Promises and to request selector values and root component properties. As a very rough sketch of how you might use this library:

// controllers/app_controller.js

import { controller, getProps } from 'react-redux-controller';
import AppLayout from '../components/app_layout';
import * as actions from '../actions';
import * as mySelectors from '../selectors/my_selectors';

const controllerGenerators = {
  *initialize() {
    // ... any code that should run before initial render (like loading actions)
  },
  *onUserActivity(meaningfulParam) {
    const { dispatch, otherData } = yield getProps;
    dispatch(actions.fetchingData());
    try {
      const apiData = yield httpRequest(`http://myapi.com/${otherData}`);
      return dispatch(actions.fetchingSuccessful(apiData));
    } catch (err) {
      return dispatch(actions.errorFetching(err));
    }
  },
  // ... other controller generators follow
};

const selectorBundles = [
  mySelectors,
];

export default controller(AppLayout, controllerGenerators, selectorBundles);

Example

To see an in-depth example of usage of this library, the async example from the redux guide is ported to use the controller approach in this repo.

react-redux-controller's People

Contributors

acjay avatar artptr avatar artsyit avatar ashfurrow avatar cogell avatar damassi avatar dependabot[bot] avatar icirellik avatar joeyaghion avatar yn5 avatar

Stargazers

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

Watchers

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

react-redux-controller's Issues

Couple controllers to selectors using `this`

Building on #14, I believe it's possible to eliminate the generator from RRC.

Right now, controllers rely on generators to resolve promises and to get access to the selectors and dispatch. They use this to access each other. A dance happens in RRC to convert those controller generator methods into vanilla methods.

The reason generators are used is that it allows us to retrieve a "property getter" function, which will return the latest version of the selectors. If the selectors were simple properties, they could be stale in callbacks within the controllers. I didn't want to wrap properties in functions so that they would be expliticly called to retrieve their values at time of use. I'm not sure I was aware of Javascript's getter mechanism, which allows for this without requiring the caller to explicitly invoke them. That inspired #14.

Once that is done, all that's left to do is efficiently get all of these getters and dispatch on the controller instance. This seems extremely doable, and it's just a matter of choosing the right mechanism (mixin, Object.assign, prototype, whatever). And then, we'd just let the caller choose their preferred async mechanism, rather than baking co into RRC.

Performance issues to be reckoned with

According to the react-redux README, the approach of using a single connect to listen to the store for the whole component tree is a potential performance landmine.

How bad is it, if the whole component tree is stateless? I don't know. We have a lot of features to add in the project this is being piloted in. We do know we're going to see the worst possible scenario because every component depends on the React context, which changes every time the state changes, which is probably every user- or service-initiated action. This won't lead to repaints, but it will lead to a lot of throwaway React work.

That said, all the information to optimize this is readily available, especially since all components are dumb and pure. We know, at build-time, all the possible child components of a component. We know, at build-time, all the data dependencies of a component, via propTypes and contextTypes. So we should be able to pretty easily derive reasonable shouldComponentUpdate implementations at places where we find optimization to be necessary.

How to use this information best, e.g. with the least amount of boilerplate, is the question.

Selector dependencies using `this`

Right now, RRC provides no real help for the selector story. In our use case at Artsy, we have a great deal of selectors, which depend on the state and on each other. However, writing these selectors as functions from state to a derived value is a bit awkward, composing them is even more so, and using something like reselect for memoization is so awkward that we don't even bother.

In the example, we have:

import { PropTypes } from 'react'

export const selectedReddit = state => state.selectedReddit
selectedReddit.propType = PropTypes.string.isRequired

export const postsByReddit = state => state.postsByReddit
postsByReddit.propType = PropTypes.object.isRequired

export const posts = state => {
  const p = state.postsByReddit[selectedReddit(state)]
  return p && p.items ? p.items : []
}
posts.propType = PropTypes.array.isRequired

export const isFetching = state => {
  const posts = state.postsByReddit[selectedReddit(state)]
  return posts ? posts.isFetching : true
}
isFetching.propType = PropTypes.bool.isRequired

export const lastUpdated = state => {
  const posts = state.postsByReddit[selectedReddit(state)]
  return posts && posts.lastUpdated
}
lastUpdated.propType = PropTypes.number

The functional composition works, but it's also pretty noisy. A better API might be:

export class Selectors {
  get posts() {
    const p = this.postsByReddit[this.selectedReddit]
    return p && p.items ? p.items : []
  }

  get isFetching() {
    const posts = this.postsByReddit[this.selectedReddit]
    return posts ? posts.isFetching : true
  }

  get lastUpdated() {
    const posts = this.postsByReddit[this.selectedReddit]
    return posts && posts.lastUpdated
  }
}

Selectors.propTypes = {
  selectedReddit: PropTypes.string.isRequired,
  postsByReddit: PropTypes.object.isRequired,
  posts: PropTypes.array.isRequired,
  isFetching: PropTypes.bool.isRequired,
  lastUpdated: PropTypes.number
};

This feels much more direct and intuitive. Library magic would make sure that the Redux state is accessible via this.

Selectors would also become lazy, instead of eagerly computed. It may also be possible to rely on said library magic to opt selectors into memoization.

is it compatible with React 0.15?

I have warnings about manually validating PropType:

Warning: You are manually calling a React.PropTypes validation function for the `routing` prop on `Controller`. This is deprecated and will not work in production with the next major version. You may be seeing this warning due to a third-party PropTypes library. See https://fb.me/react-warning-dont-call-proptypes for details.

how to mark controller's method as "blocking"?!

Required:
somehow inside of controller without a lot of boilerplate code to get next: at begin of method dispatch action with promise for promise-middleware, that will be resolved/rejected at the end of method.

Recap:

  1. at begin of method set some redux state to true
  2. at end of method set same redux state to false
  3. at error do same as at (2)

In most cases it's common loading flag (e.g. API requests) to make UI to response changes of state (e.g. block/unblock controls).

Right now I'm doing it like this, but not sure that it's good way, maybe I miss something:

helper:

const blocking = (type, method, dispatch) => (...args)  => {
	return dispatch({ type, payload: new Promise((resolve, reject) => {
		try {
			resolve(method(...args));
		} catch (e) {
			reject(e);
		}
	})});
};

generators with marked onLogin as blocking:

const generators = {
	*initialize() {
		const { dispatch } = yield getProps;

		this.onLogin = blocking(types.APP_PREFIX, this.onLogin, dispatch);

		yield this.userInfo(true, '/');
	},

	*onSiderCollapse() {
		const { dispatch } = yield getProps;
		dispatch(actions.siderCollapse());
	},

	*onLogin(params) {
		const { dispatch } = yield getProps;

			try {
				yield api.login(params);
				yield this.userInfo(false, '/');
			} catch(e) {
				dispatch(error(e)),
				dispatch(auth.reset());
			}
	},

	*onLogout() {
		const { dispatch } = yield getProps;

		try {
			yield api.logout();
		} catch(e) {
		}

		dispatch(auth.reset());
		dispatch(navigate('/'));
	},

	*userInfo(silent = false, returnUrl) {
		const { dispatch, route } = yield getProps;

		try {
			dispatch(auth.request());
			const identity = yield api.fetchCurrrentUser();
			dispatch(auth.receive(identity));

			//if route has url to return, then use it
			let redirect = route && route.query && route.query.return;
			if(!redirect) {
				redirect = returnUrl;
			}

			if(redirect) {
				dispatch(navigate(redirect));
			}
		} catch(e) {
			console.log(e);

			if(!silent) {
				dispatch(error(e));
			}

			dispatch(auth.reset());
			dispatch(navigate('/'));
		}
	}
};

I need it for my reducer, e.g.:

import typeToReducer from 'type-to-reducer';
import * as loading from '../../utils/loading';
import types from './actions';

const initialState = {
	checking: false,
	siderCollapsed: false,
};

const app = typeToReducer({
	[types.SIDER_COLLAPSE]: (state, action) => ({ ...state, siderCollapsed: !state.siderCollapsed }),
	[types.APP_PREFIX]: loading.reducers('checking'),
}, initialState);

export default app;

utils is like this:

export const promiseTypeSuffixes = ['pending', 'fulfilled', 'rejected'];

export const reducers = (key, suffixes = promiseTypeSuffixes) => {
	const reducers = { };
	suffixes.forEach((suffix, i) => reducers[suffix] = (state, action) => ({ ...state, [key]: !i }));
	return reducers;
};

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.