200 bytes to control the state of React components
The main question is: how is this package better than Redux? Well...
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"))
I (Jamie Kyle - approx. Lane.) View this library as the successor to Unstated . I did Unstated, because I was convinced that React did a great job managing the state, and he lacked only a simple mechanism for separating the general state and logic. Therefore, I created Unstated as a "minimal" solution for this problem.
With the advent of hooks, React has become much better in terms of identifying a common state and logic. So much better that, from my point of view, Unstated has become an unnecessary abstraction.
HOWEVER , I believe that many developers have little idea how to separate the logic and general state of an application using React-hooks. This may simply be due to poor documentation quality and community inertia, but I believe that a clear API is able to correct this flaw.
Unstated Next is the very API. Instead of being "Minimal API for sharing common state and logic in React", now it is "Minimal API for understanding how to share common state and logic in React".
I really like React, I want React to flourish. I would prefer the community to stop using external libraries to manage state, like Redux, and finally start using the tools built into React in full force.
If instead of using Unstated, you will simply use React - I will only welcome it. Write about it in your blogs! Speak about it at conferences! Share your knowledge with the community.
If you are not familiar with React-hooks yet, I recommend stopping reading and getting acquainted with
excellent documentation on the React website .
So, with the help of hooks, you can write something like this component:
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> ) }
If component logic is required to be used in several places, it can be removed.
in a separate custom hook:
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 to do when you need a general state, and not just logic?
This is where the context comes in:
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> ) }
It is wonderful and beautiful; the more people write in this style, the better.
However, it is worth bringing a little more structure and clarity so that the API clearly expresses your intentions.
To do this, we added the createContainer()
function so that you can treat your custom hooks as "containers", so that our clear and clear API cannot be used incorrectly.
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> ) }
Compare the component text before and after our changes:
- 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 write in TypeScript (and if not, I strongly recommend that you familiarize yourself with it), you also get better type inference to everything else. If your custom hook is strongly typed, the output of all other types will work automatically.
createContainer(useHook)
import { createContainer } from "unstated-next" function useCustomHook() { let [value, setValue] = useState() let onChange = e => setValue(e.currentTarget.value) return { value, onChange } } let Container = createContainer(useCustomHook) // Container === { Provider, useContainer }
<Container.Provider>
function ParentComponent() { return ( <Container.Provider> <ChildComponent /> </Container.Provider> ) }
Container.useContainer()
function ChildComponent() { let input = Container.useContainer() return <input value={input.value} onChange={input.onChange} /> }
useContainer(Container)
import { useContainer } from "unstated-next" function ChildComponent() { let input = useContainer(Container) return <input value={input.value} onChange={input.onChange} /> }
Since we are dealing with custom hooks, we can combine containers inside other hooks.
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 } }
Containers are best made small and clearly focused on a specific task. If you need additional business logic in containers, take out new operations in separate hooks, and let the state be stored in containers.
function useCount() { return useState(0) } let Count = createContainer(useCount) function useCounter() { let [count, setCount] = Count.useContainer() let decrement = () => setCount(count - 1) let increment = () => setCount(count + 1) let reset = () => setCount(0) return { count, decrement, increment, reset } }
There is no separate "optimization" for unstated-next
, rather ordinary methods of optimizing React components.
Before:
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> </div> </div> </div> </div> </div> ) }
After:
function ExpensiveComponent() { return ( <div> <div> <div> <div> </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> ) }
Before:
function CounterDisplay(props) { let counter = Counter.useContainer() // , `counter` — 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:
function CounterDisplay(props) { let counter = Counter.useContainer() // , 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> ) }
Before:
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:
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} /> }
unstated
I purposely publish this library as a separate package, because the entire API is completely new. Therefore, you can install both packages in parallel and migrate gradually.
Share your impressions about the transition to unstated-next
, because in the next few months I plan to do two things based on this information:
unstated-next
meets all unstated
user needs.unstated
there is a clear and clear migration process to unstated-next
.Perhaps I will add some API to the old or new library to simplify the life of the developers. As for the unstated-next
, I promise that the added APIs will be as minimal as possible, and I will do my best to keep the library small.
In the future, I will probably transfer the unstated-next
code back to unstated
as a new major version. unstated-next
will still be available so that you can use unstated@2
and unstated-next
in parallel in one project. Then, when you finish the migration, you can upgrade to the unstated@3
version and delete unstated-next
(of course, updating all the imports ... there should be enough search and replace).
Despite the fundamental change of the API, I hope that I can provide you with the easiest possible migration, as far as possible. I would welcome any comments on what could be done better.
Source: https://habr.com/ru/post/451410/
All Articles