Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
1) What is MobX ??
2) 프로젝트에 Decorator 설정하기
3) @observable (by mobx)
4) @observer (by mobx-react)
5) @computed (by mobx)
6) @action (by mobx)
7) @inject 와 Provider
8) mobx-devtools
9) stores
10) Asynchronous actions
2017.07
2018.01
데코레이터를 적극 활용한다.
cra 에 데코레이터를 사용하는 법...
스토어 객체에 붙이는 데코레이터가 있고, => @observable
컴포넌트에서 사용하는 데코레이터가 있다. => @observer
TypeScript 가 Base 인 라이브러리이다
Redux 와 마찬가지로, 스토어에 필요한 부분과 리액트에 필요한 부분이 있다.
npm i mobx -D
npm i mobx-react -D
리덕스와 다르게 단일 스토어를 강제하진 않는다.
데코레이터의 적극적인 사용으로 인해, 깔끔한 구성
하지만, 단일 스토어가 아니다.
https://github.com/gothinkster/react-mobx-realworld-example-app
결국 최상위 스토어를 만들고, props 로 공유해가는 방식으로
스토어를 어떻게 사용지에 대한 적합한 해결을 모색해야 할 것
npm i customize-cra react-app-rewired -D
const { override, addDecoratorsLegacy } = require('customize-cra');
module.exports = override(addDecoratorsLegacy());
{
"compilerOptions": {
"experimentalDecorators": true
}
}
import { observable, action, makeObservable } from 'mobx';
class BookStore {
@observable
books = [];
@observable
loading = false;
@observable
error = null;
constructor() {
makeObservable(this);
}
@action
start = () => {
this.loading = true;
this.books = [];
this.error = null;
};
@action
success = (books) => {
this.loading = false;
this.books = books;
this.error = null;
};
@action
fail = (error) => {
this.loading = false;
this.books = [];
this.error = error;
};
}
export default BookStore;
import { Provider } from 'mobx-react';
import BookStore from './mobx/BookStore';
const bookStore = new BookStore();
function App() {
return (
<ErrorBoundary FallbackComponent={Error}>
<Provider bookStore={bookStore}>
<ConnectedRouter history={history}>
<Switch>
<Route path="/add" component={Add} />
<Route path="/signin" component={Signin} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</ConnectedRouter>
</Provider>
</ErrorBoundary>
);
}
export default App;
import React from 'react';
import BookList from '../components/BookList';
import { inject, observer } from 'mobx-react';
import BookService from '../services/BookService';
@inject('bookStore')
@observer
class BookListContainer extends React.Component {
render() {
const { books, loading, error } = this.props.bookStore;
return (
<BookList
books={books}
loading={loading}
error={error}
getBooks={this.getBooks}
/>
);
}
getBooks = async () => {
const { bookStore } = this.props;
const token = localStorage.getItem('token');
try {
bookStore.start();
const books = await BookService.getBooks(token);
bookStore.success(books);
} catch (error) {
bookStore.fail(error);
}
};
}
export default BookListContainer;
export default inject('bookStore')(
observer(function BookListContainer({ bookStore }) {
const { books, loading, error } = bookStore;
const getBooks = useCallback(async () => {
try {
bookStore.start();
const token = localStorage.getItem('token');
const books = await BookService.getBooks(token);
bookStore.success(books);
} catch (error) {
bookStore.fail(error);
}
}, [bookStore]);
return (
<BookList
books={books}
loading={loading}
error={error}
getBooks={getBooks}
/>
);
}),
);
observable(<value>)
데코레이터 없이 사용하는 방식
@ 없이, 함수처럼 사용해서 리턴한 객체를 사용
@observable <클래스의 프로퍼티>
데코레이터로 사용하는 법
클래스 내부에 프로퍼티 앞에 붙여서 사용
한 클래스 안에 여러개의 @observable 존재
import { observable } from 'mobx';
// array 에 사용
const list = observable([1, 2, 4]);
// boolean 에 사용
const isLogin = observable(true);
// literal 객체에 사용
const age = observable({
age: 35
});
// 클래스의 멤버 변수에 데코레이터로 사용
class AgeStore {
@observable
private _age = 35;
}
const ageStore = new AgeStore();
observer(<컴포넌트>);
데코레이터 없이 사용하는 방식
함수 컴포넌트에 사용
<컴포넌트 클래스> 에 @observer 달아서 처리
클래스 컴포넌트에 사용
computed(내부에서 observable 을 사용하는 함수);
데코레이터 없이 사용하는 방식
<observable 클래스> 의 getter 에 @computed 달아서 처리
스토어에 사용
getter 에만 붙일수 있다.
함수가 아니라 리액티브 하다는 것에 주목
실제 컴포넌트에서 사용하는 (게터)값들에 달아서 사용하면 최소 범위로 변경할 수 있기 때문에 유용하다.
40살이 넘었을때만 나이를 올리면 40살 이하일때는 재랜더링 대상이 아닌 것과 같은 경우
내부적으로 고도의 최적화 => 어떻게 ?
매번 재계산을 하지 않는다
계산에 사용할 observable 값이 변경되지 않으면 재실행하지 않음.
다른 computed 또는 reaction 에 의해 호출되지 않으면 재실행하지 않음.
observable 이 변했는데 computed 가 변하지 않을때 랜더하지 않음.
npm i mobx-react-devtools -D
import DevTools from 'mobx-react-devtools';
class MyApp extends React.Component {
render() {
return (
<div>
...
<DevTools />
</div>
);
}
}
import AuthStore from './AuthStore';
import BookStore from './BookStore';
class RootStore {
constructor() {
this.authStore = new AuthStore(this);
this.bookStore = new BookStore(this);
}
}
export default RootStore;
import { observable, action, makeObservable } from 'mobx';
class BookStore {
@observable
books = [];
@observable
loading = false;
@observable
error = null;
constructor(rootStore) {
makeObservable(this);
this.rootStore = rootStore;
}
@action
start = () => {
this.loading = true;
this.books = [];
this.error = null;
};
@action
success = (books) => {
this.loading = false;
this.books = books;
this.error = null;
};
@action
fail = (error) => {
this.loading = false;
this.books = [];
this.error = error;
};
}
export default BookStore;
import { observable, action, makeObservable } from 'mobx';
class AuthStore {
@observable
token = [];
@observable
loading = false;
@observable
error = null;
constructor(rootStore) {
makeObservable(this);
this.rootStore = rootStore;
}
@action
start = () => {
this.loading = true;
this.token = null;
this.error = null;
};
@action
success = (token) => {
this.loading = false;
this.token = token;
this.error = null;
};
@action
fail = (error) => {
this.loading = false;
this.token = null;
this.error = error;
};
}
export default AuthStore;
import { Provider } from 'mobx-react';
import RootStore from './mobx/RootStore';
const rootStore = new RootStore();
function App() {
return (
<ErrorBoundary FallbackComponent={Error}>
<Provider {...rootStore}>
<ConnectedRouter history={history}>
<Switch>
<Route path="/add" component={Add} />
<Route path="/signin" component={Signin} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</ConnectedRouter>
</Provider>
</ErrorBoundary>
);
}
export default App;
import { observable, action, makeObservable } from 'mobx';
class BookStore {
...
getBooks = async () => {
this.loading = true;
this.token = null;
this.error = null;
try {
const books = await BookService.getBooks(this.rootStore.authStore.token);
runInAction(() => {
this.loading = false;
this.books = books;
this.error = null;
});
} catch (error) {
runInAction(() => {
this.loading = false;
this.token = null;
this.error = error;
});
}
};
}
export default BookStore;
export default inject('bookStore')(
observer(function BookListContainer({ bookStore }) {
const { books, loading, error, getBooks } = bookStore;
return (
<BookList
books={books}
loading={loading}
error={error}
getBooks={getBooks}
/>
);
}),
);