๐ ์์ง ์์ฑ๋์ง ์์์ต๋๋ค. ๋ง๋ค์ด ๋ณด๊ณ ์๋ ์ค์ ๋๋ค!
- ์ํฐํฐ ๊ธฐ๋ฐ ์ํ๊ด๋ฆฌ ๋๊ตฌ
- Top-down๊ณผ Bottom-up์ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์
// State
interface State {
count:number
doubledCount:number
}
// Action
interface Actions {
INCREASE(by:number):void
DECREASE(by:number):void
RESET():void
}
// Store.md
export const useStore = createStore<State, Actions>(({store, reducer}) => {
// Reducer
store.count = reducer(0, (on) => {
on.INCREASE((by) => (state) => (state.count += by))
on.DECREASE((by) => (state) => (state.count -= by))
on.RESET(() => (state) => (state.count = 0))
})
// Computed
store.doubledCount = reducer((state) => state.count * 2)
})
You can use store in React.
// Component
function Counter() {
const {dispatch, count, doubledCount} = useStore()
const ์ฆ๊ฐ = () => dispatch.INCREASE(1)
const ๊ฐ์ = () => dispatch.DECREASE(1)
const ์ด๊ธฐํ = () => dispatch.RESET()
return (
<>
<div>count is {count}</div>
<div>doubledCount is {doubledCount}</div>
<button onClick={์ฆ๊ฐ}>+</button>
<button onClick={๊ฐ์}>-</button>
<button onClick={์ด๊ธฐํ}>RESET</button>
</>
)
}
ComponentStore is a modern state management library for React, designed to offer a more granular and flexible approach to managing state across components. It enables developers to create separate state management contexts for different parts of their application, reducing the complexity and enhancing the reusability of components.
- Separate State Contexts: Enables the creation of separate state contexts (
Providers
) for different components or component groups. - Reduced Props Drilling: By leveraging
Providers
, the need for prop drilling is significantly reduced, leading to cleaner and more maintainable code. - Enhanced Reusability: Components become more reusable and maintainable, as their state management is more self-contained.
- Flexible State Sharing: Allows for flexible state sharing and interactions between different state contexts, making it suitable for complex state management scenarios.
- createComponentStore Manages the state of individual todo items.
interface Todo {
id: string
text: string
completed: boolean
creatorId: string
}
interface TodoExtra {
creator?: User
์์ ๊ถํ์ด_์๋๊ฐ: false
}
interface TodoActions {
TOGGLE(): void
SET_TEXT(text: string): void
}
export const [useTodo, TodoProvider, TodoRepo] = createComponentStore<Todo, TodoActions, TodoExtra>(({store: Todo, reducer, key}) => {
// Todo.id = key
Todo.text = reducer("", (on) => {
on.SET_TEXT((text) => (state) => (state.text = text))
})
Todo.completed = reducer(false, (on) => {
on.TOGGLE(() => (state) => (state.completed = !state.completed))
})
})
- createStore: Manages the state of the entire todo list.
interface TodoApp {
Todo: Record<PropertyKey, Todo>
todos: Todo[]
num_todos: number
num_completed_todos: number
}
interface TodoAppActions {
ADD_TODO(id: string, text: string): void
REMOVE_TODO(id: string): void
}
export const useTodoApp = createStore<TodoApp, TodoAppActions>(({store, reducer}) => {
// Repository
store.Todo = reducer(TodoRepo, (on) => {
on.ADD_TODO((id, text) => (state) => {
state.Todo[id] = {id, text, completed: false, creatorId: "tmp"}
})
on.REMOVE_TODO((id) => (state) => {
delete state.Todo[id]
})
})
// computed value
store.todos = reducer((state) => Object.values(state.Todo).filter(Boolean))
store.num_todos = reducer((state) => state.todos.length)
store.num_completed_todos = reducer((state) => state.todos.filter((todo) => todo.completed).length)
})
- TodoList Component: Uses
TodoListProvider
to manage the list.
export default function TodoList() {
const {todos, num_todos, dispatch} = useTodoApp()
const generateUniqueId = () => Math.random().toString(36).slice(2)
const addTodo = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing) return
if (e.key === "Enter") {
const text = e.currentTarget.value
const newId = generateUniqueId()
dispatch.ADD_TODO(newId, text)
e.currentTarget.value = ""
}
}
return (
<>
<div>num_todos: {num_todos}</div>
<input type="text" onKeyDownCapture={addTodo} />
<ul>
{todos.map((todo) => (
// extra value๋ค๋ ๋๊ธธ์ ์์ผ๋ฉด ์ข๊ฒ ๋ค. index๊ฐ์...
<TodoProvider key={todo.id} id={todo.id}>
<TodoItem />
</TodoProvider>
))}
</ul>
</>
)
}
- TodoItem Component: Manages its own state using
TodoProvider
.
function TodoItem() {
const {id, text, completed, dispatch} = useTodo()
const app = useTodoApp()
const toggleTodo = () => dispatch.TOGGLE()
const removeTodo = () => app.dispatch.REMOVE_TODO(id)
return (
<li className="hbox pointer" style={{textDecoration: completed ? "line-through" : "none"}} onClickCapture={toggleTodo}>
<div>
{id} - {text}
</div>
<button onClickCapture={removeTodo}>์ญ์ </button>
</li>
)
}
/* ์ฌ๊ธฐ์ TodoItem์ ์ปดํฌ๋ํธ๊ฐ ๋ณต์กํด์ง๋ฉด์ ๊ธฐ์กด์๋ props-drill์ด ๋ฐ์ํ์ง๋ง ์ฌ๊ธฐ์์๋ ๊ทธ๋ ์ง ์๋ค๋ ๊ฒ์ ํตํด์ ๋ทฐ ๋ณ๊ฒฝ์ ์์ ๋ก์์ ๋ณด์ฌ์ฃผ๋ ๋ด์ฉ๊ณผ ์์๋ฅผ ์ถ๊ฐํ์ */
- Props๋ฅผ ์์ฑํ๊ณ ์ ๋ฌํ๊ณ ํนํ Props Type ์ง์ ์ด ๋๋ฌด ๊ดด๋กญ๋ค.
- ์ถํ์ ๋์์ธ ๋ณ๊ฒฝ์ ๋ฐ๋ฅธ ์ปดํฌ๋ํธ ๊ตฌ์กฐ ๋ณ๊ฒฝ์ด ์ด๋ ค์์ง.
interface TodoItem {
id: string
text: string
completed: boolean
}
// TodoList ์ปดํฌ๋ํธ
function TodoList() {
const [todos, setTodos] = useState([{id: "1", text: "Learn React", completed: false}])
const toggleTodo = (id: string) => {
// ํฌ๋ ์์ดํ
์ํ ๋ณ๊ฒฝ ๋ก์ง
}
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />
))}
</ul>
)
}
// TodoItem์ Props ํ์
type TodoItemProps = {
todo: TodoItem
onToggle: (id: string) => void
}
// TodoItem ์ปดํฌ๋ํธ
function TodoItem({todo, onToggle}: TodoItemProps) {
return (
<li>
<TodoText text={todo.text} />
<TodoCheckbox completed={todo.completed} onToggle={() => onToggle(todo.id)} />
</li>
)
}
// TodoText ์ปดํฌ๋ํธ
function TodoText({text}: {text: string}) {
return <span>{text}</span>
}
// TodoCheckbox ์ปดํฌ๋ํธ
function TodoCheckbox({completed, onToggle}: {completed: boolean; onToggle: () => void}) {
return <input type="checkbox" checked={completed} onChange={onToggle} />
}
export default TodoList
ComponentStore
๋ฅผ ์ฌ์ฉํ๋ฉด, ๊ฐ TodoItem
์ปดํฌ๋ํธ๋ ์์ฒด์ ์ผ๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ ์ ์์ผ๋ฉฐ, ์์ ์ปดํฌ๋ํธ๋ก๋ถํฐ ๋ง์ props
๋ฅผ ์ ๋ฌ๋ฐ์ ํ์๊ฐ ์์ด์ง๋๋ค.
// TodoItemStore ์ค์
const [useTodo, TodoProvider, TodoRepo] = createComponentStore<...>(...)
const useTodoApp = createStore<...>(...)
// TodoList ์ปดํฌ๋ํธ
function TodoList() {
const {todos, dispatch} = useTodoApp()
const addTodo = (text) => {
const newId = generateUniqueId()
dispatch.ADD_TODO(newId)
}
return (
<>
<input type="text" onKeyPress={(e) => e.key === "Enter" && addTodo(e.target.value)} />
<ul>
{todos.map((id) => (
<TodoProvider key={id} id={id}>
<TodoItem />
</TodoProvider>
))}
</ul>
</>
)
}
// TodoItem ์ปดํฌ๋ํธ
function TodoItem() {
return (
<li>
<TodoText />
<TodoCheckbox />
</li>
)
}
// TodoText ์ปดํฌ๋ํธ
function TodoText() {
const {text} = useTodo()
return <span>{text}</span>
}
// TodoCheckbox ์ปดํฌ๋ํธ
function TodoCheckbox() {
const {completed, dispatch} = useTodo()
const toggleTodo = dispatch.TOGGLE_TODO()
return <input type="checkbox" checked={completed} onChange={toggleTodo} />
}
์ด ์์ ์์ ComponentStore
๋ฅผ ์ฌ์ฉํ๋ฉด TodoItem
๋ด๋ถ์ TodoText
์ TodoCheckbox
์ปดํฌ๋ํธ๊ฐ ์์ ์ปดํฌ๋ํธ๋ก๋ถํฐ ์ง์ props
๋ฅผ ์ ๋ฌ๋ฐ์ง ์๊ณ ๋ ํ์ํ ์ํ์ ์ ๊ทผํ ์ ์์ต๋๋ค. ์ด๋ก ์ธํด Props Drilling
๋ฌธ์ ๊ฐ ํด๊ฒฐ๋๊ณ , ์ปดํฌ๋ํธ ๊ตฌ์กฐ๊ฐ ๋ ๊ฐ๊ฒฐํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์์ง๋๋ค.
- Granular State Management: The use of separate
Providers
for the todo list and individual todo items allows for more detailed and controlled state management. - Independent State Management: Each provider manages its own state independently, reducing inter-component dependencies and enhancing maintainability.
- Flexible and Efficient State Interactions: The ability to have different state contexts interact with each other provides a powerful tool for managing complex state behaviors in large-scale applications.
In conclusion, ComponentStore provides an innovative approach to state management in React applications. It emphasizes modularity, reusability, and flexibility, making it an ideal choice for developers looking to streamline their state management practices in complex applications.
- ์ ๋ต์ด ์๋ ํ๋ ์์ํฌ๊ฐ ๋์.
- ๊ฐ๋ ฅํ ์ ํ์ ๊ฑธ๋ฉด ์ฝ๋๋ ํด๋ฆฐํด์ง ์ ์๋ค.
- ํ๋ ์์ํฌ๋ฅผ ์ฐ๋ ๊ฒ ์์ฒด๊ฐ ์ปจ๋ฒค์ ์ด ๋ ์ ์๋๋ก ํ์.
- ๋๊ฐ ์์ฑ์ ํด๋ ํด๋ฆฐ์ฝ๋๊ฐ ๋ ์ ์๋๋ก ๋์ง๋ฅผ ๋ฐํ
- ๊ทธ๋ ์ง๋ง Draftํ ๊ฐ๋ฐ ๊ณผ์ ์์ ๋น ๋ฅด๊ฒ ๊ฐ๋ฐ์ ํ ์ ์๋๋ก ์ ๊ฐ๋ฐ ํ ๋ฆฌํฉํ ๋ง๋ ๊ฐ๋ฅํ๊ฒ
- ๋น ๋ฅธ ๊ฐ๋ฐ๋ณด๋ค ์ถ์ ๊ณผ ๋๋ฒ๊น ์ ๋ ์ค์ํ๊ฒ ์๊ฐํ๋ค.
- ๊ทธ๋ ๋ค๊ณ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ํ๋ค์ด ๋์ด์๋ ์๋๋ค.
- ํ์คํ CQRS
- ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ์ปจ์ (๋ถ๋ณ์ฑ, ๋จ๋ฐฉํฅ)
- state๋ Action์ ํตํด์๋ง ์์ ์ ํ ์ ์๋ค.
export interface Todo {
id:number
text:string
completed:boolean
}
export type VisibilityFilter = "SHOW_ALL"|"SHOW_COMPLETED"|"SHOW_ACTIVE"
export interface TodoState {
Query: {
todos:Todo[]
filteredTodos:Todo[]
}
Todo: Record<string, Todo>
visibilityFilter:VisibilityFilter
}
export interface TodoActions {
ADD_TODO(text:string):void
TOGGLE_TODO(id:number):void
REMOVE_TODO(id:number):void
SET_VISIBILITY_FILTER(filter:VisibilityFilter):void
}
export const useStore = createStore<TodoState, TodoActions>(({store, reducer}) => {
store.Todo = reducer([], on => {
on.ADD_TODO((text) => (state) => {
const newTodo = {id: Date.now(), text, completed: false}
state.Todo[id] = newTodo
})
on.TOGGLE_TODO((id) => (state) => {
state.Todo[id].completed = !state.Todo[id].completed
})
on.REMOVE_TODO((id) => (state) => {
delete state.Todo[id]
})
})
store.Query.todos = reducer(state => Object.values(state.Todo))
store.Query.filteredTodos = reducer(state => {
const todos = state.Query.todos
const visibilityFilter = state.visibilityFilter
if (visibilityFilter === "SHOW_ACTIVE") {
return todos.filter(todo => !todo.completed)
}
if (visibilityFilter === "SHOW_COMPLETED") {
return todos.filter(todo => todo.completed)
}
return todos
})
store.visibilityFilter = reducer("SHOW_ALL", on => {
on.SET_VISIBILITY_FILTER((filter) => (state) => state.visibilityFilter = filter)
})
})
-
State
-
Store
-
Action
-
Dispatch
-
On
-
Reducer(+Computed)
-
Draft
- CQRS
- Redux(Single Source, Reducer)
- NgRx(Effect)
- StateForge๋ TypeScript์ ๊ฐ๋ ฅํ ํ์ ์์คํ ์ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ถ๋์์ต๋๋ค. ์ด๋ ๊ฐ๋ฐ ์ค์ ๋์ ์์ค์ ์๋์์ฑ ์ง์์ ์ ๊ณตํ๋ฉฐ, ํ์ ๊ด๋ จ ์ค๋ฅ๋ฅผ ์ฌ์ ์ ๋ฐฉ์งํ ์ ์๊ฒ ํด์ค๋๋ค. ๊ฐ๋ฐ์๊ฐ ์ฝ๋๋ฅผ ์์ฑํ ๋ ํ์ํ ์์ฑ์ด๋ ์ก์ ์ ์ฝ๊ฒ ์ฐพ์ ์ ์๋๋ก ๋์์ค๋๋ค.
- Redux์ ๊ฐ์ ๊ธฐ์กด์ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋ง์ ์ค์ ๊ณผ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๊ฐ ํ์ํฉ๋๋ค. StateForge๋ ์ด๋ฐ ๋ถ๋ถ์ ๋ํญ ๊ฐ์ํํ์ฌ ๊ฐ๋ฐ์๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ ๋ ์ง์คํ ์ ์๋๋ก ์ค๊ณ๋์์ต๋๋ค. ํ์ํ ๊ธฐ๋ฅ์ ๋ช ์ค์ ์ฝ๋๋ก ๊ฐ๊ฒฐํ๊ฒ ํํํ ์ ์์ต๋๋ค.
- StateForge๋ Redux Toolkit์ ์๊ฐ์ ๋ฐ์ ๋ ๋์ ๊ฐ๋ฐ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค. ํ์ง๋ง StateForge๋ ํ์ ์์ ์ฑ๊ณผ ์๋์์ฑ์ ๊ฐ์ ํ์ฌ Redux Toolkit์ด ์ ๊ณตํ๋ ๊ฒ ์ด์์ ๊ฐ๋ฐ์ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
- StateForge์ API๋ ์ง๊ด์ ์ด๋ฉฐ ์ฌ์ฉํ๊ธฐ ์ฝ์ต๋๋ค. ์ฌ๋ผ์ด์ค ์์ฑ, ์ก์ ์ ์, ์คํ ์ด ๊ตฌ์ฑ ๋ฑ์ ๊ณผ์ ์ด ๋จ์ํ๋์ด ์์ด, ์๋ก์ด ๊ฐ๋ฐ์๋ ์ฝ๊ฒ ์ํ ๊ด๋ฆฌ ์์คํ ์ ์ดํดํ๊ณ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- Interface๋ฅผ ๋จผ์ ์ค๊ณํ๊ณ ๊ฐ๋ฐํ๋ ๋ฐฉ์
- State์ Action์ ๋ถ๋ฆฌํด์ ๊ฐ๋ฐํ๊ธฐ ์ฝ๊ฒ! BDD, SDD
- ์ธ๋ฐ์์ ActionType, ActionCreator ์ด๋ฐ๊ฑฐ NoNo!
- Proxy ๊ธฐ๋ฐ์ผ๋ก ์ธ๋ฐ์์ด ๋ถ๋ณ์ฑ์ ์งํค๊ธฐ ์ํ ์ฝ๋ฉ ํ์ง ์๋๋ค.
"Store"๋ผ๋ ์ฉ์ด๋ ์์ (store)์์ ์ ๋ํ์ต๋๋ค. ์์ ์ฒ๋ผ ๋ค์ํ ๋ฌผ๊ฑด์ ํ ๊ณณ์ ๋ชจ์๋๊ณ ํ์ํ ๋ ๊บผ๋ด ์ฐ๋ ๊ฒ๊ณผ ๋น์ทํ๊ฒ, ์ํ ๊ด๋ฆฌ์์์ store๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ํ ๋ฐ์ดํฐ(State)๋ฅผ ํ๋์ ์ฅ์์ ์ ์ฅํ๊ณ ํ์ํ ๋ ์ปดํฌ๋ํธ๊ฐ ์ ๊ทผํ์ฌ ์ฌ์ฉํ ์ ์๋๋ก ํฉ๋๋ค.
์ด๋ฌํ ์ค์ ์ง์ค์ ๊ด๋ฆฌ ๋ฐฉ์์ ๋ฐ์ดํฐ์ ์ผ๊ด์ฑ์ ์ ์งํ๊ณ , ์ํ ๋ณํ์ ๋ํ ์ถ์ ๊ณผ ๋๋ฒ๊น ์ ์ฉ์ดํ๊ฒ ํฉ๋๋ค. ๋ํ, ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ๋ฅผ ํ ๊ณณ์์ ๊ด๋ฆฌํจ์ผ๋ก์จ ๋ฐ์ดํฐ ํ๋ฆ์ ๋ณด๋ค ๋ช ํํ๊ฒ ๋ง๋ค๊ณ , ๋ณต์กํ ์ํ ๊ด๋ฆฌ๋ฅผ ๋จ์ํํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
- ์ํ ๋ณด๊ด: ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฒด ์ํ๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ์ ์ฅํฉ๋๋ค.
- ์ํ ์ ๊ทผ: ์ปดํฌ๋ํธ์์ store์ ์ํ์ ์ ๊ทผํ ์ ์๊ฒ ํฉ๋๋ค.
- ์ํ ๊ฐฑ์ : ์ก์ ์ ํตํด ์ํ๋ฅผ ๋ณ๊ฒฝํ๊ณ , ์ด์ ๋์ํ๋ ๋ฆฌ๋์๋ก ์๋ก์ด ์ํ๋ฅผ ์์ฑํฉ๋๋ค.
- ๊ตฌ๋ ๊ด๋ฆฌ: ์ํ ๋ณํ๋ฅผ ๊ตฌ๋ ํ๊ณ ์๋ ์ปดํฌ๋ํธ์ ๋ณํ๋ฅผ ์๋ฆฝ๋๋ค.
- ์๊ตฌ์ฌํญ์ ๊ตฌํํ๋ ๊ฒ๋ ๋์์ ๋ฐ์ดํฐ๋ก ๋ณ๊ฒฝํ๊ธฐ
- ex) turnOnLight() vs isLight = true
- ๊ทธ๋ฐ๋ฐ ํ๋ก๊ทธ๋จ์ด ๋ณต์กํด์ง๋ฉด ๊ฐ์ผ๋ก๋ง ๊ธฐ์ ํ๋ค ์ด๊ฒ ์ด๋ค ๋์์ธ์ง ์ดํด๊ฐ ์ด๋ ค์์ง๋ค.
- ์ง์ ๊ฐ์ ์์ ํ๋ค ๋ณด๋ฉด ์ค์๋ฅผ ํ ์ ์๊ฒ ๋๋ค.
- "ํด๋ฒ: ์ํ(State)์ ๋ณํ๋ฅผ ์ก์ ๊ณผ ๋ฆฌ๋์๋ก ๋ถ๋ฆฌํ๊ธฐ"
- ๋ฐ์ดํฐ์ ๋ณํ์ ๊ธฐ์ ์ ๋ฐ์ดํฐ scope๋ด์์ ์์ฑํด์ ๋ฐ์ดํฐ์ ๋ณํ๋ฅผ ์ถ์ ํ ์ ์๋ค.
- ํ๋ก๊ทธ๋จ์ ๊ฐ์ ๋ณํ๊ฐ ์๋๋ผ ์๊ตฌ์ฌํญ๊ณผ ๋น์ทํ ํํ๋ก ์์ฑํ ์ ์๊ฒ ๋๋ค.
- ์กฐ๊ธ ๋ ๊ท์ฐฎ์์ผ ํ๋ค.
- ๋ฌธ๋ฒ์ด ๋ณต์กํด์ง๋ค(?)
- => ๊ฐ๋จํ๊ฒ on์ด๋ผ๋ helper๊ฐ์ฒด๋ฅผ ์ ๊ณตํด์ ๋ณด์ผ๋ฌ ํ๋ ์ดํธ๋ฅผ ์ค์๋ค!
- => immer์ ๊ฐ์ด ๋ถ๋ณ์ฑ์ ์ ์งํ๋ฉด์ ๋จ์ํ ๋ฌธ๋ฒ์ผ๋ก ์์ฑํ ์ ์๊ฒ ํด๊ฒฐํ๋ค.
store.Todo = reducer([], on => {
on.ADD_TODO((text) => (state) => {
const newTodo = {id: Date.now(), text, completed: false}
state.Todo[id] = newTodo
})
on.TOGGLE_TODO((id) => (state) => {
state.Todo[id].completed = !state.Todo[id].completed
})
on.REMOVE_TODO((id) => (state) => {
delete state.Todo[id]
})
})
- ์๋ฐ์คํฌ๋ฆฝํธ๋ ๋ถ๋ณ์ฑ์ ์ธ์ด์ฐจ์์์ ์ง์ํ์ง ์์ผ๋ฏ๋ก ์๋นํ ๋ถํธํ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค.
- ReactForge์์๋ ์๋์ผ๋ก ๋ถ๋ณ์ฑ์ ์ ์งํ๋ ์ฝ๋๋ฅผ ์์ฑํด์ค๋๋ค. (like Immer)
// ๊ฒฐ๊ตญ ์์ํจ์ ํํ๋ก ์ ๊ณต๋๋ค.
function createReducer(state, action, reducerFn) {
const draft = clone(state) // ๊ตฌ์กฐ์ ๊ณต์ ๋ฅผ ํตํ ํจ๊ณผ์ ์ธ ๋ณต์ฌ
const on = helper(action)
reducerFn(on)(draft)
return draft
}
๐ค (์์) classicReducer๋ ๋ง์ด๊ทธ๋ ์ด์ ์ฉ์ผ๋ก ์ ๊ณตํด ๋ณผ๊น??
function todoReducer(state = {Todo:{}}, action) {
switch (action.type) {
case "ADD_TODO": {
const {text} = action.payload
const todo = {id: Date.now(), text, completed: false}
return {...state, Todo: {...state.Todo, [todo.id]: todo}}
}
case "TOGGLE_TODO": {
const {id} = action.payload
const completed = !state.Todo[id].completed
return {...state, Todo: {...state.Todo, [id]: {...state.Todo[id], completed}}}
}
case "REMOVE_TODO": {
const {id} = action.payload
const {[id]: value, ...newTodo} = state.Todo;
return {...state, Todo: newTodo};
delete newState.Todo[id];
}
}
return state
}
store.todo = classicReducer(TodoReducer)
// on.ACTION(params => (state) => state.value++))
on.INCREASE(by => (state) => state.count)
// on ํจ์๋ฅผ ์ด์ฉํ๋ฉด, ํ๋์ ํจ์๋ก 2๊ฐ์ง ์ก์
์ ๋์ํ ์ ์๋ค.
on(on.INCREASE, on.DECREASE)((inc, dec) => (state) => {
acount = inc ? inc : -dec
state.count += amount
})
// on ํจ์์ store๋ฅผ ๊ฒฐํฉํ์ฌ ํด๋น ๊ฐ์ด ๋ฐ๋๋ ์ก์
์ผ๋ก ๋ฐ์ ์ ์๋ค.
// like Rxjs's combineLastest
// each value changes, but every value is not undefined
on(store.account.id, store.User)((accountId, User) => (state) => {
const user = User[accountId]
if(!user) return
state.account.name = user.name
})
// SUCCESS or FAILURE or onChange(store.User)
on(on.SUCCESS, on.FAILURE, store.User)((succ, fail, user) => (state) => {
if (succ) state.ok = true
else if (fail) state.ok = false
else state.ok = !!user
})
interface Actions {
ADD_TODO(text:string, id?:number):void
}
// action middleware
store.dispatch.ADD_TODO = (text:string, id:number = Date.now()) => {
return [text, id]
}
store.Todo = reducer([], (on) => {
on.ADD_TODO((text, id) => (state) => {
state.Todo[id] = {id, text, complted: false}
})
/* Bad
on.ADD_TODO((text) => (state) => {
// Date.now() is not pure!
const newTodo = {id: Date.now(), text, completed: false}
state.Todo[id] = newTodo
})
*/
})
function Component() {
const someHandler = (msg:string) => dispatch.ADD_TODO(msg)
return <></>
}
Promise๋ฅผ returnํ๋ฉด SUCCESS, FAILTURE, SETTLED ์ก์ ์ด ์๋์ผ๋ก ์์ฑ๋์ด ํธ์ถ๋๋ค.
// async action (promise)
store.dispatch.ADD_TODO_ASYNC = (text:string, id:string) => {
return new Promise(resolve => setTimeout(() => resolve([text, id]), 1000)
}
store.Todo = reducer({}, on => {
on.ADD_TODO_ASYNC.REQUEST(res => (state) => {
/* ... */
})
on.ADD_TODO_ASYNC.SUCCESS(res => (state) => {
/* ... */
})
on.ADD_TODO_ASYNC.FAILTURE(res => (state) => {
/* ... */
})
on.ADD_TODOADD_TODO.COMPLETED(res => (state) => {
/* ... */
})
})
์ค์ ์์ : API ์ฐ๋,
interface Todo {
id:string
text:string
completed:boolean
}
interface Actions {
ADD_TODO: {
(text:string):(dispatch)=>Promise
REQUEST(todo:Todo)
SUCCESS(todo:Todo)
FAILTURE(error:Error)
}
REMOVE_TODO: {
}
}
store.dispatch.ADD_TODO = (text:string) => (dispatch) => {
const id = Math.random().toString(36).slice(2)
const newTodo = {id, text, completed: false}
return dispatch.REQUEST(newTodo, api.POST("/todos")(newTodo).then(res => res.data))
}
store.dispatch.REMOVE_TODO = (id:string) => (dispatch) => {
return dispatch.REQUEST(id, api.DELETE("/todos/:id")(id))
}
store.dispatch.REMOVE_TODO = mutation((id:string) => api.DELETE("/todos/:id")(id))
store.dispatch.ADD_TODO = mutation(
(text) => {
const id = Math.random().toString(36).slice(2)
return {id, text, completed: false}
},
(newTodo) => api.POST("/todos")(newTodo)
)
store.dispatch.ADD_TODO = mutation()
.onMutate(text => {
const id = Math.random().toString(36).slice(2)
return {id, text, completed: false}
})
.mutateFn(newTodo => api.POST("/todos")(newTodo))
.onSuccess(() => invalidate("/todos/", id)
)
store.Todo = reducer([], (on, effect) => {
on.ADD_TODO.REQUEST(newTodo => (state) => {
state.Todo[id] = newTodo
})
on.ADD_TODO.SUCCESS((todo, context) => {
delete state.Todo[context.id]
state.Todo[todo.id] = todo
})
// request๋ ์ปจํ
์ธ ๋ ์๋์ผ๋ก ๋ณต์๋๋ค.
on.ADD_TODO.FAILTURE((todo, context) => {
/* TODO */
})
on.TOGGLE_TODO((id) => (state) => {
state.Todo[id].completed = !state.Todo[id].completed
})
on.REMOVE_TODO.REQUEST((id) => (state) => {
delete state.Todo[id]
})
})
store.dispatch.REMOVE_TODO = (id:string) => (dispatch, transaction) => {
return dispatch.REQUEST(id, transaction(state => {
delete state.Todo[id]
return api.DELETE("/todos/:id")(id)
}))
}
๊ทธ๋ฐ์ Action ํ์ฅ ์๊ฐ๊ฑฐ๋ฆฌ๋ค..
-
.CANCELED: ์งํ ์ค์ธ ๋น๋๊ธฐ ์์ ์ ์ทจ์ํ๋ ์ก์ ์ ๋๋ค. ์ฌ์ฉ์๊ฐ ์์ฒญ์ ์ค๋จํ๊ฑฐ๋ ๋ค๋ฅธ ์์ ์ผ๋ก ์ ํํ ๋ ์ ์ฉํฉ๋๋ค.
-
.RETRY: ์คํจํ ๋น๋๊ธฐ ์์ ์ ๋ค์ ์๋ํ๋ ์ก์ ์ ๋๋ค. FAILURE ํ์ ๋คํธ์ํฌ ์ํ๊ฐ ๊ฐ์ ๋๊ฑฐ๋ ์ค๋ฅ๊ฐ ์์ ๋ ๊ฒฝ์ฐ ์ ์ฉํฉ๋๋ค.
-
.THROTTLE / .DEBOUNCE: ์์ฒญ์ ๋น๋๋ฅผ ์กฐ์ ํ๋ ์ก์ ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์ ๋ ฅ์ ๋ฐ๋ฅธ ์๋ ์์ฑ ๊ธฐ๋ฅ์์ ์๋ฒ ๋ถํ๋ฅผ ์ค์ด๊ธฐ ์ํด ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
-
.UPDATE: ์งํ ์ค์ธ ๋น๋๊ธฐ ์์ ์ ๋ํ ์ค๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ์ ๊ณตํ๋ ์ก์ ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ํ์ผ ์ ๋ก๋์ ์งํ ์ํฉ์ ํ์ํ ๋ ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
-
.POLLING_START / ,POLLING_STOP: ์ ๊ธฐ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ํด๋ง(polling) ์์ ์ ์์ํ๊ฑฐ๋ ์ค๋จํ๋ ์ก์ ์ ๋๋ค. ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ ๊ฒฝ์ฐ ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
- ์ปดํฌ๋ํธ์์๋ 1)๊ฐ๊ณผ 2)dispatch๋ง ์ฌ์ฉํ๋ค.
- ๋ณํ๋ ๊ฐ์ value๋ก ๋ง๋ค๊ณ State์ ๋ฑ๋กํ๋ค.
- ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๊ทธ๋๋ก dispatchํ๊ณ Action์ ๋ฑ๋กํ๋ค.
- ํด๋น Action์ ํ๊ณ ๋๋ฉด ์ด๋ค ๊ฐ์ด ๋ฐ๋์ด์ผ ํ๋์ง ์๊ฐํด๋ณธ๋ค.
- ๋ฐ๋๋ ๊ฐ์ reducer์ ๊ฐ์ on.ACTION ์ดํ ๊ฐ์ ๋ณํ ์ํจ๋ค.
- ์๊ตฌ์ฌํญ์ ์๊ฐํด๋ณธ๋ค.
- ์ด๋ค ๊ฐ์ด ๋ฐ๋์ด์ผ ํ๋๊ฐ?
- ๊ทธ ๊ฐ์ด ๋ฐ๋๊ธฐ ์ํด์ ์ด๋ค ๋ฐ์ดํฐ๊ฐ ํ์ํ๊ฐ?
- ์ธ์ ๊ทธ๊ฐ์ด ๋ฐ๋์ด์ผ ํ๋๊ฐ?
- ํญ์ ํน์ ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ๋ก ํ์ํ๋ค๋ฉด on(store.data.path)๋ฅผ ์ด์ฉํ๋ค.
- ํน์ ์์ ์ด ํ์ํ๋ค๋ฉด disaptch.ACTION์ ํตํด์ ํด๊ฒฐํ๋ค.
๋น๋๊ธฐ ์ก์ ์ฒ๋ฆฌ- ์ดํํธ ์ฒ๋ฆฌ
- ์ํ ์ถ์ ๋ฐ ๋๋ฒ๊น
- ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
- ์ํ๊ด๋ฆฌ ๋ฉํ ๋ชจ๋ธ
- ์กฐ๊ฑด๋ถ ์คํ ๋ฆฌ
- ์ํฐํฐ์ ๋ฐ์ดํฐ ์ ๊ทํ(Normalize)
- createComponentStore()
- ๋ฑ๋ฑ...
๋ชฉํ
- fetchXXX, getXXX, ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ์์ ๊ธฐ
- d.ts ํ์ผ์๋ค๊ฐ interface๋ก ๋ฑ๋กํ๋ฉด ๋ฒ๋ค์ ํฌํจ์ด ๋์ง ์๋๋ค.
- proxy์ typescript๋ฅผ ํตํด ์๋์์ฑ ๋ฐ์ ์ ์๋ค.
- ์ด ํ์์ ์ค์จ๊ฑฐ๋ฅผ ํตํด์ ์๋์์ฑ ํ ์ ์๋ค
type Response<State> = createResponse<{
status:number,
data:State
}>
interface API_Post {
GET:{
["/posts/recommend"]():Response<{lastKey:string, list:Post[]}>
["/posts/:postId"](postId:string):Response<Post>
["/posts/:postId/comments"](postId:string, params?:unknown):Response<Comment[]>
}
}
interface API_Calendar {
GET:{
["/calendars"]():Response<Calendar[]>
["/calendars/:calendarId"](calendarId:string):Response<Calendar>
}
POST:{
["/calendars/:calendarId"](calendarId:string, body:Calendar, q:{lastKey:string}):Response<Calendar>
}
PUT:{
["/calendars/:calendarId"]():Response<Calendar>
}
}
type API
= API_Post
& API_Calendar
export const api = createAPI<API>({
baseURL: "https://example.com/api",
fetchOptions: {
/* @TODO: ์ฌ๊ธฐ ํค๋์ ๋ณด์ ์์
์ถ๊ฐ ๋์ด์ผ ํจ.*/
}
})
// GET /posts/recommend
const res = await api.GET["/posts/recommend"]()
console.log(res.data.data.list)
// GET /posts/7yKG9ccNK82?lastKey=100
const res2 = await api.GET["/posts/:postId"]("7yKG9ccNK82", {lastKey:100})
console.log(res2)
// POST /calendars/7yKG9ccNK82?lastKey=100 body:{x:100}
const res3 = await api.POST["/calendars/:calendarId"]("7yKG9ccNK82", {x:100}, {lastKey:100})