Skip to content

Instantly share code, notes, and snippets.

@kazuma1989
Last active March 11, 2020 06:20
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 kazuma1989/0714d3968b9f5a86f59896d153663605 to your computer and use it in GitHub Desktop.
Save kazuma1989/0714d3968b9f5a86f59896d153663605 to your computer and use it in GitHub Desktop.
MobX と hooks でプレーンな書き味の React コンポーネントを書く (https://qiita.com/kazuma1989/items/16f68cf835031b03fb61)
import React, { useEffect } from 'react'
import { Section, Title, Loading, Todo } from './components'
import useTodosStore from './useTodosStore'
export default function App() {
// このファイルにべた書きされた store インスタンスではなく、コンテキスト経由の store を使うことで、
// テスト時や Storybook を使うときにモックしやすくなる。
const [todos, loading, toggle, fetchTodos] = useTodosStore(store => [
store.todos,
store.loading,
store.toggle,
store.fetch,
])
useEffect(() => {
fetchTodos()
}, [fetchTodos])
if (loading) {
return (
<Section>
<Loading />
</Section>
)
}
return (
<Section>
<Title>Todos</Title>
{todos?.map(({ id, title, completed }) => (
<Todo
key={id}
label={title}
completed={completed}
onChange={() => toggle(id)}
/>
))}
</Section>
)
}
import { observable, computed, action, flow } from 'mobx'
type Todo = {
userId: number
id: number
title: string
completed: boolean
}
/**
* TODO 一覧を管理する store
*/
export default class TodosStore {
/**
* TODO 一覧
*/
@observable todos?: Todo[]
/**
* TODO 一覧を読み込み中のとき true
*/
@computed get loading() {
return !this.todos
}
/**
* TODO の一つの完了/未完了を切り替える
*
* @param id 対象の TODO の ID
*/
@action.bound toggle(id: number) {
this.todos = this.todos?.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
}
/**
* TODO 一覧を API から取得する
*/
fetch = flow(function*(this: TodosStore) {
this.todos = undefined
this.todos = yield fetch(
'https://jsonplaceholder.typicode.com/todos?userId=1',
).then(r => r.json())
}).bind(this)
}
import { useContext } from 'react'
import { useObserver } from 'mobx-react'
export type Selector<TStore, TSelection> = (store: TStore) => TSelection
// useContext と useObserver を組み合わせた、任意の store 型に対応したカスタムフック。
// この hook を介して store slice を取得すれば、コンポーネントが store の mutable な変更を検知できる。
export default function useStore<TStore, TSelection>(
context: React.Context<TStore>,
selector: Selector<TStore, TSelection>,
) {
const store = useContext(context)
if (!store) {
throw new Error('need to pass a value to the context')
}
return useObserver(() => selector(store))
}
import { createContext } from 'react'
import useStore, { Selector } from './useStore'
import TodosStore from './TodosStore'
const context = createContext<TodosStore | null>(null)
export const TodosProvider = context.Provider
// TodosStore の slice を取得するための hook
// 汎用の useStore を TodosStore 専用にした。
export default function useTodosStore<TSelection>(selector: Selector<TodosStore, TSelection>) {
return useStore(context, selector)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment