git checkout step-4
npm install redux react-redux -S npm install @types/react-redux -D
{ "compilerOptions": { "lib": [ "es5", "es6", "es7", "dom" ], "target": "es5", "module": "esnext", "jsx": "react", "moduleResolution": "node" } }
/** * State * * , * . */ export interface FieldState { value: string; focus: boolean; } const initialState: FieldState = { value: '', focus: false } /** * Constants * * , . * , * . */ const SET = 'field/SET'; type SET = typeof SET; const FOCUS = 'field/FOCUS'; type FOCUS = typeof FOCUS; const BLUR = 'field/BLUR'; type BLUR = typeof BLUR; /** * Actions * * Redux TypeScript, * , (FieldAction) */ export interface SetAction { type: SET; payload: string; } export interface FocusAction { type: FOCUS; } export interface BlurAction { type: BLUR; } type FieldAction = SetAction | FocusAction | BlurAction; /** * Reducer * * , , * . * action , * (FieldAction), * ( case SET) * action. */ export default function reducer(state: FieldState = initialState, action: FieldAction): FieldState { switch (action.type) { case SET: return { ...state, value: action.payload } case FOCUS: return { ...state, focus: true } case BLUR: return { ...state, focus: false } default: return state; } } /** * Action Creators * * , * * , * . */ export const set = (payload: string): SetAction => ({ type: SET, payload }); export const focus = (): FocusAction => ({ type: FOCUS }); export const blur = (): BlurAction => ({ type: BLUR });
import { combineReducers } from 'redux'; import fieldReducer from './field'; export default combineReducers({ field: fieldReducer })
import { createStore } from 'redux'; import rootReducer from '../redux'; import { FieldState } from '../redux/field'; /** * mapStateToProps, * , * (, redux-thunk) */ export interface IStore { field: FieldState } /** * . */ const configureStore = (initialState?: IStore) => { return createStore( rootReducer, initialState, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ) }; export default configureStore;
interface Window { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; __REDUX_DEVTOOLS_EXTENSION__: any; }
import * as React from 'react'; import { connect, Dispatch, DispatchProp } from 'react-redux'; import { IStore } from '../store'; import { set, focus, blur } from '../redux/field'; /** * DispatchProp , dispatch * . connect, * ( mapDispatchToProps) */ interface FieldProps extends DispatchProp<IStore>, React.HTMLProps<HTMLInputElement> { value?: string; } class Field extends React.Component<FieldProps, {}> { handleChange = (event: React.FormEvent<HTMLInputElement>) => { const { dispatch } = this.props; const value = event.currentTarget.value; /** * set dispatch * . */ dispatch(set(value)); } handleFocus = () => { const { dispatch } = this.props; dispatch(focus()); } handleBlur = () => { const { dispatch } = this.props; dispatch(blur()); } render() { const { value, dispatch, ...inputProps } = this.props; return ( <input {...inputProps} type="text" value={value} onChange={this.handleChange} onFocus={this.handleFocus} onBlur={this.handleBlur} /> ); } } /** * mapStateToProps, ( ) * */ const mapStateToProps = (state: IStore, ownProps: FieldProps) => ({ value: state.field.value }); /** * mapDispatchToProps: * (dispatch: Dispatch<IStore>, ownProps: FieldProps) => ({ ... }) */ /** * connect , 10 * . * , 3 : * mapStateToProps, mapDispatchToProps, * . * , , * . */ export default connect<{}, {}, FieldProps>(mapStateToProps)(Field);
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import configureStore from './store'; import Field from './components/Field'; /** * , initialState * window.__INITIAL_STATE__, * . */ const store = configureStore(); /** * * DevTools */ const App = () => ( <Provider store={store}> <div> <h1>Hello, Redux!</h1> <Field placeholder='I like dev tools!' /> </div> </Provider> ); ReactDOM.render(<App />, document.getElementById('root'));
git checkout step-5
import { Middleware, MiddlewareAPI, Dispatch, Action } from 'redux'; import { IStore } from '../store'; /** * store - MiddlewareAPI<S & IStore> - * , * . , * middleware -. */ const logger: Middleware = <S>(store: MiddlewareAPI<S & IStore>) => (next: Dispatch<S>) => // - <A extends Action>(action: A), . (action: any) => { // store.getState().field.value; console.log('dispatching', action); let result = next(action); console.log('next state', store.getState()); return result; } export default logger;
import { createStore, compose, applyMiddleware } from 'redux'; import rootReducer from '../redux'; import { FieldState } from '../redux/field'; import logger from '../middlewares/logger'; export interface IStore { field: FieldState } let composeEnhancers = compose; // middleware const middlewares = [ logger ]; if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; } const configureStore = (initialState?: IStore) => { return createStore( rootReducer, initialState, composeEnhancers( applyMiddleware(...middlewares) ) ) }; export default configureStore;
import { Reducer, Action } from 'redux'; import { IStore } from '../store'; /** * , * , createNamedReducer */ export interface namedAction extends Action { name: string; } function createNamedReducer<S>(reducer: Reducer<S>, reducerName: string): Reducer<S> { return (state: S, action: namedAction) => { const { name } = action; const isInitializationCall = state === undefined; if (name !== reducerName && !isInitializationCall) { return state; } return reducer(state, action); } } export default createNamedReducer;
git checkout step-6
npm install axios redux-thunk -S
import axios from 'axios'; /** * redux * , . */ const client = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com' }); export default client;
import { AxiosPromise } from 'axios'; import client from './client'; // export interface IUser { id: number; name: string; username: string; email: string; address: any; phone: string; website: string; company: any; } export function get(id: number): AxiosPromise<IUser> { return client.get(`/users/${id}`); } export function getList(): AxiosPromise<IUser[]> { return client.get('/users'); }
import { Dispatch } from 'redux'; import { IStore } from '../store'; import * as client from '../services/users'; // type Error = any; // http interface AsyncState<D> { isFetching: boolean; error: Error; data: D; } // , interface AsyncAction<P> { status?: 'error' | 'success'; payload?: P | Error; } /** * State */ export interface UsersState { get: AsyncState<client.IUser>; getList: AsyncState<client.IUser[]>; } const initialState: UsersState = { get: { isFetching: false, error: null, data: null }, getList: { isFetching: false, error: null, data: [] } } /** * Constants */ const GET = 'users/GET'; type GET = typeof GET; const GET_LIST = 'users/GET_LIST'; type GET_LIST = typeof GET_LIST; /** * Actions */ export interface GetAction extends AsyncAction<client.IUser> { type: GET; } export interface GetListAction extends AsyncAction<client.IUser[]> { type: GET_LIST; } type UsersAction = GetAction | GetListAction; /** * Reducer * * , ! * , * , * . * , * , - . */ export default function reducer(state: UsersState = initialState, action: UsersAction): UsersState { switch (action.type) { case GET: if (!action.status) { return { ...state, get: { ...state.get, isFetching: true, error: null } } } if (action.status === 'error') { return { ...state, get: { isFetching: false, error: action.payload, data: null } } } return { ...state, get: { isFetching: false, error: null, data: action.payload } } case GET_LIST: if (!action.status) { return { ...state, getList: { ...state.getList, isFetching: true, error: null } } } if (action.status === 'error') { return { ...state, getList: { isFetching: false, error: action.payload, data: [] } } } return { ...state, getList: { isFetching: false, error: null, data: action.payload } } default: return state; } } /** * Action Creators */ export const getActionCreator = ( status?: 'error' | 'success', payload?: client.IUser | Error ): GetAction => ({ type: GET, status, payload, }); export const getListActionCreator = ( status?: 'error' | 'success', payload?: client.IUser[] | Error ): GetListAction => ({ type: GET_LIST, status, payload, }); /** * Thunk Actions */ export function get(id: number) { return async (dispatch: Dispatch<IStore>, getState: () => IStore) => { dispatch(getActionCreator()); try { const response = await client.get(id); dispatch(getActionCreator('success', response.data)); } catch (e) { dispatch(getActionCreator('error', e)); throw new Error(e); } } } export function getList() { return async (dispatch: Dispatch<IStore>, getState: () => IStore) => { dispatch(getListActionCreator()); try { const response = await client.getList(); dispatch(getListActionCreator('success', response.data)); } catch (e) { dispatch(getListActionCreator('error', e)); throw new Error(e); } } }
import { combineReducers } from 'redux'; import fieldReducer from './field'; import usersReducer from './users'; export default combineReducers({ field: fieldReducer, users: usersReducer });
import { createStore, compose, applyMiddleware } from 'redux'; import ReduxThunk from 'redux-thunk' import rootReducer from '../redux'; import { FieldState } from '../redux/field'; import { UsersState } from '../redux/users'; import logger from '../middlewares/logger'; export interface IStore { field: FieldState, users: UsersState } let composeEnhancers = compose; const middlewares = [ logger, ReduxThunk ]; if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; } const configureStore = (initialState?: IStore) => { return createStore( rootReducer, initialState, composeEnhancers( applyMiddleware(...middlewares) ) ) }; export default configureStore;
import * as React from 'react'; import { connect, Dispatch, DispatchProp } from 'react-redux'; import { IStore } from '../store'; import { getList, Error } from '../redux/users'; import { IUser } from '../services/users'; interface UsersProps extends DispatchProp<IStore> { isFetching?: boolean; error?: Error; users?: IUser[]; } class Users extends React.Component<UsersProps, {}> { componentDidMount() { const { dispatch } = this.props; dispatch(getList()); } render() { const { isFetching, error, users } = this.props; if (error) { return <b> !</b> } if (isFetching) { return '...'; } return users.map((user) => <div>{user.name}</div>); } } const mapStateToProps = (state: IStore, ownProps: UsersProps) => ({ isFetching: state.users.getList.isFetching, error: state.users.getList.error, users: state.users.getList.data }); export default connect<{}, {}, UsersProps>(mapStateToProps)(Users);
git checkout step-7
npm install core-js -S npm install @types/core-js -D
import 'core-js/es6/promise'; import 'core-js/es6/map'; import 'core-js/es6/set'; if (typeof window.requestAnimationFrame !== 'function') { window.requestAnimationFrame = (callback: FrameRequestCallback) => window.setTimeout(callback, 0); } ...
npm install better-npm-run -D
{ ... "scripts": { "start": "better-npm-run dev", "build": "better-npm-run build" }, "betterScripts": { "dev": { "command": "webpack-dev-server", "env": { "NODE_ENV": "development" } }, "build": { "command": "webpack", "env": { "NODE_ENV": "production" } } }, ... }
const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); // const env = process.env.NODE_ENV; const __DEV__ = env === 'development'; const __PRODUCTION__ = env === 'production'; const paths = { src: path.resolve(__dirname, 'src'), dist: path.resolve(__dirname, 'dist') }; const config = { context: paths.src, entry: { app: './index' }, // , output: { path: paths.dist, filename: __PRODUCTION__ ? '[name].bundle.[chunkhash].js' : '[name].bundle.js', chunkFilename: __PRODUCTION__ ? '[name].bundle.[chunkhash].js' : '[name].bundle.js' }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }, module: { rules: [ { test: /\.tsx?$/, loader: 'awesome-typescript-loader' } ] }, plugins: [ // NODE_ENV new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env) }), new HtmlWebpackPlugin({ template: './index.html' }), // new webpack.optimize.ModuleConcatenationPlugin() ] }; if (__DEV__) { // source map development config.devtool = 'inline-source-map'; } if (__PRODUCTION__) { config.plugins.push(new CleanWebpackPlugin(['dist'])); // config.plugins.push(new webpack.optimize.UglifyJsPlugin()); } module.exports = config;
npm run build
Source: https://habr.com/ru/post/341132/
All Articles