Skip to content

Instantly share code, notes, and snippets.

@pasaran
Created July 7, 2019 20:08
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/8c2f0f0a167452a75fd4a9e945bd54ba to your computer and use it in GitHub Desktop.
Save pasaran/8c2f0f0a167452a75fd4a9e945bd54ba to your computer and use it in GitHub Desktop.
Redux replacement in typescript
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: true,
},
},
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
],
};
build
node_modules
package-lock=false
import { Store } from './Store';
interface State {
count: number;
}
const initial_state: State = {
count: 0,
};
export default new Store( initial_state, {
increment( n = 1 ) {
this.update( ( state ) => ( { ...state, count: state.count + n } ) );
},
decrement( n = 1 ) {
this.update( ( state ) => ( { ...state, count: state.count - n } ) );
},
} );
<html>
<body>
<div id="root"></div>
<script src="build/index.js"></script>
</body>
</html>
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Items from './Items';
const root = document.getElementById( 'root' );
ReactDOM.render( React.createElement( React.Fragment, null,
React.createElement( Items, { by: 5 } ),
React.createElement( Items )
), root );
import * as React from 'react';
import { connect, ConnectedProps } from './Store';
import items_store from './ItemsStore';
import count_store from './CountStore';
const stores = {
items: items_store,
count: count_store,
};
interface State {
clicks: number;
}
interface OwnProps {
by: number;
}
type Props = OwnProps & ConnectedProps<typeof stores>;
class Items extends React.Component<Props, State> {
public static defaultProps: Partial<OwnProps> = {
by: 1,
}
state: State = {
clicks: 0,
}
componentDidMount() {
this.props.items.load_more();
}
render() {
let more;
let loading;
if ( this.props.items.loading ) {
loading = 'Loading...';
} else {
more = (
<div className="Items__load-more" onClick={ this.props.items.load_more }>More...</div>
);
}
const items = this.props.items.items.map( item => {
return (
<div className="Item" key={ item }>{ item }</div>
);
} );
return (
<div className="Items">
{ items }
{ more }
{ loading }
<div className="Items__count">
{ this.props.count.count }
<div onClick={ () => this.increment() }>[ + ]</div>
<div onClick={ () => this.decrement() }>[ - ]</div>
</div>
<div className="Items__clicks">
clicks: { this.state.clicks }
</div>
</div>
);
}
increment() {
this.setState({
clicks: this.state.clicks + 1,
})
this.props.count.increment( this.props.by );
}
decrement() {
this.setState({
clicks: this.state.clicks + 1,
})
this.props.count.decrement( this.props.by );
}
}
export default connect( stores, Items );
import { Store } from './Store';
type State = {
loading: boolean,
items: Array<number>,
};
const initial_state: State = {
loading: false,
items: [],
};
export default new Store( initial_state, {
load_more() {
const state = this.state;
if ( state.loading ) {
return;
}
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 );
},
} );
{
"dependencies": {
"react": "*",
"react-dom": "*"
},
"devDependencies": {
"@babel/core": "*",
"@babel/preset-env": "*",
"@babel/preset-react": "*",
"@babel/preset-typescript": "*",
"@types/react": "*",
"@types/react-dom": "*",
"babel-loader": "*",
"ts-loader": "*",
"typescript": "*",
"webpack": "*",
"webpack-cli": "*"
}
}
import * as React from 'react';
type StoreSubscribeCallback<State> = ( state: State ) => void;
type StoreUnsubscribeCallback = () => void;
export class Store<State, Actions> {
private _state: State;
get state() {
return this._state;
}
private callbacks: Array<StoreSubscribeCallback<State>>;
readonly actions: Actions & ThisType<Store<State, Actions>>;
constructor( initial_state: State, actions: Actions & ThisType<Store<State, Actions>> ) {
this._state = initial_state;
this.callbacks = [];
this.actions = {} as Actions & ThisType<Store<State, Actions>>;
if ( actions ) {
for ( const name in actions ) {
const action = actions[ name ];
this.actions[ name ] = ( action as unknown as Function).bind( this );
}
}
}
update( reducer: ( state: State ) => State | State ) {
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: StoreSubscribeCallback<State> ): StoreUnsubscribeCallback {
this.callbacks.push( callback );
return () => {
this.callbacks = this.callbacks.filter( ( item ) => item !== callback );
};
}
}
type ConnectedProp<State, Actions> = State & Actions & ThisType<Store<State, Actions>>;
export type ConnectedProps<Stores> = {
[ K in keyof Stores ]: Stores[ K ] extends Store<infer State, infer Actions> ? ConnectedProp<State, Actions> : never;
}
export function connect<
Stores extends {
[ K in keyof Stores ]: Stores[ K ] extends Store<infer State, infer Actions> ? Store<State, Actions> : never;
},
OwnProps = {},
OwnState = {}
>( stores: Stores, component: React.ComponentClass<ConnectedProps<Stores> & OwnProps, OwnState> ): React.ComponentClass<OwnProps> {
const name = `Connect(${ component.name })`;
const o = {
[ name ]: class extends React.Component<OwnProps, ConnectedProps<Stores>> {
unsubscribes: Array<StoreUnsubscribeCallback>;
constructor( props: OwnProps ) {
super( props );
const state = {} as Record<string, any>;
for ( let key in stores ) {
const store = stores[ key ];
state[ key ] = { ...store.state, ...store.actions };
}
this.state = state as ConnectedProps<Stores>;
}
componentDidMount() {
this.unsubscribes = [];
for ( let key in stores ) {
const store = stores[ key ];
this.unsubscribes.push( store.subscribe( ( new_state ) => {
this.setState( {
[ key ]: { ...new_state, ...store.actions }
} as ConnectedProps<Stores> );
} ) );
}
}
componentWillUnmount() {
this.unsubscribes.forEach( unsubscribe => unsubscribe() );
}
render() {
return React.createElement( component, { ...this.props, ...this.state } );
}
},
};
return o[ name ];
}
{
"compilerOptions": {
"alwaysStrict": true,
"strictBindCallApply": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noEmitOnError": true,
"strictNullChecks": true,
"keyofStringsOnly": true,
"jsx": "react",
"target": "es2015",
"lib": [
"DOM",
"ES2015",
"ScriptHost"
],
"module": "commonjs",
"moduleResolution": "node"
},
"files": [
"index.tsx"
]
}
const path_ = require( 'path' );
const webpack = require( 'webpack' );
module.exports = {
mode: 'development',
entry: {
index: './index.tsx',
},
output: {
filename: '[name].js',
path: path_.resolve(__dirname, 'build'),
publicPath: '/build/',
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader',
},
{
test: /\.jsx?$/,
exclude: [
/node_modules/,
],
use: {
loader: 'babel-loader',
// options: babelOptions,
},
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment