aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md467
1 files changed, 467 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2318f17
--- /dev/null
+++ b/README.md
@@ -0,0 +1,467 @@
+# Unstated Next
+
+> 200 bytes to never think about React state management libraries ever again
+
+- **React Hooks** _use them for all your state management._
+- **~200 bytes** _min+gz._
+- **Familiar API** _just use React as intended._
+- **Minimal API** _it takes 5 minutes to learn._
+- **Written in TypeScript** _and will make it easier for you to type your React code._
+
+But, the most important question: Is this better than Redux? Well...
+
+- **It's smaller.** _It's 40x smaller._
+- **It's faster.** _Componentize the problem of performance._
+- **It's easier to learn.** _You already will have to know React Hooks & Context, just use them, they rock._
+- **It's easier to integrate.** _Integrate one component at a time, and easily integrate with every React library._
+- **It's easier to test.** _Testing reducers is a waste of your time, make it easier to test your React components._
+- **It's easier to typecheck.** _Designed to make most of your types inferable._
+- **It's minimal.** _It's just React._
+
+So you decide.
+
+## Install
+
+```sh
+npm install --save unstated-next
+```
+
+## Example
+
+```js
+import React, { useState } from "react"
+import { createContainer } from "unstated-next"
+import { render } from "react-dom"
+
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count - 1)
+ return { count, decrement, increment }
+}
+
+let Counter = createContainer(useCounter)
+
+function CounterDisplay() {
+ let counter = Counter.useContainer()
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <span>{counter.count}</span>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+
+function App() {
+ return (
+ <Counter.Provider>
+ <CounterDisplay />
+ <CounterDisplay />
+ </Counter.Provider>
+ )
+}
+
+render(<App />, document.getElementById("root"))
+```
+
+## API
+
+### `createContainer(useHook)`
+
+```js
+import { createContainer } from "unstated-next"
+
+function useCustomHook() {
+ let [value, setInput] = useState()
+ let onChange = e => setValue(e.currentTarget.value)
+ return { value, onChange }
+}
+
+let Container = createContainer(useCustomHook)
+// Container === { Provider, useContainer }
+```
+
+### `<Container.Provider>`
+
+```js
+function ParentComponent() {
+ return (
+ <Container.Provider>
+ <ChildComponent />
+ </Container.Provider>
+ )
+}
+```
+
+### `Container.useContainer()`
+
+```js
+function ChildComponent() {
+ let input = Container.useContainer()
+ return <input value={input.value} onChange={input.onChange} />
+}
+```
+
+### `useContainer(Container)`
+
+```js
+import { useContainer } from "unstated-next"
+
+function ChildComponent() {
+ let input = useContainer(Container)
+ return <input value={input.value} onChange={input.onChange} />
+}
+```
+
+## Guide
+
+If you've never used React Hooks before, I recommend pausing and going to read
+through [the excellent docs on the React site](https://reactjs.org/docs/hooks-intro.html).
+
+So with hooks you might create a component like this:
+
+```js
+function CounterDisplay() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ return (
+ <div>
+ <button onClick={decrement}>-</button>
+ <p>You clicked {count} times</p>
+ <button onClick={increment}>+</button>
+ </div>
+ )
+}
+```
+
+Then if you want to share the logic behind the component, you could pull it out
+into a custom hook:
+
+```js
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ return { count, decrement, increment }
+}
+
+function CounterDisplay() {
+ let counter = useCounter()
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+```
+
+But what if you want to share the state in addition to the logic, what do you do?
+
+This is where context comes into play:
+
+```js
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ return { count, decrement, increment }
+}
+
+let Counter = createContext(null)
+
+function CounterDisplay() {
+ let counter = useContext(Counter)
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+
+function App() {
+ let counter = useCounter()
+ return (
+ <Counter.Provider value={counter}>
+ <CounterDisplay />
+ <CounterDisplay />
+ </Counter.Provider>
+ )
+}
+```
+
+This is great, it's perfect, more people should write code like this.
+
+But sometimes we all need a little bit more structure and intentional API design in order to get it consistently right.
+
+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.
+
+```js
+import { createContainer } from "unstated-next"
+
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ return { count, decrement, increment }
+}
+
+let Counter = createContainer(useCounter)
+
+function CounterDisplay() {
+ let counter = Counter.useContainer()
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+
+function App() {
+ return (
+ <Counter.Provider>
+ <CounterDisplay />
+ <CounterDisplay />
+ </Counter.Provider>
+ )
+}
+```
+
+Here's the diff of that change:
+
+```diff
+- import { createContext, useContext } from "react"
++ import { createContainer } from "unstated-next"
+
+ function useCounter() {
+ ...
+ }
+
+- let Counter = createContext(null)
++ let Counter = createContainer(useCounter)
+
+ function CounterDisplay() {
+- let counter = useContext(Counter)
++ let counter = Counter.useContainer()
+ return (
+ <div>
+ ...
+ </div>
+ )
+ }
+
+ function App() {
+- let counter = useCounter()
+ return (
+- <Counter.Provider value={counter}>
++ <Counter.Provider>
+ <CounterDisplay />
+ <CounterDisplay />
+ </Counter.Provider>
+ )
+ }
+```
+
+If you're using TypeScript (which I encourage to 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.
+
+## Tips
+
+### Tip #1: Composing Containers
+
+Because we're just working with custom React hooks, we can compose containers inside of other hooks.
+
+```js
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ return { count, decrement, increment, setCount }
+}
+
+let Counter = createContainer(useCounter)
+
+function useResettableCounter() {
+ let counter = Counter.useContainer()
+ let reset = () => counter.setCount(0)
+ return { ...counter, reset }
+}
+```
+
+### Tip #2: Keeping Containers Small
+
+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.
+
+```js
+function useCount() {
+ return useState(0)
+}
+
+let Count = createContainer(useCount)
+
+function useCounter() {
+ let [count, setCount] = Counter.useContainer()
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ let reset = () => setCount(0)
+ return { count, decrement, increment, reset }
+}
+```
+
+### Tip #3: Optimizing components
+
+There's no "optimizing" `unstated-next` to be done, all of the optimizations you might do would be standard React optimizations.
+
+#### 1) Optimizing expensive sub-trees by splitting the component apart
+
+**Before:**
+
+```js
+function CounterDisplay() {
+ let counter = Counter.useContainer()
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ <div>
+ <div>
+ <div>
+ <div>SUPER EXPENSIVE RENDERING STUFF</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+```
+
+**After:**
+
+```js
+function ExpensiveComponent() {
+ return (
+ <div>
+ <div>
+ <div>
+ <div>SUPER EXPENSIVE RENDERING STUFF</div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+function CounterDisplay() {
+ let counter = Counter.useContainer()
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ <ExpensiveComponent />
+ </div>
+ )
+}
+```
+
+#### 2) Optimizing expensive operations with useMemo()
+
+**Before:**
+
+```js
+function CounterDisplay(props) {
+ let counter = Counter.useContainer()
+
+ // Recalculating this every time `counter` changes is expensive
+ let expensiveValue = expensiveComputation(props.input)
+
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+```
+
+**After:**
+
+```js
+function CounterDisplay(props) {
+ let counter = Counter.useContainer()
+
+ // Only recalculate this value when its inputs have changed
+ let expensiveValue = useMemo(() => {
+ return expensiveComputation(props.input)
+ }, [props.input])
+
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+```
+
+#### 3) Reducing re-renders using React.memo() and useCallback()
+
+**Before:**
+
+```js
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = () => setCount(count - 1)
+ let increment = () => setCount(count + 1)
+ return { count, decrement, increment }
+}
+
+let Counter = createContainer(useCounter)
+
+function CounterDisplay(props) {
+ let counter = Counter.useContainer()
+ return (
+ <div>
+ <button onClick={counter.decrement}>-</button>
+ <p>You clicked {counter.count} times</p>
+ <button onClick={counter.increment}>+</button>
+ </div>
+ )
+}
+```
+
+**After:**
+
+```js
+function useCounter() {
+ let [count, setCount] = useState(0)
+ let decrement = useCallback(() => setCount(count - 1), [count])
+ let increment = useCallback(() => setCount(count + 1), [count])
+ return { count, decrement, increment }
+}
+
+let Counter = createContainer(useCounter)
+
+let CounterDisplayInner = React.memo(props => {
+ return (
+ <div>
+ <button onClick={props.decrement}>-</button>
+ <p>You clicked {props.count} times</p>
+ <button onClick={props.increment}>+</button>
+ </div>
+ )
+})
+
+function CounterDisplay(props) {
+ let counter = Counter.useContainer()
+ return <CounterDisplayInner {...counter} />
+}
+```