From 436fe821b81675f1aa6d03c30dc3a3604f8d2781 Mon Sep 17 00:00:00 2001 From: Jamie Kyle Date: Wed, 1 May 2019 12:17:21 -0700 Subject: init commit --- README.md | 467 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 README.md (limited to 'README.md') 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 ( +
+ + {counter.count} + +
+ ) +} + +function App() { + return ( + + + + + ) +} + +render(, 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 } +``` + +### `` + +```js +function ParentComponent() { + return ( + + + + ) +} +``` + +### `Container.useContainer()` + +```js +function ChildComponent() { + let input = Container.useContainer() + return +} +``` + +### `useContainer(Container)` + +```js +import { useContainer } from "unstated-next" + +function ChildComponent() { + let input = useContainer(Container) + return +} +``` + +## 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 ( +
+ +

You clicked {count} times

+ +
+ ) +} +``` + +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 ( +
+ +

You clicked {counter.count} times

+ +
+ ) +} +``` + +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 ( +
+ +

You clicked {counter.count} times

+ +
+ ) +} + +function App() { + let counter = useCounter() + return ( + + + + + ) +} +``` + +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 ( +
+ +

You clicked {counter.count} times

+ +
+ ) +} + +function App() { + return ( + + + + + ) +} +``` + +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 ( +
+ ... +
+ ) + } + + function App() { +- let counter = useCounter() + return ( +- ++ + + + + ) + } +``` + +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 ( +
+ +

You clicked {counter.count} times

+ +
+
+
+
SUPER EXPENSIVE RENDERING STUFF
+
+
+
+
+ ) +} +``` + +**After:** + +```js +function ExpensiveComponent() { + return ( +
+
+
+
SUPER EXPENSIVE RENDERING STUFF
+
+
+
+ ) +} + +function CounterDisplay() { + let counter = Counter.useContainer() + return ( +
+ +

You clicked {counter.count} times

+ + +
+ ) +} +``` + +#### 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 ( +
+ +

You clicked {counter.count} times

+ +
+ ) +} +``` + +**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 ( +
+ +

You clicked {counter.count} times

+ +
+ ) +} +``` + +#### 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 ( +
+ +

You clicked {counter.count} times

+ +
+ ) +} +``` + +**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 ( +
+ +

You clicked {props.count} times

+ +
+ ) +}) + +function CounterDisplay(props) { + let counter = Counter.useContainer() + return +} +``` -- cgit v1.2.3