Skip to content

Instantly share code, notes, and snippets.

@pasaran
Created May 8, 2019 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pasaran/021740a534ab4c251c99a122b237d529 to your computer and use it in GitHub Desktop.
Save pasaran/021740a534ab4c251c99a122b237d529 to your computer and use it in GitHub Desktop.
Anti Redux
const React = require( 'react' );
const { connect } = require( './store' );
class App extends React.Component {
componentDidMount() {
this.props.items.load_more();
}
render() {
return (
<div>
{ this.render_count() }
{ this.render_items() }
</div>
);
}
render_items() {
let more;
let loading;
// В каждом сторе могут быть и данные и экшены.
// И, скажем, селекторы могли бы быть.
//
// Тут свойство используется.
if ( this.props.items.loading ) {
loading = 'Loading...';
} else {
// Тут экшен используется.
more = (
<div onClick={ () => this.props.items.load_more() }>More...</div>
);
}
const items = this.props.items.items.map( item => {
return (
<div key={ item }>{ item }</div>
);
} );
return (
<div>
{ items }
{ more }
{ loading }
</div>
);
}
render_count() {
return (
<div>
{ this.props.count.count }
<div onClick={ () => this.props.count.increment() }>[ + ]</div>
<div onClick={ () => this.props.count.decrement() }>[ - ]</div>
</div>
);
}
}
// Компонент коннектится к конкретным сторам.
// Любое использование компонента притащит за собой все нужные сторы, экшены и редьюсеры.
//
module.exports = connect( {
items: require( './store-items' ),
count: require( './store-count' ),
}, App );
<html>
<body>
<div id="root"></div>
<script src="build/index.js"></script>
</body>
</html>
const React = require( 'react' );
const ReactDOM = require( 'react-dom' );
const App = require( './App' );
const root = document.getElementById( 'root' );
ReactDOM.render( <App/>, root );
{
"dependencies": {
"react": "*",
"react-dom": "*"
},
"devDependencies": {
"@babel/core": "*",
"@babel/preset-env": "*",
"@babel/preset-react": "*",
"babel-loader": "*",
"webpack": "*",
"webpack-cli": "*"
}
}
const { create_store } = require( './store' );
const initial_state = {
count: 0,
};
module.exports = create_store( initial_state, {
// Это экшен, который сразу вызывает нужные редьюсер.
// Без всяких промежуточных объектов для экшена.
// Не нужно заводить action-type, не нужно потом его матчить внутри гигантского редьюсера.
//
increment( by = 1 ) {
// В this.update можно передать или новое значение стейта,
// или же редьюсер, в который передается стейт из которого нужно вернуть
// новое значение стейта.
//
this.update( ( state ) => ( { ...state, count: state.count + by } ) );
},
decrement( by = 1 ) {
this.update( ( state ) => ( { ...state, count: state.count - by } ) );
},
} );
const { create_store } = require( './store' );
const initial_state = {
loading: false,
items: [],
};
module.exports = create_store( initial_state, {
load_more() {
// Тут не нужно заводить отдельный action-type,
// чтобы только поменять значение флага loading.
//
this.update( ( state ) => ( { ...state, loading: true } ) );
// Типа асинхронный экшен.
//
setTimeout( () => {
this.update( ( state ) => {
const items = state.items;
const more_items = [ 1, 2, 3 ].map( () => Math.floor( Math.random() * 10000 ) );
return {
...state,
loading: false,
items: items.concat( more_items ),
};
} );
}, 1000 );
},
} );
const React = require( 'react' );
class Store {
constructor( initial_state, actions ) {
this._state = initial_state;
this._actions = {};
if ( actions ) {
for ( let name in actions ) {
this._actions[ name ] = actions[ name ].bind( this );
}
}
this._callbacks = [];
}
get_state() {
return this._state;
}
update( reducer ) {
const new_state = ( typeof reducer === 'function' ) ? reducer( this._state ) : reducer;
if ( new_state !== undefined && new_state !== this._state ) {
this._state = new_state;
Promise.resolve().then( () => {
this._callbacks.forEach( ( callback ) => callback( new_state ) );
} );
}
}
subscribe( callback ) {
this._callbacks.push( callback );
const unsubscribe = () => {
const index = this._callbacks.findIndex( callback );
if ( index !== -1 ) {
this._callbacks.splice( index, 1 );
}
};
return unsubscribe;
}
}
function create_store( state, actions ) {
return new Store( state, actions );
}
function connect( stores, component ) {
const connected = class extends React.Component {
constructor( props ) {
super( props );
const state = {};
for ( let key in stores ) {
const store = stores[ key ];
state[ key ] = Object.assign( {}, store.get_state(), store._actions );
}
this.state = state;
}
componentDidMount() {
this.unsubscribes = [];
for ( let key in stores ) {
const store = stores[ key ];
this.unsubscribes.push( store.subscribe( ( new_state ) => {
this.setState( {
[ key ]: Object.assign( {}, new_state, store._actions ),
} );
} ) );
}
}
componentWillUnmount() {
this.unsubscribes.forEach( ( unsubscribe ) => unsubscribe() );
}
render() {
return React.createElement( component, { ...this.props, ...this.state } );
}
};
connected.displayName = `Connect(${ component.name })`;
return connected;
}
module.exports = {
create_store: create_store,
connect: connect,
};
const path_ = require( 'path' );
const webpack = require( 'webpack' );
module.exports = {
mode: 'development',
entry: {
index: './index.js',
},
output: {
filename: '[name].js',
path: path_.resolve(__dirname, 'build'),
publicPath: '/build/',
},
module: {
rules: [
{
test: /\.js$/,
exclude: [
/node_modules/,
],
use: {
loader: 'babel-loader',
},
},
],
},
resolve: {
extensions: [ '.js' ],
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
],
};
@pasaran
Copy link
Author

pasaran commented May 8, 2019

Что мне не нравится в redux/react-redux:

  • Нет никакой жесткой зависимости между законнекченным компонентом и данными в сторе. Нет никакой гарантии, что в сторе будет нужные данные, если мы просто используем компонент.
  • Точно так же, если компонент использует какие-то экшены, нет никакой гарантии, что в сторе предусмотрены соответствующие редьюсеры.
  • Избыточность цепочки "вызов экшена" -> "объект экшена" -> "общий редьюсер" -> "редьюсер экшена". На самом деле мы всегда знаем, какой редьюсер в конечном итоге должен быть вызван. Непонятно, зачем тогда нужен этот объект { type, payload }, который потом нужно еще засунуть в гигантский switch. И зачем на каждый чих заводить отдельный тип экшена. Вызов экшена должен сразу же вызывать свой редьюсер и менять стор.

Тут некий proof of concept. Основные файлы: App.js, store-count.js, store-items.js, store.js.

Собрать пример так:

$ npm i
$ ./node_modules/.bin/webpack
$ open index.html

Главная идея:

  • Стор содержит и данные и экшены.
  • Компонент коннектится к конкретным сторам. Тем самым можно просто использовать компонент. С ним приедут нужные данные, экшены и редьюсеры.
  • Экшен сам вызывает нужные редьюсеры, тем самым сильно меньше boilerplate нужно.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment