reselect with TypeScript

2woongjae@gmail.com

Mark Lee (이웅재)

  • Studio XID inc. ProtoPie Engineer
  • Seoul.JS 오거나이저
  • 타입스크립트 한국 유저 그룹 오거나이저
  • 일렉트론 한국 유저 그룹 운영진
  • Seoul.js 오거나이저
  • Microsoft MVP - Visual Studio and Development Technologies
  • Code Busking with Jimmy
    • https://www.youtube.com/channel/UCrKE8ihOKHxYHBzI0Ys-Oow

Reselect with Redux

Reselect ★ 10152

2018.02

Reselect 주요 특징

  • Selector library for Redux
    • Selectors can compute derived data, allowing Redux to store the minimal possible state.
    • Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
    • Selectors are composable. They can be used as input to other selectors.
  • type definition 이 모듈 안에 존재
  • Redux 의 변경에 항상 재계산하지 않도록 중간에서 기억하고 변경될때만 재처리
  • 꼭 Redux 가 아니더라도 사용 가능하다.
  • transform 이 중요하다 ㅜ

redux 의 state 를 캐싱하기 위해 selector 로 만든다.

import { createSelector } from 'reselect'

const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent

const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal + tax })
)

let exampleState = {
  shop: {
    taxPercent: 8,
    items: [
      { name: 'apple', value: 1.20 },
      { name: 'orange', value: 0.95 },
    ]
  }
}

console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState))      // 0.172
console.log(totalSelector(exampleState))    // { total: 2.322 }

reselect-ts-quick-start

프로젝트 준비

~/Project/workshop-201801 
➜ npx create-react-app reselect-ts-quick-start --scripts-version=react-scripts-ts

~/Project/workshop-201801 took 1m 36s 
➜ cd reselect-ts-quick-start 

Project/workshop-201801/reselect-ts-quick-start is 📦 v0.1.0 via ⬢ v8.9.4 
➜ npm i redux -D
+ redux@3.7.2
added 3 packages in 9.771s

Project/workshop-201801/reselect-ts-quick-start is 📦 v0.1.0 via ⬢ v8.9.4 took 11s 
➜ npm i react-redux @types/react-redux -D
+ react-redux@5.0.6
+ @types/react-redux@5.0.14
added 3 packages in 8.452s

Project/workshop-201801/reselect-ts-quick-start is 📦 v0.1.0 via ⬢ v8.9.4 took 9s 
➜ npm i reselect -D                      
+ reselect@3.0.1
added 1 package in 7.86s

시나리오

  • state.persons 에 {name: string; age: number} 인 객체 배열을 넣는다.
  • 2초마다 state.count 를 1 씩 증가시킨다.
  • mapStateToProps 에서 state.persons 를 트랜스폼 시킨다.

    • ​그러면 다른 props 로 여겨서 count 가 올라갈때 같이 랜더가 실행된다.

  • state.persons 가 변경되지 않으면, 트랜스폼 결과를 기억해서 그대로 준다. 

index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import './index.css';
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker();

store.tsx

import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

reducer.tsx

import { AnyAction, combineReducers } from 'redux';

export const ADD_PERSON = 'ADD_PERSON';
export type ADD_PERSON = typeof ADD_PERSON;

export const CHANGE_COUNT = 'CHANGE_COUNT';
export type CHANGE_COUNT = typeof CHANGE_COUNT;

export interface State {
  persons: PersonsState;
  count: CounterState;
}

type PersonsState = {
  name: string;
  age: number;
}[];

type CounterState = number;

export function addPerson(name: string) {
  return {
    type: ADD_PERSON,
    name
  };
}

export function changeCount() {
  return {
    type: CHANGE_COUNT
  };
}

function personsReducer(
  state: PersonsState = [
    {
      name: 'Mark',
      age: 1
    }
  ],
  action: AnyAction
) {
  if (action.type === ADD_PERSON) {
    return [...state, { name: action.name, age: state.length + 1 }];
  } else {
    return state;
  }
}

function counterReducer(state: CounterState = 0, action: AnyAction) {
  if (action.type === CHANGE_COUNT) {
    return state + 1;
  } else {
    return state;
  }
}

export default combineReducers<State>({
  persons: personsReducer,
  count: counterReducer
});

App.tsx

import * as React from 'react';
import './App.css';
import Persons from './Persons';
import Counter from './Counter';
import store from './store';

const initial = store.getState();

const logo = require('./logo.svg');

export default class App extends React.Component {
  input: HTMLInputElement;
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <Persons />
        <Counter />
        <p>{JSON.stringify(initial)}</p>
      </div>
    );
  }
}

Counter.tsx

import * as React from 'react';
import './App.css';
import { Dispatch, AnyAction } from 'redux';
import { connect } from 'react-redux';
import { State, changeCount } from './reducer';

interface CounterStateProps {
  count: number;
}

interface CounterDispatchProps {
  changeCount: () => void;
}

class Counter extends React.Component<
  CounterStateProps & CounterDispatchProps
> {
  input: HTMLInputElement;
  componentDidMount() {
    setInterval(() => {
      this.props.changeCount();
      // tslint:disable-next-line:align
    }, 2000);
  }
  render() {
    return (
      <div>
        <p>{this.props.count}</p>
      </div>
    );
  }
}

const mapStateToProps = (state: State): CounterStateProps => {
  return {
    count: state.count
  };
};

const mapDispatchToProps = (
  dispatch: Dispatch<AnyAction>
): CounterDispatchProps => ({
  changeCount: () => dispatch(changeCount())
});

export default connect<CounterStateProps, CounterDispatchProps>(
  mapStateToProps,
  mapDispatchToProps
)(Counter);

Persons.tsx

import * as React from 'react';
import { Dispatch, AnyAction } from 'redux';
import { connect } from 'react-redux';
import { addPerson } from './reducer';

import { makeMapStateToProps } from './selector';

interface PersonsStateProps {
  persons: string[];
}

interface PersonsDispatchProps {
  addPerson: (name: string) => void;
}

class Persons extends React.Component<
  PersonsStateProps & PersonsDispatchProps
> {
  input: HTMLInputElement;
  render() {
    console.log(this.props.persons);
    return (
      <div className="App">
        <p className="App-intro">
          <input
            type="text"
            ref={ref => (this.input = ref as HTMLInputElement)}
          />
          <button onClick={() => this.props.addPerson(this.input.value)}>
            이름 추가
          </button>
        </p>
        <p className="App-intro">{JSON.stringify(this.props.persons)}</p>
      </div>
    );
  }
}

/*
const mapStateToProps = (state: State): PersonsStateProps => {
  return {
    persons: state.persons.map(person => person.name)
  };
};
*/

const mapDispatchToProps = (
  dispatch: Dispatch<AnyAction>
): PersonsDispatchProps => ({
  addPerson: name => dispatch(addPerson(name))
});

export default connect<PersonsStateProps, PersonsDispatchProps>(
  makeMapStateToProps,
  mapDispatchToProps
)(Persons);

selector.tsx

import { State } from './reducer';

import { createSelector } from 'reselect';

const makeGetPersons = () =>
  createSelector([(state: State) => state.persons], persons =>
    persons.map(person => person.name)
  );

export const makeMapStateToProps = () => {
  const getPersons = makeGetPersons();
  return (state: State) => {
    console.log('makeMapStateToProps');
    return {
      persons: getPersons(state)
    };
  };
};

[CODEBUSKING WORKSHOP] reselect with TypeScript

By Woongjae Lee

[CODEBUSKING WORKSHOP] reselect with TypeScript

코드버스킹 워크샵 - React with TypeScript 세번째 (2018년 1월 버전)

  • 1,199