├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README-ru-ru.md ├── README-th-th.md ├── README-vi-vn.md ├── README-zh-cn.md ├── README.md ├── example ├── index.html └── index.tsx ├── package-lock.json ├── package.json ├── src └── unstated-next.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | example/dist 4 | dist 5 | .rts2_cache_* 6 | .cache 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .rts2_cache_* 2 | node_modules 3 | .*ignore 4 | dist 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "overrides": [ 6 | { 7 | "files": "*.md", 8 | "options": { 9 | "useTabs": false 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README-ru-ru.md: -------------------------------------------------------------------------------- 1 | 

2 | 3 | English | 4 | 中文 | 5 | Русский | 6 | ภาษาไทย | 7 | Tiếng Việt 8 | 9 |
10 | (Please contribute translations!) 11 |

12 | 13 | # Unstated Next 14 | 15 | > 200 байт, чтобы навсегда забыть о библиотеках для управления состоянием React-компонентов 16 | 17 | - **React-хуки**: _это все, что нужно для управления состоянием._ 18 | - **~200 байт**, _min+gz._ 19 | - **Знакомый API**: _просто пользуйтесь React, как обычно._ 20 | - **Минимальный API**: _хватит пяти минут, чтобы разобраться._ 21 | - **Написан на TypeScript**, _чтобы обеспечить автоматический вывод типов в коде ваших компонентов React._ 22 | 23 | Главный вопрос: чем он лучше, чем Redux? Ну... 24 | 25 | - **Он меньше.** _Он в 40 раз меньше._ 26 | - **Он быстрее.** _Изолируйте проблемы производительности на уровне компонентов._ 27 | - **Он проще в изучении.** _Вам в любом случае нужно уметь пользоваться React-хуками и контекстом, они классные._ 28 | - **Он проще в интеграции.** _Подключайте по одному компоненту за раз, не ломая совместимости с другими React-библиотеками._ 29 | - **Он проще в тестировании.** _Тестировать отдельно редьюсеры — напрасная трата времени, тестируйте сами React-компоненты._ 30 | - **Он проще с точки зрения типизации.** _Написан так, чтобы максимально задействовать выведение типов._ 31 | - **Он минималистичный.** _Это просто React._ 32 | 33 | Вам решать. 34 | 35 | ### [См. также: миграция с Unstated →](#миграция-с-unstated) 36 | 37 | ## Установка 38 | 39 | ```sh 40 | npm install --save unstated-next 41 | ``` 42 | 43 | ## Пример 44 | 45 | ```js 46 | import React, { useState } from "react" 47 | import { createContainer } from "unstated-next" 48 | import { render } from "react-dom" 49 | 50 | function useCounter(initialState = 0) { 51 | let [count, setCount] = useState(initialState) 52 | let decrement = () => setCount(count - 1) 53 | let increment = () => setCount(count + 1) 54 | return { count, decrement, increment } 55 | } 56 | 57 | let Counter = createContainer(useCounter) 58 | 59 | function CounterDisplay() { 60 | let counter = Counter.useContainer() 61 | return ( 62 |
63 | 64 | {counter.count} 65 | 66 |
67 | ) 68 | } 69 | 70 | function App() { 71 | return ( 72 | 73 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | render(, document.getElementById("root")) 86 | ``` 87 | 88 | ## API 89 | 90 | ### `createContainer(useHook)` 91 | 92 | ```js 93 | import { createContainer } from "unstated-next" 94 | 95 | function useCustomHook() { 96 | let [value, setInput] = useState() 97 | let onChange = e => setValue(e.currentTarget.value) 98 | return { value, onChange } 99 | } 100 | 101 | let Container = createContainer(useCustomHook) 102 | // Container === { Provider, useContainer } 103 | ``` 104 | 105 | ### `` 106 | 107 | ```js 108 | function ParentComponent() { 109 | return ( 110 | 111 | 112 | 113 | ) 114 | } 115 | ``` 116 | 117 | ### `` 118 | 119 | ```js 120 | function useCustomHook(initialState = "") { 121 | let [value, setValue] = useState(initialState) 122 | // ... 123 | } 124 | 125 | function ParentComponent() { 126 | return ( 127 | 128 | 129 | 130 | ) 131 | } 132 | ``` 133 | 134 | ### `Container.useContainer()` 135 | 136 | ```js 137 | function ChildComponent() { 138 | let input = Container.useContainer() 139 | return 140 | } 141 | ``` 142 | 143 | ### `useContainer(Container)` 144 | 145 | ```js 146 | import { useContainer } from "unstated-next" 147 | 148 | function ChildComponent() { 149 | let input = useContainer(Container) 150 | return 151 | } 152 | ``` 153 | 154 | ## Руководство 155 | 156 | Если вы пока не знакомы с React-хуками, рекомендую прервать чтение и ознакомиться с 157 | [прекрасной документацией на сайте React](https://reactjs.org/docs/hooks-intro.html). 158 | 159 | Итак, с помощью хуков вы можете написать что-нибудь вроде такого компонента: 160 | 161 | ```js 162 | function CounterDisplay() { 163 | let [count, setCount] = useState(0) 164 | let decrement = () => setCount(count - 1) 165 | let increment = () => setCount(count + 1) 166 | return ( 167 |
168 | 169 |

You clicked {count} times

170 | 171 |
172 | ) 173 | } 174 | ``` 175 | 176 | Если логику компонента требуется использовать в нескольких местах, ее можно вынести 177 | в отдельный кастомный хук: 178 | 179 | ```js 180 | function useCounter() { 181 | let [count, setCount] = useState(0) 182 | let decrement = () => setCount(count - 1) 183 | let increment = () => setCount(count + 1) 184 | return { count, decrement, increment } 185 | } 186 | 187 | function CounterDisplay() { 188 | let counter = useCounter() 189 | return ( 190 |
191 | 192 |

You clicked {counter.count} times

193 | 194 |
195 | ) 196 | } 197 | ``` 198 | 199 | Но что делать, когда вам требуется общее состояние, а не только логика? 200 | Здесь пригодится контекст: 201 | 202 | ```js 203 | function useCounter() { 204 | let [count, setCount] = useState(0) 205 | let decrement = () => setCount(count - 1) 206 | let increment = () => setCount(count + 1) 207 | return { count, decrement, increment } 208 | } 209 | 210 | let Counter = createContext(null) 211 | 212 | function CounterDisplay() { 213 | let counter = useContext(Counter) 214 | return ( 215 |
216 | 217 |

You clicked {counter.count} times

218 | 219 |
220 | ) 221 | } 222 | 223 | function App() { 224 | let counter = useCounter() 225 | return ( 226 | 227 | 228 | 229 | 230 | ) 231 | } 232 | ``` 233 | 234 | Это замечательно и прекрасно; чем больше людей будет писать в таком стиле, тем лучше. 235 | 236 | Однако стоит внести еще чуть больше структуры и ясности, чтобы API предельно четко выражал ваши намерения. 237 | 238 | Для этого мы добавили функцию `createContainer()`, чтобы можно было рассматривать ваши кастомные хуки как "контейнеры", чтобы наш четкий и ясный API просто невозможно было использовать неправильно. 239 | 240 | ```js 241 | import { createContainer } from "unstated-next" 242 | 243 | function useCounter() { 244 | let [count, setCount] = useState(0) 245 | let decrement = () => setCount(count - 1) 246 | let increment = () => setCount(count + 1) 247 | return { count, decrement, increment } 248 | } 249 | 250 | let Counter = createContainer(useCounter) 251 | 252 | function CounterDisplay() { 253 | let counter = Counter.useContainer() 254 | return ( 255 |
256 | 257 |

You clicked {counter.count} times

258 | 259 |
260 | ) 261 | } 262 | 263 | function App() { 264 | return ( 265 | 266 | 267 | 268 | 269 | ) 270 | } 271 | ``` 272 | 273 | Сравните текст компонента до и после наших изменений: 274 | 275 | ```diff 276 | - import { createContext, useContext } from "react" 277 | + import { createContainer } from "unstated-next" 278 | 279 | function useCounter() { 280 | ... 281 | } 282 | 283 | - let Counter = createContext(null) 284 | + let Counter = createContainer(useCounter) 285 | 286 | function CounterDisplay() { 287 | - let counter = useContext(Counter) 288 | + let counter = Counter.useContainer() 289 | return ( 290 |
291 | ... 292 |
293 | ) 294 | } 295 | 296 | function App() { 297 | - let counter = useCounter() 298 | return ( 299 | - 300 | + 301 | 302 | 303 | 304 | ) 305 | } 306 | ``` 307 | 308 | Если вы пишете на TypeScript (а если нет — настоятельно рекомендую ознакомиться с ним), вы ко всему прочему получаете более качественный вывод типов. Если ваш кастомный хук строго типизирован, вывод всех остальных типов сработает автоматически. 309 | 310 | ## Советы 311 | 312 | ### Совет #1: Объединение контейнеров 313 | 314 | Поскольку мы имеем дело с кастомными хуками, мы можем объединять контейнеры внутри других хуков. 315 | 316 | ```js 317 | function useCounter() { 318 | let [count, setCount] = useState(0) 319 | let decrement = () => setCount(count - 1) 320 | let increment = () => setCount(count + 1) 321 | return { count, decrement, increment, setCount } 322 | } 323 | 324 | let Counter = createContainer(useCounter) 325 | 326 | function useResettableCounter() { 327 | let counter = Counter.useContainer() 328 | let reset = () => counter.setCount(0) 329 | return { ...counter, reset } 330 | } 331 | ``` 332 | 333 | ### Совет #2: Используйте маленькие контейнеры 334 | 335 | Контейнеры лучше всего делать маленькими и четко сфокусированными на конкретной задаче. Если вам нужна дополнительная бизнес-логика в контейнерах — выносите новые операции в отдельные хуки, а состояние пусть хранится в контейнерах. 336 | 337 | ```js 338 | function useCount() { 339 | return useState(0) 340 | } 341 | 342 | let Count = createContainer(useCount) 343 | 344 | function useCounter() { 345 | let [count, setCount] = Count.useContainer() 346 | let decrement = () => setCount(count - 1) 347 | let increment = () => setCount(count + 1) 348 | let reset = () => setCount(0) 349 | return { count, decrement, increment, reset } 350 | } 351 | ``` 352 | 353 | ### Совет #3: Оптимизация компонентов 354 | 355 | Не существует никакой отдельной "оптимизации" для `unstated-next`, достаточно обычных приемов оптимизации React-компонентов. 356 | 357 | #### 1) Оптимизация тяжелых поддеревьев с помощью разбиения компонентов на части. 358 | 359 | **До:** 360 | 361 | ```js 362 | function CounterDisplay() { 363 | let counter = Counter.useContainer() 364 | return ( 365 |
366 | 367 |

You clicked {counter.count} times

368 | 369 |
370 |
371 |
372 |
СУПЕР НАВОРОЧЕННОЕ ПОДДЕРЕВО КОМПОНЕНТОВ
373 |
374 |
375 |
376 |
377 | ) 378 | } 379 | ``` 380 | 381 | **После:** 382 | 383 | ```js 384 | function ExpensiveComponent() { 385 | return ( 386 |
387 |
388 |
389 |
СУПЕР НАВОРОЧЕННОЕ ПОДДЕРЕВО КОМПОНЕНТОВ
390 |
391 |
392 |
393 | ) 394 | } 395 | 396 | function CounterDisplay() { 397 | let counter = Counter.useContainer() 398 | return ( 399 |
400 | 401 |

You clicked {counter.count} times

402 | 403 | 404 |
405 | ) 406 | } 407 | ``` 408 | 409 | #### 2) Оптимизация тяжелых операций с помощью хука useMemo() 410 | 411 | **До:** 412 | 413 | ```js 414 | function CounterDisplay(props) { 415 | let counter = Counter.useContainer() 416 | 417 | // Вычислять выражение каждый раз, когда обновляется `counter` — слишком медленно 418 | let expensiveValue = expensiveComputation(props.input) 419 | 420 | return ( 421 |
422 | 423 |

You clicked {counter.count} times

424 | 425 |
426 | ) 427 | } 428 | ``` 429 | 430 | **После:** 431 | 432 | ```js 433 | function CounterDisplay(props) { 434 | let counter = Counter.useContainer() 435 | 436 | // Пересчитываем значение только тогда, когда входные данные изменились 437 | let expensiveValue = useMemo(() => { 438 | return expensiveComputation(props.input) 439 | }, [props.input]) 440 | 441 | return ( 442 |
443 | 444 |

You clicked {counter.count} times

445 | 446 |
447 | ) 448 | } 449 | ``` 450 | 451 | #### 3) Снижаем количество повторных рендеров с помощью React.memo() and useCallback() 452 | 453 | **До:** 454 | 455 | ```js 456 | function useCounter() { 457 | let [count, setCount] = useState(0) 458 | let decrement = () => setCount(count - 1) 459 | let increment = () => setCount(count + 1) 460 | return { count, decrement, increment } 461 | } 462 | 463 | let Counter = createContainer(useCounter) 464 | 465 | function CounterDisplay(props) { 466 | let counter = Counter.useContainer() 467 | return ( 468 |
469 | 470 |

You clicked {counter.count} times

471 | 472 |
473 | ) 474 | } 475 | ``` 476 | 477 | **После:** 478 | 479 | ```js 480 | function useCounter() { 481 | let [count, setCount] = useState(0) 482 | let decrement = useCallback(() => setCount(count - 1), [count]) 483 | let increment = useCallback(() => setCount(count + 1), [count]) 484 | return { count, decrement, increment } 485 | } 486 | 487 | let Counter = createContainer(useCounter) 488 | 489 | let CounterDisplayInner = React.memo(props => { 490 | return ( 491 |
492 | 493 |

You clicked {props.count} times

494 | 495 |
496 | ) 497 | }) 498 | 499 | function CounterDisplay(props) { 500 | let counter = Counter.useContainer() 501 | return 502 | } 503 | ``` 504 | 505 | ## Отношение к Unstated 506 | 507 | Я рассматриваю данную библиотеку как духовного преемника [Unstated](https://github.com/jamiebuilds/unstated). Я сделал Unstated, поскольку был убежден, что React и сам превосходно справлялся с управлением состоянием, и ему не хватало только простого механизма для разделения общего состояния и логики. Поэтому я создал Unstated как "минимальное" решение для данной проблемы. 508 | 509 | С появлением хуков React стал гораздо лучше в плане выделения общего состояния и логики. Настолько лучше, что, с моей точки зрения, Unstated стал излишней абстракцией. 510 | 511 | **ТЕМ НЕ МЕНЕЕ**, я считаю, что многие разработчики слабо представляют, как разделять логику и общее состояние приложения с помощью React-хуков. Это может быть связано просто с недостаточным качеством документации и инерцией сообщества, но я полагаю, что четкий API как раз способен исправить этот недостаток. 512 | 513 | Unstated Next и есть этот самый API. Вместо того, чтобы быть "Минимальным API для разделения общего состояния и логики в React", теперь он "Минимальный API для понимания, как разделять общее состояние и логику в React". 514 | 515 | Я всегда был на стороне React, и я хочу, чтобы React процветал. Я бы предпочел, чтобы сообщество отказалось от использования библиотек для управления состоянием наподобие Redux, и начало наконец в полную силу использовать встроенные в React инструменты. 516 | 517 | Если вместо того, чтобы использовать Unstated, вы будете просто использовать React — я буду это только приветствовать. Пишите об этом в своих блогах! Выступайте об этом на конференциях! Делитесь своими знаниями с сообществом. 518 | 519 | ## Миграция с `unstated` 520 | 521 | Я нарочно публикую эту библиотеку как отдельный пакет, потому что весь API полностью новый. Поэтому вы можете параллельно установить оба пакета и мигрировать постепенно. 522 | 523 | Поделитесь своими впечатлениями о переходе на `unstated-next`, потому что в течение нескольких следующих месяцев я планирую на базе этой информации сделать две вещи: 524 | 525 | - Убедиться, что `unstated-next` удовлетворяет все нужды пользователей `unstated`. 526 | - Удостовериться, что для `unstated` есть четкий и ясный процесс миграции на `unstated-next`. 527 | 528 | Возможно, я добавлю какие-то API в старую или новую библиотеку, чтобы упростить жизнь разработчикам. Что касается `unstated-next`, я обещаю, что добавленные API будут минимальными, насколько это возможно, и я приложу все усилия, чтобы библиотека осталась маленькой. 529 | 530 | В будущем, я, вероятно, перенесу код `unstated-next` обратно в `unstated` в качестве новой мажорной версии. `unstated-next` будет по-прежнему доступен, чтобы можно было параллельно пользоваться `unstated@2` и `unstated-next` в одном проекте. Затем, когда вы закончите миграцию, вы сможете обновиться до версии `unstated@3` и удалить `unstated-next` (разумеется, обновив все импорты... поиска и замены должно быть достаточно). 531 | 532 | Несмотря на кардинальную смену API, я надеюсь, что смогу обеспечить вам максимально простую миграцию, насколько это вообще возможно. Я оптимизирую процесс с точки зрения использования самых последних API React-хуков, а не с точки зрения сохранения кода, написанного с использованием `Unstated.Container`-ов. Буду рад любым замечаниям о том, что можно было бы сделать лучше. 533 | -------------------------------------------------------------------------------- /README-th-th.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | English | 4 | 中文 | 5 | Русский | 6 | ภาษาไทย | 7 | Tiếng Việt 8 | 9 |
10 | (Please contribute translations!) 11 |

12 | 13 | # Unstated Next 14 | 15 | > ขนาดเพียง 200 ไบต์ ดังนั้น ฉันไม่เคยคิดเรื่อง React state management libraries อีกเลย 16 | 17 | - **React Hooks** ใช้เพื่อจัดการสถานะทั้งหมดของคุณ 18 | - **~200 bytes** มีขนาดเพียง 200 ไบต์เท่านั้น 19 | - **Familiar API** เป็นเพียงส่วนเล็กๆในการเขียน React 20 | - **Minimal API** ใช้เวลาเพียง 5 นาทีในการเรียนรู้ 21 | - **Written in TypeScript** ทำให้การเขียน React สะดวกมากยิ่งขึ้น 22 | 23 | แต่.. คำถามสำคัญคือ มันดีกว่า Redux จริงหรือไม่ ? มาดูคำตอบกัน... 24 | 25 | - **It's smaller.** มีขนาดเล็ก 40 เท่า 26 | - **It's faster.** แก้ปัญหาเรื่องประสิทธิภาพการทำงานให้ดียิ่งขึ้น 27 | - **It's easier to learn.** สิ่งที่คุณจะต้องรู้คือ React Hooks และ Context 28 | - **It's easier to integrate.** ง่ายต่อการนำไปต่อยอดและ สามารถทำงานร่วมกับ React library ได้อย่างสบายๆ 29 | - **It's easier to test.** ลดการระยะเวลาในการทดสอบ 30 | - **It's easier to typecheck.** ออกแบบมาเพื่อทำให้คุณสามารถนำไปพัฒนาต่อได้ 31 | - **It's minimal.** มันเป็นเพียง React 32 | 33 | ดังนั้น ถ้าคุณตัดสินใจ 34 | 35 | ### [พัฒนาต่อยอดมาจาก Unstated docs →](#migration-from-unstated) 36 | 37 | ## ติดตั้ง 38 | 39 | ```sh 40 | npm install --save unstated-next 41 | ``` 42 | 43 | ## ตัวอย่าง 44 | 45 | ```js 46 | import React, { useState } from "react" 47 | import { createContainer } from "unstated-next" 48 | import { render } from "react-dom" 49 | 50 | function useCounter(initialState = 0) { 51 | let [count, setCount] = useState(initialState) 52 | let decrement = () => setCount(count - 1) 53 | let increment = () => setCount(count + 1) 54 | return { count, decrement, increment } 55 | } 56 | 57 | let Counter = createContainer(useCounter) 58 | 59 | function CounterDisplay() { 60 | let counter = Counter.useContainer() 61 | return ( 62 |
63 | 64 | {counter.count} 65 | 66 |
67 | ) 68 | } 69 | 70 | function App() { 71 | return ( 72 | 73 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | render(, document.getElementById("root")) 86 | ``` 87 | 88 | ## API 89 | 90 | ### `createContainer(useHook)` 91 | 92 | ```js 93 | import { createContainer } from "unstated-next" 94 | 95 | function useCustomHook() { 96 | let [value, setValue] = useState() 97 | let onChange = e => setValue(e.currentTarget.value) 98 | return { value, onChange } 99 | } 100 | 101 | let Container = createContainer(useCustomHook) 102 | // Container === { Provider, useContainer } 103 | ``` 104 | 105 | ### `` 106 | 107 | ```js 108 | function ParentComponent() { 109 | return ( 110 | 111 | 112 | 113 | ) 114 | } 115 | ``` 116 | 117 | ### `` 118 | 119 | ```js 120 | function useCustomHook(initialState = "") { 121 | let [value, setValue] = useState(initialState) 122 | // ... 123 | } 124 | 125 | function ParentComponent() { 126 | return ( 127 | 128 | 129 | 130 | ) 131 | } 132 | ``` 133 | 134 | ### `Container.useContainer()` 135 | 136 | ```js 137 | function ChildComponent() { 138 | let input = Container.useContainer() 139 | return 140 | } 141 | ``` 142 | 143 | ### `useContainer(Container)` 144 | 145 | ```js 146 | import { useContainer } from "unstated-next" 147 | 148 | function ChildComponent() { 149 | let input = useContainer(Container) 150 | return 151 | } 152 | ``` 153 | 154 | ## คู่มือ 155 | 156 | ถ้าคุณไม่เคยใช้ React Hooks มาก่อน, เราแนะนำให้หยุดและอ่านเอกสารของ React Hooks ก่อน [เอกสารของ React Hooks](https://reactjs.org/docs/hooks-intro.html). 157 | 158 | ดังนั้นโดย hooks คุณอาจสร้างคอมโพเนนต์เหมือนกับตัวอย่างข้างล่างนี้ 159 | 160 | ```js 161 | function CounterDisplay() { 162 | let [count, setCount] = useState(0) 163 | let decrement = () => setCount(count - 1) 164 | let increment = () => setCount(count + 1) 165 | return ( 166 |
167 | 168 |

You clicked {count} times

169 | 170 |
171 | ) 172 | } 173 | ``` 174 | 175 | จากนั้นถ้าคุณต้องการ share logic behind หลังคอมโพเนนต์, คุณสามารถดึงมันออกมาเพื่อเป็น Hooks ที่สามารถกำหนดเองได้ : 176 | 177 | 178 | ```js 179 | function useCounter() { 180 | let [count, setCount] = useState(0) 181 | let decrement = () => setCount(count - 1) 182 | let increment = () => setCount(count + 1) 183 | return { count, decrement, increment } 184 | } 185 | 186 | function CounterDisplay() { 187 | let counter = useCounter() 188 | return ( 189 |
190 | 191 |

You clicked {counter.count} times

192 | 193 |
194 | ) 195 | } 196 | ``` 197 | 198 | แต่ถ้าคุณต้องการ share state นอกเหนือจาก logic, ควรจะทำอย่างไร ? 199 | 200 | นี่เป็นตัวอย่าง : 201 | 202 | ```js 203 | function useCounter() { 204 | let [count, setCount] = useState(0) 205 | let decrement = () => setCount(count - 1) 206 | let increment = () => setCount(count + 1) 207 | return { count, decrement, increment } 208 | } 209 | 210 | let Counter = createContext(null) 211 | 212 | function CounterDisplay() { 213 | let counter = useContext(Counter) 214 | return ( 215 |
216 | 217 |

You clicked {counter.count} times

218 | 219 |
220 | ) 221 | } 222 | 223 | function App() { 224 | let counter = useCounter() 225 | return ( 226 | 227 | 228 | 229 | 230 | ) 231 | } 232 | ``` 233 | 234 | มันเยี่ยมมาก , ยอดเยี่ยม, คนส่วนมากควรเขียนโค้ดแบบนี้ 235 | 236 | แต่บางครั้งเราทุกคนจำเป็นต้องมีโครงสร้างเพิ่มเติมเล็กน้อย และ โดยเพื่อ API design ให้มันถูกต้องอย่างสม่ำเสมอ 237 | 238 | โดย เริ่มต้นฟังก์ชัน `createContainer()` , คุณสามารถใช้ Hooks แบบกำหนดเองได้ "containers" และมี API คอยเตือนและ ป้องกันคุณจากการใช้งานผิดพลาด 239 | 240 | ```js 241 | import { createContainer } from "unstated-next" 242 | 243 | function useCounter() { 244 | let [count, setCount] = useState(0) 245 | let decrement = () => setCount(count - 1) 246 | let increment = () => setCount(count + 1) 247 | return { count, decrement, increment } 248 | } 249 | 250 | let Counter = createContainer(useCounter) 251 | 252 | function CounterDisplay() { 253 | let counter = Counter.useContainer() 254 | return ( 255 |
256 | 257 |

You clicked {counter.count} times

258 | 259 |
260 | ) 261 | } 262 | 263 | function App() { 264 | return ( 265 | 266 | 267 | 268 | 269 | ) 270 | } 271 | ``` 272 | 273 | นี่คือความแตกต่างของการเปลี่ยนแปลง : 274 | 275 | ```diff 276 | - import { createContext, useContext } from "react" 277 | + import { createContainer } from "unstated-next" 278 | 279 | function useCounter() { 280 | ... 281 | } 282 | 283 | - let Counter = createContext(null) 284 | + let Counter = createContainer(useCounter) 285 | 286 | function CounterDisplay() { 287 | - let counter = useContext(Counter) 288 | + let counter = Counter.useContainer() 289 | return ( 290 |
291 | ... 292 |
293 | ) 294 | } 295 | 296 | function App() { 297 | - let counter = useCounter() 298 | return ( 299 | - 300 | + 301 | 302 | 303 | 304 | ) 305 | } 306 | ``` 307 | 308 | หากคุณใช้ TypeScript (ซึ่งฉันขอแนะนำให้คุณเรียนรู้เพิ่มเติม), สิ่งนี้ยังมีประโยชน์ในการสร้าง TypeScript เพื่อสามารถนำไปต่อยอดของทำงานได้ดีขึ้น. 309 | 310 | ## เทคนิค 311 | 312 | ### เทคนิค #1: Composing Containers 313 | 314 | เพราะ เราสามารถทำงานกับ React hooks แบบกำหนดเองได้, เราสามารถเขียนคอนเทนเนอร์ภายใน hooks อื่นๆ ได้ 315 | 316 | ```js 317 | function useCounter() { 318 | let [count, setCount] = useState(0) 319 | let decrement = () => setCount(count - 1) 320 | let increment = () => setCount(count + 1) 321 | return { count, decrement, increment, setCount } 322 | } 323 | 324 | let Counter = createContainer(useCounter) 325 | 326 | function useResettableCounter() { 327 | let counter = Counter.useContainer() 328 | let reset = () => counter.setCount(0) 329 | return { ...counter, reset } 330 | } 331 | ``` 332 | 333 | ### เทคนิค #2: Keeping Containers Small 334 | 335 | สิ่งนี้มีประโยชน์ในการเก็บในค่าไว้ใน containers เล็กและชัดเจน. ซึ่งมันเป็นสิ่งสำคัญ ถ้าคุณต้องการทำ code split กับ logic ใน containers ของคุณ : แค่ย้ายทั้งหมดไปที่ own hooks และ เก็บค่า state ไว้ใน containers. 336 | 337 | ```js 338 | function useCount() { 339 | return useState(0) 340 | } 341 | 342 | let Count = createContainer(useCount) 343 | 344 | function useCounter() { 345 | let [count, setCount] = Count.useContainer() 346 | let decrement = () => setCount(count - 1) 347 | let increment = () => setCount(count + 1) 348 | let reset = () => setCount(0) 349 | return { count, decrement, increment, reset } 350 | } 351 | ``` 352 | 353 | ### เทคนิค #3: Optimizing components 354 | 355 | ไม่มี "การเพิ่มประสิทธิภาพ" ใน `unstated-next` , เพราะ React ทำการเพิ่มประสิทธิภาพไว้ให้อยู่แล้ว 356 | 357 | #### 1) เพิ่มประสิทธิภาพโดย splitting component ออกเป็นส่วนๆ 358 | 359 | **ก่อน:** 360 | 361 | ```js 362 | function CounterDisplay() { 363 | let counter = Counter.useContainer() 364 | return ( 365 |
366 | 367 |

You clicked {counter.count} times

368 | 369 |
370 |
371 |
372 |
SUPER EXPENSIVE RENDERING STUFF
373 |
374 |
375 |
376 |
377 | ) 378 | } 379 | ``` 380 | 381 | **หลัง:** 382 | 383 | ```js 384 | function ExpensiveComponent() { 385 | return ( 386 |
387 |
388 |
389 |
SUPER EXPENSIVE RENDERING STUFF
390 |
391 |
392 |
393 | ) 394 | } 395 | 396 | function CounterDisplay() { 397 | let counter = Counter.useContainer() 398 | return ( 399 |
400 | 401 |

You clicked {counter.count} times

402 | 403 | 404 |
405 | ) 406 | } 407 | ``` 408 | 409 | #### 2) เพิ่มประสิทธิภาพด้วย useMemo() 410 | 411 | **ก่อน:** 412 | 413 | ```js 414 | function CounterDisplay(props) { 415 | let counter = Counter.useContainer() 416 | 417 | // Recalculating this every time `counter` changes is expensive 418 | let expensiveValue = expensiveComputation(props.input) 419 | 420 | return ( 421 |
422 | 423 |

You clicked {counter.count} times

424 | 425 |
426 | ) 427 | } 428 | ``` 429 | 430 | **หลัง:** 431 | 432 | ```js 433 | function CounterDisplay(props) { 434 | let counter = Counter.useContainer() 435 | 436 | // Only recalculate this value when its inputs have changed 437 | let expensiveValue = useMemo(() => { 438 | return expensiveComputation(props.input) 439 | }, [props.input]) 440 | 441 | return ( 442 |
443 | 444 |

You clicked {counter.count} times

445 | 446 |
447 | ) 448 | } 449 | ``` 450 | 451 | #### 3) ลดการ re-renders โดยใช้ React.memo() และ useCallback() 452 | 453 | **ก่อน:** 454 | 455 | ```js 456 | function useCounter() { 457 | let [count, setCount] = useState(0) 458 | let decrement = () => setCount(count - 1) 459 | let increment = () => setCount(count + 1) 460 | return { count, decrement, increment } 461 | } 462 | 463 | let Counter = createContainer(useCounter) 464 | 465 | function CounterDisplay(props) { 466 | let counter = Counter.useContainer() 467 | return ( 468 |
469 | 470 |

You clicked {counter.count} times

471 | 472 |
473 | ) 474 | } 475 | ``` 476 | 477 | **หลัง:** 478 | 479 | ```js 480 | function useCounter() { 481 | let [count, setCount] = useState(0) 482 | let decrement = useCallback(() => setCount(count - 1), [count]) 483 | let increment = useCallback(() => setCount(count + 1), [count]) 484 | return { count, decrement, increment } 485 | } 486 | 487 | let Counter = createContainer(useCounter) 488 | 489 | let CounterDisplayInner = React.memo(props => { 490 | return ( 491 |
492 | 493 |

You clicked {props.count} times

494 | 495 |
496 | ) 497 | }) 498 | 499 | function CounterDisplay(props) { 500 | let counter = Counter.useContainer() 501 | return 502 | } 503 | ``` 504 | 505 | #### 4) ครอบ elements ของคุณด้วย `useMemo()` 506 | 507 | [via Dan Abramov](https://github.com/facebook/react/issues/15156#issuecomment-474590693) 508 | 509 | **ก่อน:** 510 | 511 | ```js 512 | function CounterDisplay(props) { 513 | let counter = Counter.useContainer() 514 | let count = counter.count 515 | 516 | return ( 517 |

You clicked {count} times

518 | ) 519 | } 520 | ``` 521 | 522 | **หลัง:** 523 | 524 | ```js 525 | function CounterDisplay(props) { 526 | let counter = Counter.useContainer() 527 | let count = counter.count 528 | 529 | return useMemo(() => ( 530 |

You clicked {count} times

531 | ), [count]) 532 | } 533 | ``` 534 | 535 | ## สิ่งนี้สัมพันธ์กับ Unstated 536 | 537 | สิ่งนี้โดยสืบทอดมาจาก [Unstated](https://github.com/jamiebuilds/unstated). ฉันสร้าง Unstated เพราะฉันเชื่อว่า React ยอดเยี่ยมมาก ในเรื่องของการจัดการ state อยู่แล้ว และ สิ่งที่ขาดหายไปเพียงอย่างเดียวคือ sharing state และ logic อย่างง่าย. ดังนั้น ฉันสร้าง Unstated เพื่อให้มัน "minimal" ที่สุด และ จะสามารถจัดการเรื่อง sharing React state และ logic. 538 | 539 | อย่างไรก็ตาม, โดย React Hooks เคลมว่า จะสามารถจัดการ sharing state and logic ได้ดีมากยิ่งขึ้น. จนถึงจุดที่ฉันคิดว่า Unstated ได้กลายเป็นสิ่งที่ไม่จำเป็นแล้ว. 540 | 541 | **อย่างไรก็ตาม**, ฉันคิดว่านักพัฒนาซอฟต์แวร์หลายคนดิ้นรน เพื่อดูว่า share state และ logic ทำอย่างไร โดย React Hooks สำหรับ "application state". นั่นอาจเป็นปัญหาของ เอกสาร และ ชุมชน, แต่ฉันคิดว่า API สามารถช่วยเชื่อมโยงสิ่งนั้นได้ โดยอุดช่องว่างของปัญหาที่เกิดขึ้น. 542 | 543 | API นั้นคืออะไร Unstated Next. แทนที่จะเป็น "Minimal API for sharing state and logic in React", ตอนนี้มันเป็น "Minimal API for understanding shared state and logic in React". 544 | 545 | แทนที่จะใช้ Unstated, คุณแค่ต้องการใช้ React, ฉันขอแนะนำว่า. ควรจะเขียนบล็อกเกี่ยวกับสิ่งนั้น !! และ กระจายความรู้ เพื่อเป็นทำให้เกิด community ที่ดีขึ้น 546 | 547 | ## ย้ายจาก `unstated` 548 | 549 | ฉันตั้งใจเผยแพร่ นี่เป็นแพ็คเกจแยกต่างหาก เพราะ เป็นการรีเซ็ตที่สมบูรณ์ใน API. วิธีนี้คุณสามารถมีทั้งการติดตั้งและ ย้ายมา เพื่อใช้งานแบบเป็นค่อยไป ทีละ Component. 550 | 551 | โปรดให้ข้อเสนอแนะกับฉัน เกี่ยวกับกระบวนการย้ายมาใช้งาน เพราะในอีกไม่กี่เดือนข้างหน้าฉันหวังว่าจะรับข้อเสนอแนะนั้นและ ทำสองสิ่ง: 552 | 553 | - ตรวจสอบให้แน่ใจ `unstated-next` ตอบสนองทุกความต้องการของผู้ใช้งาน `unstated`. 554 | - ตรวจสอบให้แน่ใจ `unstated` มีกระบวนการโยกย้ายมาใช้งานที่ clean `unstated-next`. 555 | 556 | ฉันอาจเลือกที่จะเพิ่ม API ทั้ง library เพื่อทำให้นักพัฒนา ทำงานได้ง่ายขึ้น. สำหรับ `unstated-next` ฉันสัญญาว่า API ที่เพิ่มจะน้อยที่สุดเท่าที่จะเป็นไปได้ และ ฉันจะพยายามจะเก็บ library ให้มีขนาดเล็กที่สุด. 557 | 558 | ในอนาคต, ฉันคิดว่าจะรวม `unstated-next` เข้าไปใน `unstated` ที่เป็นเวอร์ชันหลักในตอนนี้. `unstated-next` จะยังคงอยู่เพื่อให้คุณสามารถติดตั้งทั้ง `unstated@2` และ `unstated-next` ได้. จากนั้นเมื่อทำการโยกย้ายเสร็จ คุณสามารถอัปเดตเป็น`unstated@3` และ จะลบ `unstated-next` 559 | 560 | แม้ว่านี่เป็นการเปลี่ยนแปลง API ใหม่ที่สำคัญ, ฉันหวังว่าฉันจะทำให้การย้ายครั้งนี้เป็นเรื่องง่ายที่สุดสำหรับคุณ. ฉันจะปรับให้เหมาะสม เพื่อให้คุณใช้งานได้ React Hooks APIs และ ไม่ใช่สำหรับ preserving code ที่เขียนด้วย `Unstated.Container`. และ อย่าลังเลที่จะให้ข้อเสนอแนะเกี่ยวกับวิธีการที่สามารถทำได้ดีกว่าวิธีการที่ฉันคิดไว้ -------------------------------------------------------------------------------- /README-vi-vn.md: -------------------------------------------------------------------------------- 1 | 

2 | 3 | English | 4 | 中文 | 5 | Русский | 6 | ภาษาไทย | 7 | Tiếng Việt 8 | 9 |
10 | (Please contribute translations!) 11 |

12 | 13 | # Unstated Next 14 | 15 | > Chỉ với 200 bytes, không cần lo nghĩ về quản lý React state 16 | 17 | - **React Hooks** _dùng React Hooks để quản lý tất cả các state._ 18 | - **~200 bytes** _min+gz._ 19 | - **API quen thuộc** _viết code như React thuần._ 20 | - **API đơn giản** _chỉ tốn 5 phút để học._ 21 | - **Written in TypeScript** _việc đặt kiểu cho React code sẽ rất dễ dàng._ 22 | 23 | Tuy nhiên, câu hỏi quan trọng là: Nó có tốt hơn Redux? Để xem nào... 24 | 25 | - **It's smaller.** _Unstated-next nhỏ gọn hơn đến 40 lần._ 26 | - **It's faster.** _Component hóa vấn đề về tốc độ._ 27 | - **It's easier to learn.** _Nếu bạn đã biết về React Hooks & Context, chỉ cần sử dụng thôi, nó rất tuyệt._ 28 | - **It's easier to integrate.** _Tương thích với từng Component một, và có thể kết hợp dễ dàng với mọi thư viện React khác._ 29 | - **It's easier to test.** _Kiểm thử reducers rất phí thời gian, nó sẽ đơn giản hơn khi bạn chỉ cần test React components._ 30 | - **It's easier to typecheck.** _Thiết kế ra để bạn định nghĩa được hầu hết các types._ 31 | - **It's minimal.** _Nó chỉ là React._ 32 | 33 | Vậy quyết định là ở bạn. 34 | 35 | ### [Xem các nâng cấp từ Unstated →](#migration-from-unstated) 36 | 37 | ## Cài đặt 38 | 39 | ```sh 40 | npm install --save unstated-next 41 | ``` 42 | 43 | ## Ví dụ 44 | 45 | ```js 46 | import React, { useState } from "react" 47 | import { createContainer } from "unstated-next" 48 | import { render } from "react-dom" 49 | 50 | function useCounter(initialState = 0) { 51 | let [count, setCount] = useState(initialState) 52 | let decrement = () => setCount(count - 1) 53 | let increment = () => setCount(count + 1) 54 | return { count, decrement, increment } 55 | } 56 | 57 | let Counter = createContainer(useCounter) 58 | 59 | function CounterDisplay() { 60 | let counter = Counter.useContainer() 61 | return ( 62 |
63 | 64 | {counter.count} 65 | 66 |
67 | ) 68 | } 69 | 70 | function App() { 71 | return ( 72 | 73 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | render(, document.getElementById("root")) 86 | ``` 87 | 88 | ## API 89 | 90 | ### `createContainer(useHook)` 91 | 92 | ```js 93 | import { createContainer } from "unstated-next" 94 | 95 | function useCustomHook() { 96 | let [value, setValue] = useState() 97 | let onChange = e => setValue(e.currentTarget.value) 98 | return { value, onChange } 99 | } 100 | 101 | let Container = createContainer(useCustomHook) 102 | // Container === { Provider, useContainer } 103 | ``` 104 | 105 | ### `` 106 | 107 | ```js 108 | function ParentComponent() { 109 | return ( 110 | 111 | 112 | 113 | ) 114 | } 115 | ``` 116 | 117 | ### `` 118 | 119 | ```js 120 | function useCustomHook(initialState = "") { 121 | let [value, setValue] = useState(initialState) 122 | // ... 123 | } 124 | 125 | function ParentComponent() { 126 | return ( 127 | 128 | 129 | 130 | ) 131 | } 132 | ``` 133 | 134 | ### `Container.useContainer()` 135 | 136 | ```js 137 | function ChildComponent() { 138 | let input = Container.useContainer() 139 | return 140 | } 141 | ``` 142 | 143 | ### `useContainer(Container)` 144 | 145 | ```js 146 | import { useContainer } from "unstated-next" 147 | 148 | function ChildComponent() { 149 | let input = useContainer(Container) 150 | return 151 | } 152 | ``` 153 | 154 | ## Hướng dẫn 155 | 156 | Nếu bạn chưa bao giờ sử dụng React Hooks trước đây, Tôi khuyên bạn nên dừng lại và đọc qua [tài liệu tuyệt vời trên trang React](https://reactjs.org/docs/hooks-intro.html). 157 | 158 | Vậy với hooks bạn có thể tạo một component như thế này: 159 | 160 | ```js 161 | function CounterDisplay() { 162 | let [count, setCount] = useState(0) 163 | let decrement = () => setCount(count - 1) 164 | let increment = () => setCount(count + 1) 165 | return ( 166 |
167 | 168 |

You clicked {count} times

169 | 170 |
171 | ) 172 | } 173 | ``` 174 | 175 | Sau đó nếu bạn muốn chia sẻ phần logic đằng sau component, bạn có thể tách nó ra thành một custom hook: 176 | 177 | ```js 178 | function useCounter() { 179 | let [count, setCount] = useState(0) 180 | let decrement = () => setCount(count - 1) 181 | let increment = () => setCount(count + 1) 182 | return { count, decrement, increment } 183 | } 184 | 185 | function CounterDisplay() { 186 | let counter = useCounter() 187 | return ( 188 |
189 | 190 |

You clicked {counter.count} times

191 | 192 |
193 | ) 194 | } 195 | ``` 196 | 197 | Nhưng trong trường hợp bạn muốn chia sẻ cả trạng thái lẫn logic, bạn sẽ làm gì? 198 | 199 | Đây sẽ là lúc context giúp được bạn: 200 | 201 | ```js 202 | function useCounter() { 203 | let [count, setCount] = useState(0) 204 | let decrement = () => setCount(count - 1) 205 | let increment = () => setCount(count + 1) 206 | return { count, decrement, increment } 207 | } 208 | 209 | let Counter = createContext(null) 210 | 211 | function CounterDisplay() { 212 | let counter = useContext(Counter) 213 | return ( 214 |
215 | 216 |

You clicked {counter.count} times

217 | 218 |
219 | ) 220 | } 221 | 222 | function App() { 223 | let counter = useCounter() 224 | return ( 225 | 226 | 227 | 228 | 229 | ) 230 | } 231 | ``` 232 | 233 | Điều này thật tuyệt, thật hoàn hảo, và mọi người nên viết code theo cách này. 234 | 235 | Nhưng đôi khi chúng ta đều cần một cấu trúc rõ ràng và được thiết kế bài bản để mang lại một sự nhất quán. 236 | 237 | Xin giới thiệu hàm `createContainer()`, bạn có thể coi như custom hooks của bạn là một "containers" và có một API rõ ràng, có thể ngăn chặn bạn khỏi việc sử dụng nó sai mục đích. 238 | 239 | ```js 240 | import { createContainer } from "unstated-next" 241 | 242 | function useCounter() { 243 | let [count, setCount] = useState(0) 244 | let decrement = () => setCount(count - 1) 245 | let increment = () => setCount(count + 1) 246 | return { count, decrement, increment } 247 | } 248 | 249 | let Counter = createContainer(useCounter) 250 | 251 | function CounterDisplay() { 252 | let counter = Counter.useContainer() 253 | return ( 254 |
255 | 256 |

You clicked {counter.count} times

257 | 258 |
259 | ) 260 | } 261 | 262 | function App() { 263 | return ( 264 | 265 | 266 | 267 | 268 | ) 269 | } 270 | ``` 271 | 272 | Đây là khác biệt của sự thay đổi: 273 | 274 | ```diff 275 | - import { createContext, useContext } from "react" 276 | + import { createContainer } from "unstated-next" 277 | 278 | function useCounter() { 279 | ... 280 | } 281 | 282 | - let Counter = createContext(null) 283 | + let Counter = createContainer(useCounter) 284 | 285 | function CounterDisplay() { 286 | - let counter = useContext(Counter) 287 | + let counter = Counter.useContainer() 288 | return ( 289 |
290 | ... 291 |
292 | ) 293 | } 294 | 295 | function App() { 296 | - let counter = useCounter() 297 | return ( 298 | - 299 | + 300 | 301 | 302 | 303 | ) 304 | } 305 | ``` 306 | 307 | Nếu bạn đang sử dụng TypeScript (cái mà tôi khuyến khích bạn học thêm về nó nếu bạn chưa dùng), nó còn có lợi ích trong việc làm các gợi ý có sẵn của TypeScript's hoạt đông tốt hơn. Miễn là custom hook của bạn đã được đặt kiểu, thì mọi thứ sẽ hoạt động. 308 | 309 | ## Lời khuyên 310 | 311 | ### Tip #1: Tạo các Containers 312 | 313 | Vì chúng ta chỉ đang làm việc với custom React hooks, chúng ta có thể tạo các containers bên trong những hooks khác. 314 | 315 | ```js 316 | function useCounter() { 317 | let [count, setCount] = useState(0) 318 | let decrement = () => setCount(count - 1) 319 | let increment = () => setCount(count + 1) 320 | return { count, decrement, increment, setCount } 321 | } 322 | 323 | let Counter = createContainer(useCounter) 324 | 325 | function useResettableCounter() { 326 | let counter = Counter.useContainer() 327 | let reset = () => counter.setCount(0) 328 | return { ...counter, reset } 329 | } 330 | ``` 331 | 332 | ### Tip #2: Giữ cho những Containers thật nhỏ 333 | 334 | Nó sẽ hữu ích nếu như bạn giữ được các containers của bạn thật nhỏ và tập trung vào một mục đích nhất định. Điều này có thể sẽ quan trọng nếu bạn muốn tách phần logic khỏi containers: Chỉ cần chuyển logic sang phần hooks của nó và giữ state lại ở containers. 335 | 336 | ```js 337 | function useCount() { 338 | return useState(0) 339 | } 340 | 341 | let Count = createContainer(useCount) 342 | 343 | function useCounter() { 344 | let [count, setCount] = Count.useContainer() 345 | let decrement = () => setCount(count - 1) 346 | let increment = () => setCount(count + 1) 347 | let reset = () => setCount(0) 348 | return { count, decrement, increment, reset } 349 | } 350 | ``` 351 | 352 | ### Tip #3: Tối ưu hóa components 353 | 354 | Không có "tối ưu" `unstated-next` cần làm ở đây, tất cả việc tối ưu bạn có thể phải làm đó là tối ưu React code của bạn. 355 | 356 | #### 1) Tối ưu các nhánh component nặng bằng việc chia ra thành một component riêng 357 | 358 | **Trước:** 359 | 360 | ```js 361 | function CounterDisplay() { 362 | let counter = Counter.useContainer() 363 | return ( 364 |
365 | 366 |

You clicked {counter.count} times

367 | 368 |
369 |
370 |
371 |
SUPER EXPENSIVE RENDERING STUFF
372 |
373 |
374 |
375 |
376 | ) 377 | } 378 | ``` 379 | 380 | **Sau:** 381 | 382 | ```js 383 | function ExpensiveComponent() { 384 | return ( 385 |
386 |
387 |
388 |
SUPER EXPENSIVE RENDERING STUFF
389 |
390 |
391 |
392 | ) 393 | } 394 | 395 | function CounterDisplay() { 396 | let counter = Counter.useContainer() 397 | return ( 398 |
399 | 400 |

You clicked {counter.count} times

401 | 402 | 403 |
404 | ) 405 | } 406 | ``` 407 | 408 | #### 2) Tối ưu những phép tính toán nặng bằng useMemo() 409 | 410 | **Trước:** 411 | 412 | ```js 413 | function CounterDisplay(props) { 414 | let counter = Counter.useContainer() 415 | 416 | // Recalculating this every time `counter` changes is expensive 417 | let expensiveValue = expensiveComputation(props.input) 418 | 419 | return ( 420 |
421 | 422 |

You clicked {counter.count} times

423 | 424 |
425 | ) 426 | } 427 | ``` 428 | 429 | **Sau:** 430 | 431 | ```js 432 | function CounterDisplay(props) { 433 | let counter = Counter.useContainer() 434 | 435 | // Only recalculate this value when its inputs have changed 436 | let expensiveValue = useMemo(() => { 437 | return expensiveComputation(props.input) 438 | }, [props.input]) 439 | 440 | return ( 441 |
442 | 443 |

You clicked {counter.count} times

444 | 445 |
446 | ) 447 | } 448 | ``` 449 | 450 | #### 3) Giảm thiểu việc render lại bằng React.memo() và useCallback() 451 | 452 | **Trước:** 453 | 454 | ```js 455 | function useCounter() { 456 | let [count, setCount] = useState(0) 457 | let decrement = () => setCount(count - 1) 458 | let increment = () => setCount(count + 1) 459 | return { count, decrement, increment } 460 | } 461 | 462 | let Counter = createContainer(useCounter) 463 | 464 | function CounterDisplay(props) { 465 | let counter = Counter.useContainer() 466 | return ( 467 |
468 | 469 |

You clicked {counter.count} times

470 | 471 |
472 | ) 473 | } 474 | ``` 475 | 476 | **Sau:** 477 | 478 | ```js 479 | function useCounter() { 480 | let [count, setCount] = useState(0) 481 | let decrement = useCallback(() => setCount(count - 1), [count]) 482 | let increment = useCallback(() => setCount(count + 1), [count]) 483 | return { count, decrement, increment } 484 | } 485 | 486 | let Counter = createContainer(useCounter) 487 | 488 | let CounterDisplayInner = React.memo(props => { 489 | return ( 490 |
491 | 492 |

You clicked {props.count} times

493 | 494 |
495 | ) 496 | }) 497 | 498 | function CounterDisplay(props) { 499 | let counter = Counter.useContainer() 500 | return 501 | } 502 | ``` 503 | 504 | #### 4) Gói các elements của bạn với `useMemo()` 505 | 506 | [theo Dan Abramov](https://github.com/facebook/react/issues/15156#issuecomment-474590693) 507 | 508 | **Trước:** 509 | 510 | ```js 511 | function CounterDisplay(props) { 512 | let counter = Counter.useContainer() 513 | let count = counter.count 514 | 515 | return ( 516 |

You clicked {count} times

517 | ) 518 | } 519 | ``` 520 | 521 | **Sau:** 522 | 523 | ```js 524 | function CounterDisplay(props) { 525 | let counter = Counter.useContainer() 526 | let count = counter.count 527 | 528 | return useMemo(() => ( 529 |

You clicked {count} times

530 | ), [count]) 531 | } 532 | ``` 533 | 534 | ## Sự tương quan với Unstated 535 | 536 | Tôi coi thư viện này là sự kế thừa tinh thần cho [Unstated](https://github.com/jamiebuilds/unstated). Tôi đã tạo ra Unstated vì tôi tin rằng React đã thực sự tuyệt vời trong việc quản state và điều duy nhất còn thiếu là chia sẻ state, logic một cách dễ dàng. Vì vậy, tôi đã tạo ra Unstated, một giải pháp "tối giản" để chia sẻ React state và logic. 537 | 538 | Tuy nhiên, với Hook, React đã trở nên tốt hơn nhiều trong việc chia sẻ state và logic. Đến mức tôi nghĩ Unstated đã trở thành một abstraction không cần thiết. 539 | 540 | **Tuy nhiên**, Tôi nghĩ rằng nhiều nhà phát triển đã vật lộn để xem cách chia sẻ state và logic với React Hook trong một "trạng thái của ứng dụng". Đó có thể chỉ là vấn đề về tài liệu và động lực từ cộng đồng, nhưng tôi nghĩ rằng sẽ có một API có thể giúp thu hẹp khoảng cách về tinh thần đó. 541 | 542 | API đó chính là Unstated Next. Thay vì là "API tối thiểu để chia sẻ state và logic trong React", giờ đây nó là "API tối thiểu để hiểu state và logic được chia sẻ trong React". 543 | 544 | Tôi đã luôn đứng về phía React. Tôi muốn React giành chiến thắng. Tôi muốn thấy cộng đồng từ bỏ các thư viện quản lý state như Redux và tìm ra những cách tốt hơn để sử dụng chuỗi công cụ tích hợp có sẵn của React. 545 | 546 | Nếu thay vì sử dụng Unstated, bạn chỉ muốn sử dụng React, tôi rất khuyến khích điều đó. Viết bài đăng trên blog về nó! Hãy kể về nó! Truyền bá kiến thức của bạn trong cộng đồng. 547 | 548 | ## Cách cập nhật từ `unstated` 549 | 550 | Tôi đã cố tình xuất bản thư viện này dưới dạng tên gói riêng vì API đã được viết lại hoàn toàn. Bằng cách này, bạn có thể cài cả hai và migrate dần dần. 551 | 552 | Vui lòng cung cấp cho tôi thông tin phản hồi về quá trình migrate đó, vì trong vài tháng tới tôi hy vọng sẽ nhận được nhiều thông tin và thực hiện hai điều: 553 | 554 | - Đảm bảo rằng `unstated-next` đáp ứng tất cả các nhu cầu của người dùng` unstated`. 555 | - Đảm bảo rằng `unstated` có một quy trình migrate sạch sẽ đối với` unstated-next`. 556 | 557 | Tôi có thể chọn thêm API vào một trong hai thư viện để giúp các nhà phát triển dễ dàng hơn. Đối với `unstated-next` Tôi hứa rằng các API được thêm vào sẽ tối thiểu nhất có thể và tôi sẽ cố gắng giữ thư viện thật nhỏ. 558 | 559 | Trong tương lai, tôi có thể sẽ hợp nhất `unstated-next` trở lại thành `unstated` như một phiên bản chính mới. `unstated-next` vẫn sẽ tồn tại để bạn có thể cài đặt cả `unstated@2` và `unstated-next`. Sau đó, khi bạn hoàn thành việc migrate, bạn có thể cập nhật thành `unstated@3` và xóa `unstated-next` (chắc chắn bạn đã cập nhật tất cả các imports... bằng cách tìm kiếm và thay thế). 560 | 561 | Mặc dù đây là một thay đổi API mới, tôi hy vọng rằng tôi có thể thực hiện việc migrate này dễ dàng nhất có thể đối với bạn. Tôi đang tối ưu hóa cho bạn để sử dụng API React Hook mới nhất chứ không phải để giữ code được viết bằng `Unstated.Container`. Hãy cho ý kiến của bạn về cách nó có thể được làm tốt hơn. 562 | -------------------------------------------------------------------------------- /README-zh-cn.md: -------------------------------------------------------------------------------- 1 | 

2 | 3 | English | 4 | 中文 | 5 | Русский | 6 | ภาษาไทย | 7 | Tiếng Việt 8 | 9 |
10 | (Please contribute translations!) 11 |

12 | 13 | # Unstated Next 14 | 15 | > 永远不必再考虑 React 状态管理了,仅仅 200 字节的状态管理解决方案。 16 | 17 | - **React Hooks** _React Hooks 用做你所有的状态管理。_ 18 | - **~200 bytes** _min+gz._ 19 | - **熟悉的 API** _仅仅使用了 React,没有依赖第三方库。_ 20 | - **最小 API** _只需 5 分钟学习。_ 21 | - **TypeScript 编写** _推断代码更容易,易于编写 React 代码。_ 22 | 23 | 但是,最重要的问题是:这比 Redux 更好吗? 答案可能是。 24 | 25 | - **它更小。** _比 Redux 小 40 倍。_ 26 | - **它更快。** _组件性能问题。_ 27 | - **它更容易学习。** _你必须已经知道 React Hooks 和 Context 。只需使用它们,它们就会嗨起来。_ 28 | - **更容易集成。** _一次集成一个组件,并且轻松与其他 React 库集成。_ 29 | - **它更容易测试。** _测试 reducers 纯属浪费你的时间,这个库使你更容易测试 React 组件。_ 30 | - **它更容易进行类型检查。** _旨在使你的大多数类型可推断。_ 31 | - **它是最小的。** _仅仅使用了 React 。_ 32 | 33 | 你自己看着办吧! 34 | 35 | ### [查看 Unstated 迁移手册 →](#%E4%BB%8E-unstated-%E8%BF%81%E7%A7%BB) 36 | 37 | ## 安装 38 | 39 | ```sh 40 | npm install --save unstated-next 41 | ``` 42 | 43 | ## Example 44 | 45 | ```js 46 | import React, { useState } from "react" 47 | import { createContainer } from "unstated-next" 48 | import { render } from "react-dom" 49 | 50 | function useCounter(initialState = 0) { 51 | let [count, setCount] = useState(initialState) 52 | let decrement = () => setCount(count - 1) 53 | let increment = () => setCount(count + 1) 54 | return { count, decrement, increment } 55 | } 56 | 57 | let Counter = createContainer(useCounter) 58 | 59 | function CounterDisplay() { 60 | let counter = Counter.useContainer() 61 | return ( 62 |
63 | 64 | {counter.count} 65 | 66 |
67 | ) 68 | } 69 | 70 | function App() { 71 | return ( 72 | 73 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | render(, document.getElementById("root")) 86 | ``` 87 | 88 | ## API 89 | 90 | ### `createContainer(useHook)` 91 | 92 | ```js 93 | import { createContainer } from "unstated-next" 94 | 95 | function useCustomHook() { 96 | let [value, setInput] = useState() 97 | let onChange = e => setValue(e.currentTarget.value) 98 | return { value, onChange } 99 | } 100 | 101 | let Container = createContainer(useCustomHook) 102 | // Container === { Provider, useContainer } 103 | ``` 104 | 105 | ### `` 106 | 107 | ```js 108 | function ParentComponent() { 109 | return ( 110 | 111 | 112 | 113 | ) 114 | } 115 | ``` 116 | 117 | ### `` 118 | 119 | ```js 120 | function useCustomHook(initialState = "") { 121 | let [value, setValue] = useState(initialState) 122 | // ... 123 | } 124 | 125 | function ParentComponent() { 126 | return ( 127 | 128 | 129 | 130 | ) 131 | } 132 | ``` 133 | 134 | ### `Container.useContainer()` 135 | 136 | ```js 137 | function ChildComponent() { 138 | let input = Container.useContainer() 139 | return 140 | } 141 | ``` 142 | 143 | ### `useContainer(Container)` 144 | 145 | ```js 146 | import { useContainer } from "unstated-next" 147 | 148 | function ChildComponent() { 149 | let input = useContainer(Container) 150 | return 151 | } 152 | ``` 153 | 154 | ## 指南 155 | 156 | 如果你以前从未使用过 React Hooks,我不建议你往下看,请先阅读 [React 官网的 React Hooks 文档](https://reactjs.org/docs/hooks-intro.html)。 157 | 158 | 首先,使用 React Hooks,你可以创建这样一个组件: 159 | 160 | ```js 161 | function CounterDisplay() { 162 | let [count, setCount] = useState(0) 163 | let decrement = () => setCount(count - 1) 164 | let increment = () => setCount(count + 1) 165 | return ( 166 |
167 | 168 |

You clicked {count} times

169 | 170 |
171 | ) 172 | } 173 | ``` 174 | 175 | 然后,如果你想共享组件的逻辑,你可以把它写在组件外面,自定义一个 hook: 176 | 177 | ```js 178 | function useCounter() { 179 | let [count, setCount] = useState(0) 180 | let decrement = () => setCount(count - 1) 181 | let increment = () => setCount(count + 1) 182 | return { count, decrement, increment } 183 | } 184 | 185 | function CounterDisplay() { 186 | let counter = useCounter() 187 | return ( 188 |
189 | 190 |

You clicked {counter.count} times

191 | 192 |
193 | ) 194 | } 195 | ``` 196 | 197 | 但是,除了共享逻辑之外,你还想共享状态,你会怎么做呢? 198 | 199 | 这个时候,context 就发挥了作用: 200 | 201 | ```js 202 | function useCounter() { 203 | let [count, setCount] = useState(0) 204 | let decrement = () => setCount(count - 1) 205 | let increment = () => setCount(count + 1) 206 | return { count, decrement, increment } 207 | } 208 | 209 | let Counter = createContext(null) 210 | 211 | function CounterDisplay() { 212 | let counter = useContext(Counter) 213 | return ( 214 |
215 | 216 |

You clicked {counter.count} times

217 | 218 |
219 | ) 220 | } 221 | 222 | function App() { 223 | let counter = useCounter() 224 | return ( 225 | 226 | 227 | 228 | 229 | ) 230 | } 231 | ``` 232 | 233 | 这很棒,也很完美,更多人应该编写这样的代码。 234 | 235 | 但有时我们需要更多的结构和特定的 API 设计才能使其始终保持正确。 236 | 237 | 通过引入 `createContainer()` 函数,你可以将自定义 hooks 作为 containers,并且定义明确的 API,防止错误使用。 238 | 239 | ```js 240 | import { createContainer } from "unstated-next" 241 | 242 | function useCounter() { 243 | let [count, setCount] = useState(0) 244 | let decrement = () => setCount(count - 1) 245 | let increment = () => setCount(count + 1) 246 | return { count, decrement, increment } 247 | } 248 | 249 | let Counter = createContainer(useCounter) 250 | 251 | function CounterDisplay() { 252 | let counter = Counter.useContainer() 253 | return ( 254 |
255 | 256 |

You clicked {counter.count} times

257 | 258 |
259 | ) 260 | } 261 | 262 | function App() { 263 | return ( 264 | 265 | 266 | 267 | 268 | ) 269 | } 270 | ``` 271 | 272 | 下面是前后的代码对比: 273 | 274 | ```diff 275 | - import { createContext, useContext } from "react" 276 | + import { createContainer } from "unstated-next" 277 | 278 | function useCounter() { 279 | ... 280 | } 281 | 282 | - let Counter = createContext(null) 283 | + let Counter = createContainer(useCounter) 284 | 285 | function CounterDisplay() { 286 | - let counter = useContext(Counter) 287 | + let counter = Counter.useContainer() 288 | return ( 289 |
290 | ... 291 |
292 | ) 293 | } 294 | 295 | function App() { 296 | - let counter = useCounter() 297 | return ( 298 | - 299 | + 300 | 301 | 302 | 303 | ) 304 | } 305 | ``` 306 | 307 | 如果你正在使用 TypeScript(我鼓励你了解更多关于它的信息),这也有助于 TypeScript 的内置推断做得更好。只要你的自定义 hook 类型是完善的,那么类型都会自动推断。 308 | 309 | ## 提示 310 | 311 | ### 提示 #1: 组合 Containers 312 | 313 | 因为我们只使用了自定义 React hooks,所以可以在其他 hooks 内部组合 containers。 314 | 315 | ```js 316 | function useCounter() { 317 | let [count, setCount] = useState(0) 318 | let decrement = () => setCount(count - 1) 319 | let increment = () => setCount(count + 1) 320 | return { count, decrement, increment, setCount } 321 | } 322 | 323 | let Counter = createContainer(useCounter) 324 | 325 | function useResettableCounter() { 326 | let counter = Counter.useContainer() 327 | let reset = () => counter.setCount(0) 328 | return { ...counter, reset } 329 | } 330 | ``` 331 | 332 | ### 提示 #2: 保持 Containers 很小 333 | 334 | 这对于保持 containers 小而集中非常有用。 如果你想在 containers 中对代码进行逻辑拆分,那么这一点非常重要。只需将它们移动到自己的 hooks 中,仅保存 containers 的状态即可。 335 | 336 | ```js 337 | function useCount() { 338 | return useState(0) 339 | } 340 | 341 | let Count = createContainer(useCount) 342 | 343 | function useCounter() { 344 | let [count, setCount] = Count.useContainer() 345 | let decrement = () => setCount(count - 1) 346 | let increment = () => setCount(count + 1) 347 | let reset = () => setCount(0) 348 | return { count, decrement, increment, reset } 349 | } 350 | ``` 351 | 352 | ### 提示 #3: 优化组件 353 | 354 | `unstated-next` 无需优化。所有你要做的优化,都是标准的 React 优化。 355 | 356 | #### 1) 通过拆分组件来优化耗时的子树 357 | 358 | **优化前:** 359 | 360 | ```js 361 | function CounterDisplay() { 362 | let counter = Counter.useContainer() 363 | return ( 364 |
365 | 366 |

You clicked {counter.count} times

367 | 368 |
369 |
370 |
371 |
SUPER EXPENSIVE RENDERING STUFF
372 |
373 |
374 |
375 |
376 | ) 377 | } 378 | ``` 379 | 380 | **优化后:** 381 | 382 | ```js 383 | function ExpensiveComponent() { 384 | return ( 385 |
386 |
387 |
388 |
SUPER EXPENSIVE RENDERING STUFF
389 |
390 |
391 |
392 | ) 393 | } 394 | 395 | function CounterDisplay() { 396 | let counter = Counter.useContainer() 397 | return ( 398 |
399 | 400 |

You clicked {counter.count} times

401 | 402 | 403 |
404 | ) 405 | } 406 | ``` 407 | 408 | #### 2) 使用 useMemo() 优化耗时的操作 409 | 410 | **优化前:** 411 | 412 | ```js 413 | function CounterDisplay(props) { 414 | let counter = Counter.useContainer() 415 | 416 | // 每次 `counter` 改变都要重新计算这个值,非常耗时 417 | let expensiveValue = expensiveComputation(props.input) 418 | 419 | return ( 420 |
421 | 422 |

You clicked {counter.count} times

423 | 424 |
425 | ) 426 | } 427 | ``` 428 | 429 | **优化后:** 430 | 431 | ```js 432 | function CounterDisplay(props) { 433 | let counter = Counter.useContainer() 434 | 435 | // 仅在输入更改时重新计算这个值 436 | let expensiveValue = useMemo(() => { 437 | return expensiveComputation(props.input) 438 | }, [props.input]) 439 | 440 | return ( 441 |
442 | 443 |

You clicked {counter.count} times

444 | 445 |
446 | ) 447 | } 448 | ``` 449 | 450 | #### 3) 使用 React.memo()、useCallback() 减少重新渲染次数 451 | 452 | **优化前:** 453 | 454 | ```js 455 | function useCounter() { 456 | let [count, setCount] = useState(0) 457 | let decrement = () => setCount(count - 1) 458 | let increment = () => setCount(count + 1) 459 | return { count, decrement, increment } 460 | } 461 | 462 | let Counter = createContainer(useCounter) 463 | 464 | function CounterDisplay(props) { 465 | let counter = Counter.useContainer() 466 | return ( 467 |
468 | 469 |

You clicked {counter.count} times

470 | 471 |
472 | ) 473 | } 474 | ``` 475 | 476 | **优化后:** 477 | 478 | ```js 479 | function useCounter() { 480 | let [count, setCount] = useState(0) 481 | let decrement = useCallback(() => setCount(count - 1), [count]) 482 | let increment = useCallback(() => setCount(count + 1), [count]) 483 | return { count, decrement, increment } 484 | } 485 | 486 | let Counter = createContainer(useCounter) 487 | 488 | let CounterDisplayInner = React.memo(props => { 489 | return ( 490 |
491 | 492 |

You clicked {props.count} times

493 | 494 |
495 | ) 496 | }) 497 | 498 | function CounterDisplay(props) { 499 | let counter = Counter.useContainer() 500 | return 501 | } 502 | ``` 503 | 504 | ## 与 Unstated 的关系 505 | 506 | 我认为这个库是 [Unstated](https://github.com/jamiebuilds/unstated) 精神的继承者。因为我相信 React 在状态管理方面已经非常出色,唯一缺少的就是轻松共享状态和逻辑,所以我创建了 Unstated 。我创建的 Unstated 是 React 共享状态和逻辑的 **最小** 解决方案。 507 | 508 | 然而,使用 Hooks,React 在共享状态和逻辑方面可以做得更好。我甚至认为 Unstated 成为了不必要的抽象。 509 | 510 | **但是**,我认为很多开发人员都在努力了解如何使用 React Hooks 共享状态和逻辑,从而实现应用程序共享状态。这可能只是文档和社区动力的问题,但我认为一个新的 API 可以弥补这种心理差距。 511 | 512 | 这个 API 就是 Unstated Next。 它不是 **React 中共享状态和逻辑的最小 API**,而是**用于理解如何在 React 中共享状态和逻辑的最小 API**。 513 | 514 | 我一直给 React 站队。我希望 React 可以赢。 我希望社区放弃像 Redux 这样的状态管理库,并找到使用 React 内置工具链的更好方法。 515 | 516 | 如果你不想使用 Unstated,你只想使用 React 本身,我非常鼓励你这么做。 写关于它的博客文章! 讨论它! 在社区中传播你的知识。 517 | 518 | ## 从 `unstated` 迁移 519 | 520 | 我故意将其发布为单独的包,因为它是对原有 API 的完全重写。 这样,你可以逐步安装和迁移。 521 | 522 | 请向我提供有关该迁移过程的反馈,因为在接下来的几个月里,我希望得到这些反馈并做以下两件事: 523 | 524 | - 确保 `unstated-next` 满足 `unstated` 使用者的所有需求。 525 | - 确保 `unstated` 使用者的代码可以完整地迁移到 `unstated-next`。 526 | 527 | 我可以将 API 新增到两者的任意一个仓库中,从而使开发人员工作得更轻松。 对于 `unstated-next`,我将保证新增的 API 尽可能小,同时,我也会尽量保持库很小。 528 | 529 | 未来,我可能会将 `unstated-next` 合并为 `unstated` 的主要版本。 `unstated-next` 仍然存在,这样你就可以安装 `unstated@2` 和 `unstated-next`。 当你完成迁移后,你可以更新到 `unstated@3` ,同时删除 `unstated-next`(确保更新你所有的引入,这应该只是一个查找和替换的过程)。 530 | 531 | 尽管这是一个重大的 API 更改,我希望你尽可能轻松地完成此迁移。我正在使用最新的 React Hooks API ,为你进行优化,而不是使用原有的 `Unstated.Container` 代码。请随意提供有关如何做得更好的反馈。 532 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 

2 | 3 | English | 4 | 中文 | 5 | Русский | 6 | ภาษาไทย | 7 | Tiếng Việt 8 | 9 |
10 | (Please contribute translations!) 11 |

12 | 13 | # Unstated Next 14 | 15 | > 200 bytes to never think about React state management libraries ever again 16 | 17 | - **React Hooks** _use them for all your state management._ 18 | - **~200 bytes** _min+gz._ 19 | - **Familiar API** _just use React as intended._ 20 | - **Minimal API** _it takes 5 minutes to learn._ 21 | - **Written in TypeScript** _and will make it easier for you to type your React code._ 22 | 23 | But, the most important question: Is this better than Redux? Well... 24 | 25 | - **It's smaller.** _It's 40x smaller._ 26 | - **It's faster.** _Componentize the problem of performance._ 27 | - **It's easier to learn.** _You already will have to know React Hooks & Context, just use them, they rock._ 28 | - **It's easier to integrate.** _Integrate one component at a time, and easily integrate with every React library._ 29 | - **It's easier to test.** _Testing reducers is a waste of your time, make it easier to test your React components._ 30 | - **It's easier to typecheck.** _Designed to make most of your types inferable._ 31 | - **It's minimal.** _It's just React._ 32 | 33 | So you decide. 34 | 35 | ### [See Migration From Unstated docs →](#migration-from-unstated) 36 | 37 | ## Install 38 | 39 | ```sh 40 | npm install --save unstated-next 41 | ``` 42 | 43 | ## Example 44 | 45 | ```js 46 | import React, { useState } from "react" 47 | import { createContainer } from "unstated-next" 48 | import { render } from "react-dom" 49 | 50 | function useCounter(initialState = 0) { 51 | let [count, setCount] = useState(initialState) 52 | let decrement = () => setCount(count - 1) 53 | let increment = () => setCount(count + 1) 54 | return { count, decrement, increment } 55 | } 56 | 57 | let Counter = createContainer(useCounter) 58 | 59 | function CounterDisplay() { 60 | let counter = Counter.useContainer() 61 | return ( 62 |
63 | 64 | {counter.count} 65 | 66 |
67 | ) 68 | } 69 | 70 | function App() { 71 | return ( 72 | 73 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | render(, document.getElementById("root")) 86 | ``` 87 | 88 | ## API 89 | 90 | ### `createContainer(useHook)` 91 | 92 | ```js 93 | import { createContainer } from "unstated-next" 94 | 95 | function useCustomHook() { 96 | let [value, setValue] = useState() 97 | let onChange = e => setValue(e.currentTarget.value) 98 | return { value, onChange } 99 | } 100 | 101 | let Container = createContainer(useCustomHook) 102 | // Container === { Provider, useContainer } 103 | ``` 104 | 105 | ### `` 106 | 107 | ```js 108 | function ParentComponent() { 109 | return ( 110 | 111 | 112 | 113 | ) 114 | } 115 | ``` 116 | 117 | ### `` 118 | 119 | ```js 120 | function useCustomHook(initialState = "") { 121 | let [value, setValue] = useState(initialState) 122 | // ... 123 | } 124 | 125 | function ParentComponent() { 126 | return ( 127 | 128 | 129 | 130 | ) 131 | } 132 | ``` 133 | 134 | ### `Container.useContainer()` 135 | 136 | ```js 137 | function ChildComponent() { 138 | let input = Container.useContainer() 139 | return 140 | } 141 | ``` 142 | 143 | ### `useContainer(Container)` 144 | 145 | ```js 146 | import { useContainer } from "unstated-next" 147 | 148 | function ChildComponent() { 149 | let input = useContainer(Container) 150 | return 151 | } 152 | ``` 153 | 154 | ## Guide 155 | 156 | If you've never used React Hooks before, I recommend pausing and going to read 157 | through [the excellent docs on the React site](https://reactjs.org/docs/hooks-intro.html). 158 | 159 | So with hooks you might create a component like this: 160 | 161 | ```js 162 | function CounterDisplay() { 163 | let [count, setCount] = useState(0) 164 | let decrement = () => setCount(count - 1) 165 | let increment = () => setCount(count + 1) 166 | return ( 167 |
168 | 169 |

You clicked {count} times

170 | 171 |
172 | ) 173 | } 174 | ``` 175 | 176 | Then if you want to share the logic behind the component, you could pull it out 177 | into a custom hook: 178 | 179 | ```js 180 | function useCounter() { 181 | let [count, setCount] = useState(0) 182 | let decrement = () => setCount(count - 1) 183 | let increment = () => setCount(count + 1) 184 | return { count, decrement, increment } 185 | } 186 | 187 | function CounterDisplay() { 188 | let counter = useCounter() 189 | return ( 190 |
191 | 192 |

You clicked {counter.count} times

193 | 194 |
195 | ) 196 | } 197 | ``` 198 | 199 | But what if you want to share the state in addition to the logic, what do you do? 200 | 201 | This is where context comes into play: 202 | 203 | ```js 204 | function useCounter() { 205 | let [count, setCount] = useState(0) 206 | let decrement = () => setCount(count - 1) 207 | let increment = () => setCount(count + 1) 208 | return { count, decrement, increment } 209 | } 210 | 211 | let Counter = createContext(null) 212 | 213 | function CounterDisplay() { 214 | let counter = useContext(Counter) 215 | return ( 216 |
217 | 218 |

You clicked {counter.count} times

219 | 220 |
221 | ) 222 | } 223 | 224 | function App() { 225 | let counter = useCounter() 226 | return ( 227 | 228 | 229 | 230 | 231 | ) 232 | } 233 | ``` 234 | 235 | This is great, it's perfect, more people should write code like this. 236 | 237 | But sometimes we all need a little bit more structure and intentional API design in order to get it consistently right. 238 | 239 | By introducing the `createContainer()` function, you can think about your custom hooks as "containers" and have an API that's clear and prevents you from using it wrong. 240 | 241 | ```js 242 | import { createContainer } from "unstated-next" 243 | 244 | function useCounter() { 245 | let [count, setCount] = useState(0) 246 | let decrement = () => setCount(count - 1) 247 | let increment = () => setCount(count + 1) 248 | return { count, decrement, increment } 249 | } 250 | 251 | let Counter = createContainer(useCounter) 252 | 253 | function CounterDisplay() { 254 | let counter = Counter.useContainer() 255 | return ( 256 |
257 | 258 |

You clicked {counter.count} times

259 | 260 |
261 | ) 262 | } 263 | 264 | function App() { 265 | return ( 266 | 267 | 268 | 269 | 270 | ) 271 | } 272 | ``` 273 | 274 | Here's the diff of that change: 275 | 276 | ```diff 277 | - import { createContext, useContext } from "react" 278 | + import { createContainer } from "unstated-next" 279 | 280 | function useCounter() { 281 | ... 282 | } 283 | 284 | - let Counter = createContext(null) 285 | + let Counter = createContainer(useCounter) 286 | 287 | function CounterDisplay() { 288 | - let counter = useContext(Counter) 289 | + let counter = Counter.useContainer() 290 | return ( 291 |
292 | ... 293 |
294 | ) 295 | } 296 | 297 | function App() { 298 | - let counter = useCounter() 299 | return ( 300 | - 301 | + 302 | 303 | 304 | 305 | ) 306 | } 307 | ``` 308 | 309 | If you're using TypeScript (which I encourage you to learn more about if you are not), this also has the benefit of making TypeScript's built-in inference work better. As long as your custom hook is typed, then everything else will just work. 310 | 311 | ## Tips 312 | 313 | ### Tip #1: Composing Containers 314 | 315 | Because we're just working with custom React hooks, we can compose containers inside of other hooks. 316 | 317 | ```js 318 | function useCounter() { 319 | let [count, setCount] = useState(0) 320 | let decrement = () => setCount(count - 1) 321 | let increment = () => setCount(count + 1) 322 | return { count, decrement, increment, setCount } 323 | } 324 | 325 | let Counter = createContainer(useCounter) 326 | 327 | function useResettableCounter() { 328 | let counter = Counter.useContainer() 329 | let reset = () => counter.setCount(0) 330 | return { ...counter, reset } 331 | } 332 | ``` 333 | 334 | ### Tip #2: Keeping Containers Small 335 | 336 | This can be useful for keeping your containers small and focused. Which can be important if you want to code split the logic in your containers: Just move them to their own hooks and keep just the state in containers. 337 | 338 | ```js 339 | function useCount() { 340 | return useState(0) 341 | } 342 | 343 | let Count = createContainer(useCount) 344 | 345 | function useCounter() { 346 | let [count, setCount] = Count.useContainer() 347 | let decrement = () => setCount(count - 1) 348 | let increment = () => setCount(count + 1) 349 | let reset = () => setCount(0) 350 | return { count, decrement, increment, reset } 351 | } 352 | ``` 353 | 354 | ### Tip #3: Optimizing components 355 | 356 | There's no "optimizing" `unstated-next` to be done, all of the optimizations you might do would be standard React optimizations. 357 | 358 | #### 1) Optimizing expensive sub-trees by splitting the component apart 359 | 360 | **Before:** 361 | 362 | ```js 363 | function CounterDisplay() { 364 | let counter = Counter.useContainer() 365 | return ( 366 |
367 | 368 |

You clicked {counter.count} times

369 | 370 |
371 |
372 |
373 |
SUPER EXPENSIVE RENDERING STUFF
374 |
375 |
376 |
377 |
378 | ) 379 | } 380 | ``` 381 | 382 | **After:** 383 | 384 | ```js 385 | function ExpensiveComponent() { 386 | return ( 387 |
388 |
389 |
390 |
SUPER EXPENSIVE RENDERING STUFF
391 |
392 |
393 |
394 | ) 395 | } 396 | 397 | function CounterDisplay() { 398 | let counter = Counter.useContainer() 399 | return ( 400 |
401 | 402 |

You clicked {counter.count} times

403 | 404 | 405 |
406 | ) 407 | } 408 | ``` 409 | 410 | #### 2) Optimizing expensive operations with useMemo() 411 | 412 | **Before:** 413 | 414 | ```js 415 | function CounterDisplay(props) { 416 | let counter = Counter.useContainer() 417 | 418 | // Recalculating this every time `counter` changes is expensive 419 | let expensiveValue = expensiveComputation(props.input) 420 | 421 | return ( 422 |
423 | 424 |

You clicked {counter.count} times

425 | 426 |
427 | ) 428 | } 429 | ``` 430 | 431 | **After:** 432 | 433 | ```js 434 | function CounterDisplay(props) { 435 | let counter = Counter.useContainer() 436 | 437 | // Only recalculate this value when its inputs have changed 438 | let expensiveValue = useMemo(() => { 439 | return expensiveComputation(props.input) 440 | }, [props.input]) 441 | 442 | return ( 443 |
444 | 445 |

You clicked {counter.count} times

446 | 447 |
448 | ) 449 | } 450 | ``` 451 | 452 | #### 3) Reducing re-renders using React.memo() and useCallback() 453 | 454 | **Before:** 455 | 456 | ```js 457 | function useCounter() { 458 | let [count, setCount] = useState(0) 459 | let decrement = () => setCount(count - 1) 460 | let increment = () => setCount(count + 1) 461 | return { count, decrement, increment } 462 | } 463 | 464 | let Counter = createContainer(useCounter) 465 | 466 | function CounterDisplay(props) { 467 | let counter = Counter.useContainer() 468 | return ( 469 |
470 | 471 |

You clicked {counter.count} times

472 | 473 |
474 | ) 475 | } 476 | ``` 477 | 478 | **After:** 479 | 480 | ```js 481 | function useCounter() { 482 | let [count, setCount] = useState(0) 483 | let decrement = useCallback(() => setCount(count - 1), [count]) 484 | let increment = useCallback(() => setCount(count + 1), [count]) 485 | return { count, decrement, increment } 486 | } 487 | 488 | let Counter = createContainer(useCounter) 489 | 490 | let CounterDisplayInner = React.memo(props => { 491 | return ( 492 |
493 | 494 |

You clicked {props.count} times

495 | 496 |
497 | ) 498 | }) 499 | 500 | function CounterDisplay(props) { 501 | let counter = Counter.useContainer() 502 | return 503 | } 504 | ``` 505 | 506 | #### 4) Wrapping your elements with `useMemo()` 507 | 508 | [via Dan Abramov](https://github.com/facebook/react/issues/15156#issuecomment-474590693) 509 | 510 | **Before:** 511 | 512 | ```js 513 | function CounterDisplay(props) { 514 | let counter = Counter.useContainer() 515 | let count = counter.count 516 | 517 | return ( 518 |

You clicked {count} times

519 | ) 520 | } 521 | ``` 522 | 523 | **After:** 524 | 525 | ```js 526 | function CounterDisplay(props) { 527 | let counter = Counter.useContainer() 528 | let count = counter.count 529 | 530 | return useMemo(() => ( 531 |

You clicked {count} times

532 | ), [count]) 533 | } 534 | ``` 535 | 536 | ## Relation to Unstated 537 | 538 | I consider this library the spiritual successor to [Unstated](https://github.com/jamiebuilds/unstated). I created Unstated because I believed that React was really great at state management already and the only missing piece was sharing state and logic easily. So I created Unstated to be the "minimal" solution to sharing React state and logic. 539 | 540 | However, with Hooks, React has become much better at sharing state and logic. To the point that I think Unstated has become an unnecessary abstraction. 541 | 542 | **HOWEVER**, I think many developers have struggled seeing how to share state and logic with React Hooks for "application state". That may just be an issue of documentation and community momentum, but I think that an API could help bridge that mental gap. 543 | 544 | That API is what Unstated Next is. Instead of being the "Minimal API for sharing state and logic in React", it is now the "Minimal API for understanding shared state and logic in React". 545 | 546 | I've always been on the side of React. I want React to win. I would like to see the community abandon state management libraries like Redux, and find better ways of making use of React's built-in toolchain. 547 | 548 | If instead of using Unstated, you just want to use React itself, I would highly encourage that. Write blog posts about it! Give talks about it! Spread your knowledge in the community. 549 | 550 | ## Migration from `unstated` 551 | 552 | I've intentionally published this as a separate package name because it is a complete reset on the API. This way you can have both installed and migrate incrementally. 553 | 554 | Please provide me with feedback on that migration process, because over the next few months I hope to take that feedback and do two things: 555 | 556 | - Make sure `unstated-next` fulfills all the needs of `unstated` users. 557 | - Make sure `unstated` has a clean migration process towards `unstated-next`. 558 | 559 | I may choose to add APIs to either library to make life easier for developers. For `unstated-next` I promise that the added APIs will be as minimal as possible and I'll try to keep the library small. 560 | 561 | In the future, I will likely merge `unstated-next` back into `unstated` as a new major version. `unstated-next` will still exist so that you can have both `unstated@2` and `unstated-next` installed. Then when you are done with the migration, you can update to `unstated@3` and remove `unstated-next` (being sure to update all your imports as you do... should be just a find-and-replace). 562 | 563 | Even though this is a major new API change, I hope that I can make this migration as easy as possible on you. I'm optimizing for you to get to using the latest React Hooks APIs and not for preserving code written with `Unstated.Container`'s. Feel free to provide feedback on how that could be done better. 564 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { createContainer } from "../src/unstated-next" 3 | import { render } from "react-dom" 4 | 5 | function useCounter(initialState = 0) { 6 | let [count, setCount] = useState(initialState) 7 | let decrement = () => setCount(count - 1) 8 | let increment = () => setCount(count + 1) 9 | return { count, decrement, increment } 10 | } 11 | 12 | let Counter = createContainer(useCounter) 13 | 14 | function CounterDisplay() { 15 | let counter = Counter.useContainer() 16 | return ( 17 |
18 | 19 | {counter.count} 20 | 21 |
22 | ) 23 | } 24 | 25 | function App() { 26 | return ( 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | render(, document.getElementById("root")) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unstated-next", 3 | "version": "1.1.0", 4 | "description": "200 bytes to never think about React state management libraries ever again", 5 | "source": "src/unstated-next.tsx", 6 | "main": "dist/unstated-next.js", 7 | "module": "dist/unstated-next.mjs", 8 | "unpkg": "dist/unstated-next.umd.js", 9 | "types": "dist/unstated-next.d.ts", 10 | "amdName": "UnstatedNext", 11 | "sideEffects": false, 12 | "scripts": { 13 | "format": "prettier --write '**'", 14 | "example": "parcel example/index.html --out-dir example/dist", 15 | "build": "rm -rf dist && microbundle --external react --globals react=React --strict --no-compress", 16 | "test": "ava" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@types/react": "^16.8.15", 23 | "@types/react-dom": "^16.8.4", 24 | "ava": "^1.4.1", 25 | "husky": "^2.1.0", 26 | "lint-staged": "^8.1.5", 27 | "microbundle": "^0.11.0", 28 | "parcel": "^1.12.3", 29 | "prettier": "^1.17.0", 30 | "react": "^16.8.6", 31 | "react-dom": "^16.8.6", 32 | "ts-node": "^8.1.0", 33 | "typescript": "^3.4.5" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "lint-staged" 38 | } 39 | }, 40 | "lint-staged": { 41 | "*": [ 42 | "prettier --write", 43 | "git add" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/unstated-next.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const EMPTY: unique symbol = Symbol() 4 | 5 | export interface ContainerProviderProps { 6 | initialState?: State 7 | children: React.ReactNode 8 | } 9 | 10 | export interface Container { 11 | Provider: React.ComponentType> 12 | useContainer: () => Value 13 | } 14 | 15 | export function createContainer( 16 | useHook: (initialState?: State) => Value, 17 | ): Container { 18 | let Context = React.createContext(EMPTY) 19 | 20 | function Provider(props: ContainerProviderProps) { 21 | let value = useHook(props.initialState) 22 | return {props.children} 23 | } 24 | 25 | function useContainer(): Value { 26 | let value = React.useContext(Context) 27 | if (value === EMPTY) { 28 | throw new Error("Component must be wrapped with ") 29 | } 30 | return value 31 | } 32 | 33 | return { Provider, useContainer } 34 | } 35 | 36 | export function useContainer( 37 | container: Container, 38 | ): Value { 39 | return container.useContainer() 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | { 3 | "include": ["src"], 4 | "exclude": ["test", "example"], 5 | "compilerOptions": { 6 | /* Basic Options */ 7 | "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 8 | "module": "ES2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "incremental": true, /* Enable incremental compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | 28 | /* Strict Type-Checking Options */ 29 | "strict": true, /* Enable all strict type-checking options. */ 30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 31 | // "strictNullChecks": true, /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | } 65 | } 66 | --------------------------------------------------------------------------------