diff options
author | Jamie Kyle <me@thejameskyle.com> | 2019-05-01 22:17:21 +0300 |
---|---|---|
committer | Jamie Kyle <me@thejameskyle.com> | 2019-05-02 03:45:10 +0300 |
commit | 436fe821b81675f1aa6d03c30dc3a3604f8d2781 (patch) | |
tree | bc750c1728f90ef75f4b266c25b3ee6707c1bb18 /README.md |
init commit
Diffstat (limited to 'README.md')
-rw-r--r-- | README.md | 467 |
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} /> +} +``` |