Skip to content

Instantly share code, notes, and snippets.

@kotarella1110
Last active November 21, 2018 05:45
Show Gist options
  • Save kotarella1110/3c42138ba0d53d1eba0cecd6fee23dbc to your computer and use it in GitHub Desktop.
Save kotarella1110/3c42138ba0d53d1eba0cecd6fee23dbc to your computer and use it in GitHub Desktop.
Counter app example - TypeScript + React + Redux
import * as React from 'react';
import { Store, createStore, combineReducers } from 'redux';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { reducer as counter } from './counter';
import Counter from './Counter';
const store: Store = createStore(
combineReducers({
counter,
})
);
interface IProps {
compiler: string;
framework: string;
}
const App: React.SFC<IProps> = () => (
<Provider store={store}>
<Counter title="Counter" />
</Provider>
);
ReactDOM.render(
<App compiler="TypeScript" framework="React" />,
document.getElementById('app') as HTMLElement
);
import { actions, reducer } from './counter';
describe('counter actions', () => {
it('increment should create counter/INCREMENT action', () => {
expect(actions.increment()).toEqual({
type: 'counter/INCREMENT',
});
});
it('decrement should create counter/DECREMENT action', () => {
expect(actions.decrement()).toEqual({
type: 'counter/DECREMENT',
});
});
});
describe('counter reducer', () => {
it('should handle counter/INCREMENT', () => {
expect(reducer(0, actions.increment())).toEqual(1);
});
it('should handle counter/DECREMENT', () => {
expect(reducer(1, actions.decrement())).toEqual(0);
});
});
import * as React from 'react';
import { shallow } from 'enzyme';
import { Counter } from '.';
const setup = (overrideProps = {}) => {
const props = Object.assign(
{
title: 'Counter',
counter: {
isLoading: false,
errorMessage: '',
count: 0,
},
increment: jest.fn(),
decrement: jest.fn(),
},
overrideProps
);
const component = shallow(<Counter {...props} />);
return {
component,
props,
h1: component.find('h1'),
p: component.find('p'),
buttons: component.find('button'),
};
};
describe('Counter component', () => {
it('should display title', () => {
const { h1, props } = setup();
expect(h1.text()).toBe(props.title);
});
it('should display counter.isLoading', () => {
const { buttons } = setup({ counter: { isLoading: true } });
expect(buttons.at(0).prop('disabled')).toBeTruthy();
expect(buttons.at(1).prop('disabled')).toBeTruthy();
});
it('should display counter.errorMessage', () => {
const { p } = setup({ counter: { errorMessage: 'Request failed' } });
expect(p.at(1).text()).toBe('Request failed');
});
it('should display counter.count', () => {
const { p, props } = setup();
expect(p.text()).toBe(`Clicked: ${props.counter.count} times`);
});
it('first button should call increment', () => {
const { buttons, props } = setup();
buttons.at(0).simulate('click');
expect(props.increment).toBeCalled();
});
it('second button should call decrement', () => {
const { buttons, props } = setup();
buttons.at(1).simulate('click');
expect(props.decrement).toBeCalled();
});
});
import { Action as AnyAction, Reducer } from 'redux';
export type Meta = null | { [key: string]: any };
export interface FSA<Type extends string, Payload = null> extends AnyAction {
type: Type;
payload?: Payload;
error?: boolean;
meta?: Meta;
}
enum ActionType {
increment = 'counter/INCREMENT',
decrement = 'counter/DECREMENT',
}
export type Action = FSA<ActionType.increment> | FSA<ActionType.decrement>;
const increment = (): Action => {
return { type: ActionType.increment };
};
const decrement = (): Action => {
return { type: ActionType.decrement };
};
export const actions = { increment, decrement };
export interface State {
readonly counter: number;
}
const initialState = 0;
export const reducer: Reducer<State['counter'], Action> = (
state = initialState,
action
) => {
switch (action.type) {
case ActionType.increment:
return state + 1;
case ActionType.decrement:
return state - 1;
default:
return state;
}
};
import * as React from 'react';
import { Dispatch, bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Action, State, actions } from './counter';
export interface IProps {
readonly title: string;
}
export interface IStateProps {
readonly counter: number;
}
export interface IDispatchProps extends ReturnType<typeof mapDispatchToProps> {}
export const Counter: React.SFC<IProps & IStateProps & IDispatchProps> = ({
title,
counter,
increment,
decrement,
}): JSX.Element => {
const handleIncrement = (e: React.MouseEvent<HTMLButtonElement>) =>
increment();
const handleDecrement = (e: React.MouseEvent<HTMLButtonElement>) =>
decrement();
return (
<div>
<h1>{title}</h1>
<p>Clicked: {counter} times</p>
<button onClick={handleIncrement}>+</button>
<button onClick={handleDecrement}>-</button>
</div>
);
};
const mapStateToProps = (state: State): IStateProps => ({
counter: state.counter,
});
const mapDispatchToProps = (dispatch: Dispatch<Action>) =>
bindActionCreators(actions, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment